Having Fun with Computation Expressions

Or how I build a generic computation builder library.

Over the last days I build a F# API for google music by using gmusicapi and pythonnet (two awesome projects by the way). The Python C-API requires you to request the GIL (Global Interpreter Lock) before you can safely use the API. Because I knew I would forget to do this all over the place I decided to mark those places explicitly with the help of computation expressions. Doing this on a low level means I can safely use it on higher levels.

I decided to build the computation expression like this:

type PythonData<'a> =
  private { 
    Delayed : (unit -> 'a)
    mutable Cache : 'a option
  }
let pythonFunc f = { Delayed = f; Cache = None }
let internal unsafeExecute (f:PythonData<_>) =
  match f.Cache with
  | Some d -> d
  | None ->
    let res = f.Delayed()
    f.Cache <- Some res
    res
let private getPythonData = unsafeExecute
let runInPython f = 
  use __ = Python.Runtime.Py.GIL()
  f |> getPythonData

The builder is straightforward from there (see link above).

Of course now we need to interact with sequences and er need something like FSharp.Control.AsyncSeq. Basically all I had to do was copy the code from there and replace the builder.

Wait… What? We will look into this later.

Now I got really curious, I really only want to replace the ‘runInPython’ function and there is nothing specific about my python problem in the builders. Can we be more generic here? Just adding more run functions is not really practical as then users can just use the wrong one…

Let the fun begin… Lets first start with a general purpose delayed builder and lets see what we can do from there:


type Delayed<'a> =
  private { 
    Delayed : (unit -> 'a)
    mutable Cache : 'a option
  } 
  
module Delayed =
  let create f = { Delayed = f; Cache = None }
  let execute (f:Delayed<_>) =
    match f.Cache with
    | Some d -> d
    | None ->
      let res = f.Delayed()
      f.Cache <- Some res
      res
      
  let map f d =
    (fun () -> f (d |> execute)) |> create

type ConcreteDelayedBuilder() =
	let create f = Delayed.create f
	let execute e = Delayed.execute e
	member x.Bind(d, f) =
		(fun () -> 
			let r = d |> execute
			f r |> execute
			) |> create

	member x.Return(d) = 
		(fun () -> d) |> create
	member x.ReturnFrom (d) = d
	member x.Delay (f) = 
		(fun () -> f() |> execute) |> create
	member x.Combine (v, next) = x.Bind(v, fun () -> next)
	member x.Run (f) = f
	member x.Zero () = (fun () -> ()) |> create
	member x.TryWith (d, recover) =
		(fun () -> 
		try
			d |> execute
		with e -> recover e |> execute) |> create
	member x.TryFinally (d, final) =
		(fun () -> 
		try
			d |> execute
		finally final ()) |> create
	member x.While (condF, body) =
		(fun () -> 
		while condF() do
			body |> execute) |> create
	member x.Using (var, block) =
		(fun () -> 
		use v = var
		block v |> execute) |> create
	member x.For (seq, action) = 
		(fun () -> 
		for item in seq do
			action item |> execute) |> create

let delayed = ConcreteDelayedBuilder()

Ok looks good: We have a simple delayed builder. What we want now is some kind of converter to convert this Delayed<'T>' in a PythonData<’T>`

I would design the type like this:

  type PythonData<'T> = private { D : Delayed<'T> }
  let runInPython f = 
    use __ = Python.Runtime.Py.GIL()
    f.D |> Delayed.execute

Therefore callers cannot use the underlying Delayed object. But how do we generically get a computation builder and how would the result look like?

We would like to build a generic (computation expression builder) type with some kind of converter parameter which itself calls the regular delayed builder.

type IDelayedConverter<'b> =
	member ToDelayed : 'b<'a> -> Delayed<'a>
	member OfDelayed : Delayed<'a> -> 'b<'a>

Something like this is what we want, but sadly this is not possible (see uservoice). Can we work around this limitation? I decided to use a interface with marker classes for this. If you have a better idea let me know!

type IDelayed<'b, 'a> = interface end
type DefaultMarker = class end 
type Delayed<'a> =
  private { 
    Delayed : (unit -> 'a)
    mutable Cache : 'a option
  } with
  interface IDelayed<DefaultMarker, 'a>

/// Ideally we want 'b to be a type constructor and return 'b<'a>...
type IDelayedConverter<'b> =
  abstract ToDelayed : IDelayed<'b, 'a> -> Delayed<'a>
  abstract OfDelayed : Delayed<'a> -> IDelayed<'b, 'a>

Now we can change our computation builder to take an instance of a converter:

type ConcreteDelayedBuilder<'b>(conv : IDelayedConverter<'b>) =
    let execute a = a |> conv.ToDelayed |> Delayed.execute
    let create f = f |> Delayed.create |> conv.OfDelayed

	// .. Continue with the old code.

Which leads to our default instance like this:

  // Add to the Delayed module...
  let conv =
    { new IDelayedConverter<DefaultMarker> with
       member x.ToDelayed p = (p :?> Delayed<'a>)
       member x.OfDelayed d = d :> IDelayed<DefaultMarker, _> }


[<AutoOpen>]
module DelayedExtensions =
  
  let delayed = ConcreteDelayedBuilder(Delayed.conv)

Nice! Now we can create the python builder like this:

module Python =
  type PythonDataMarker = class end 
  type PythonData<'T> = private { D : Delayed<'T> } with
    interface IDelayed<PythonDataMarker, 'T>
  let internal pythonConv =
    { new IDelayedConverter<PythonDataMarker> with
       member x.ToDelayed p = (p :?> PythonData<'a>).D
       member x.OfDelayed d = { D = d } :> IDelayed<PythonDataMarker, _> }
  let runInPython f = 
    use __ = Python.Runtime.Py.GIL()
    pythonConv.ToDelayed f |> Delayed.execute
  
  let python = ConcreteDelayedBuilder(pythonConv)

A bit of setup but wow we almost made it. What we now want is the pythonSeq or delayedSeq computation builder. When we think about it we want a generic builder which takes the regular builder as parameter.

Oh that sounds like it will create a bunch of problems, but lets start to convert the AsyncSeq code. In theory all we need to do now is copy the AsyncSeq code and replace

First problem: We cannot use modules to define our functionality because we actually have a parameter (the underlying builder).

So lets start with the interfaces

type IDelayedEnumerator<'b, 'T> =
  abstract MoveNext : unit -> IDelayed<'b, 'T option>
  inherit System.IDisposable

type IDelayedEnumerable<'b, 'T> =
  abstract GetEnumerator : unit -> IDelayedEnumerator<'b, 'T>

type DelayedSeq<'b, 'T> = IDelayedEnumerable<'b, 'T>

This will do.

Lets start with empty (from here):

type DelayedSeqBuilder<'b>(builder : ConcreteDelayedBuilder<'b>) =
  //[<GeneralizableValue>]
  member x.empty<'T> () : DelayedSeq<'b, 'T> = 
        { new IDelayedEnumerable<'b, 'T> with 
              member x.GetEnumerator() = 
                  { new IDelayedEnumerator<'b, 'T> with 
                        member x.MoveNext() = builder { return None }
                        member x.Dispose() = () } }
 

Ok, it actually compiles. This is a good step forward. There we go: A computation builder as a parameter. The next special thing happens when they define helper types within their module. We cannot do this in a type. Therefore we will just move all the helpers above the DelayedSeqBuilder<'b> type and mark them as internal (see here).

Why did I name this type DelayedSeqBuilder<'b> and not like the corresponsing module? Because we cannot define the computation builder inside. Instead we will define all the functions here as members. This will later make the following possible:

  let seq =
    pythonSeq {
      for i in [1, 2, 3] do
        // Call Python API
        let! t = tf
        yield t + "test"
    }

  let first =
    seq
    |> pythonSeq.firstOrDefault "default"

But we are not there yet. So instead of defining the AsyncSeqBuilder inside the module we will just define everything in the builder itself. We are now here in AsyncSeq, or here with the port.

Now they do something which is not good for us. They define asyncSeq inside the module to define the higher level functionality as extension methods later. One example is the emitEnumerator function. We cannot create an instance, because we don’t know the paramter. But wait we already have an this (x) reference to ourself. What if we use that?

  member x.emitEnumerator (ie: IDelayedEnumerator<'b, 'T>) = x {
      let! moven = ie.MoveNext() 
      let b = ref moven 
      while b.Value.IsSome do
          yield b.Value.Value 
          let! moven = ie.MoveNext() 
          b := moven }

And again it compiles! This is crasy. With this (!) we can easily port the rest of the functionality.

Now we just need to extend the regular builder with the async for. But this is straightforward as well:

[<AutoOpen>]
module DelayedSeqExtensions =
  // Add asynchronous for loop to the 'async' computation builder
  type ConcreteDelayedBuilder<'b> with
    member internal x.For (seq:DelayedSeq<'b, 'T>, action:'T -> IDelayed<'b, unit>) =
      let seqBuilder = DelayedSeqBuilder(x)
      seq |> seqBuilder.iterDelayedData action 

We can define the final functionality by using both builders. But wait there is one more problem. When we define extension methods for the DelayedSeqBuilder<'b> type we cannot access the underlying builder parameter anymore. So lets add a property to access it:

type DelayedSeqBuilder<'b>(builder : ConcreteDelayedBuilder<'b>) =
  // ... 
  member x.Builder = builder
// ...
[<AutoOpen>]
module DelayedSeqExtensions =
  // ...
  type DelayedSeqBuilder<'b> with
    member x.tryLast (source : DelayedSeq<'b, 'T>) = x.Builder { 
        use ie = source.GetEnumerator() 
        let! v = ie.MoveNext()
        let b = ref v
        let res = ref None
        while b.Value.IsSome do
            res := b.Value
            let! moven = ie.MoveNext()
            b := moven
        return res.Value }

And we can finaly create the computation builder instances:

  let delayedSeq = new DelayedSeqBuilder<_>(delayed)

And for our python use case:

  let python = ConcreteDelayedBuilder(pythonConv)
  let pythonSeq = DelayedSeqBuilder(python)

What have we done here:

It’s really sad that we cannot define the DelayedSeqBuilder more generically. I’m pretty sure that would be usable for some exting computation builders as well. Maybe there is something we can do with type providers here ;).

This is all for now :). You can use this in your code via nuget. Of course the code is on github.