Search code examples
f#computation-expression

F# Computation Expression to build state and defer execution


I am looking to build a computation expression where I can express the following:

let x = someComputationExpression {
    do! "Message 1"
    printfn "something 1"
    do! "Message 2"
    printfn "something 2"
    do! "Message 3"
    printfn "something 3"
    let lastValue = 4
    do! "Message 4"
    // need to reference values across `do!`
    printfn "something %s" lastValue
}

and be able to take from x a list:

[| "Message 1"
   "Message 2"
   "Message 3"
   "Message 4" |]

without printfn ever getting called, but with the ability to later execute it (if that makes sense).

It doesn't need to be with the do! keyword, it could be yield or return, whatever is required for it to work.

To put it another way, I want to be able to collect some state in a computation express, and queue up work (the printfns) that can be executed later.

I have tried a few things, but am not sure it's possible.


Solution

  • It's a bit hard to figure out a precise solution from the OP question. Instead I am going to post some code that the OP perhaps can adjust to the needs.

    I define Result and ResultGenerator

    type Result =
      | Direct  of string
      | Delayed of (unit -> unit)
    
    type ResultGenerator<'T> = G of (Result list -> 'T*Result list )
    

    The generator produces a value and a list of direct and delayed values, the direct values are the string list above but intermingled with them are the delayed values. I like returning intermingled so that the ordering is preserved.

    Note this is a version of what is sometimes called a State monad.

    Apart from the class CE components like bind and Builders I created two functions direct and delayed.

    direct is used to create a direct value and delayed a delayed one (takes a function)

    let direct v : ResultGenerator<_> =
      G <| fun rs ->
        (), Direct v::rs
    
    let delayed d : ResultGenerator<_> =
      G <| fun rs ->
        (), Delayed d::rs
    

    To improve the readability I defined delayed trace functions:

    let trace m : ResultGenerator<_> =
      G <| fun rs ->
        (), Delayed (fun () -> printfn "%s" m)::rs
    
    let tracef fmt = kprintf trace fmt
    

    From an example generator:

    let test =
      builder {
        do! direct "Hello"
        do! tracef "A trace:%s" "!"
        do! direct "There"
        return 123
      }
    

    The following result was achieved:

    (123, [Direct "Hello"; Delayed <fun:trace@37-1>; Direct "There"])
    

    (Delayed will print the trace when executed).

    Hope this can give some ideas on how to attack the actual problem.

    Full source:

    open FStharp.Core.Printf
    
    type Result =
      | Direct  of string
      | Delayed of (unit -> unit)
    
    type ResultGenerator<'T> = G of (Result list -> 'T*Result list )
    
    let value v : ResultGenerator<_> =
      G <| fun rs ->
        v,  rs
    
    let bind (G t) uf : ResultGenerator<_> =
      G <| fun rs ->
        let tv, trs = t rs
        let (G u) = uf tv
        u trs
    
    let combine (G t) (G u) : ResultGenerator<_> =
      G <| fun rs ->
        let _, trs = t rs
        u trs
    
    let direct v : ResultGenerator<_> =
      G <| fun rs ->
        (), Direct v::rs
    
    let delayed d : ResultGenerator<_> =
      G <| fun rs ->
        (), Delayed d::rs
    
    let trace m : ResultGenerator<_> =
      G <| fun rs ->
        (), Delayed (fun () -> printfn "%s" m)::rs
    
    let tracef fmt = kprintf trace fmt
    
    type Builder() =
      class
        member x.Bind       (t, uf) = bind t uf
        member x.Combine    (t, u)  = combine t u
        member x.Return     v       = value v
        member x.ReturnFrom t       = t : ResultGenerator<_>
      end
    
    let run (G t) =
      let v, rs = t []
      v, List.rev rs
    
    let builder = Builder ()
    
    let test =
      builder {
        do! direct "Hello"
        do! tracef "A trace:%s" "!"
        do! direct "There"
        return 123
      }
    
    [<EntryPoint>]
    let main argv =
      run test |> printfn "%A"
      0