2

I'm working on some DSL for my application and here's how I defined computation type and builder:

// expression type
type Action<'a,'b> = Action of ('a -> Async<'b>)

let runAction (Action r) ctx = r ctx
let returnF a = Action (fun _ -> async {return a})
let bind m f = Action (fun r -> async {
    let! a = runAction m r in return! runAction (f a) r
    })

let bindA ac f = Action (fun r -> async {
    let! a = ac in return! runAction (f a) r
    })

type ActionBuilder<'x>() =
  member this.Return(c) = returnF c
  member this.Zero()    = returnF ()
  member this.Delay(f)  = bind (returnF ()) f

  // binds both monadic and for async computations
  member this.Bind(m, f) = bind m f
  member this.Bind(m, f) = bindA m f

  member this.Combine(r1, r2) = bind r1 (fun () -> r2)
  member this.For(s:seq<_>, f)  = Action (fun x -> async {
    for i in s do runAction (f i) x |> ignore
    })

  // here's the attempt to implement 'need' operations

  [<CustomOperation("need")>]
  member this.Need(Action a,  targets: string list) =
    Action (fun x ->
      let r = a x
      printfn "need(%A, [%A])" a targets
      r)

   member this.For(a, f)  = bindA a f
   member this.Yield(()) =
    returnF ()

let action = ActionBuilder<string>()

/////////////////////////////////////////////////////////////
// other functions for Action

/// Gets action context
let getCtx = Action (fun ctx -> async {return ctx})


let needFn res = action {
    let! ctx = getCtx
    printfn "need([%A]) in %A" res ctx
  }

The resulting code is supposed to be:

let program1 = fun filename -> action {
  let! a = async {return 123}
  let f = a+1

  // need ["def"; "dd"]
  do! needFn ["def"; "dd"]
  printfn "after need"

  for i in [0..10] do
    do! Async.Sleep (1)
    printfn "i: %A" i

  let! d = async {return f}
  let! ctx = getCtx
  printfn "ctx: %A, %A" ctx f
}

Async.RunSynchronously(runAction (program1 "m.c") "abc")

Now I would like to change do! needFn ["def"; "dd"] syntax to a nicer one by defining "need" custom operation, but getting various complains from compiler. Is it correct approach or I'm misusing the computation expressions?

The other issue is that for does not work if do! is used inside loop body.

olegz
  • 1,189
  • 10
  • 20

1 Answers1

1

After reading papers, by trial and error method I came to the following for implementation (Yield builder method is not required):

    let forF (e: seq<_>) prog =
    usingF (e.GetEnumerator()) (fun e ->
        whileF
            (fun () -> e.MoveNext())
            ((fun () -> prog e.Current) |> delayF)
    )

Full source code for computation expression builder could be found in the target project. The whole project is a variation of Fake build system.

Note: Action was renamed to Recipe. need operator cannot be implemented at all.

olegz
  • 1,189
  • 10
  • 20
  • Just a note for posterity: if you write a DSL, i.e. just a bunch of `CustomOperator` stuff in a class, all you need is `Yield(())`, which is your entry point. No need for `For` or any of the other default methods. – Abel Aug 31 '22 at 14:37
  • @Abel you must have meant declarative DSLs unlike bound calculations (with Bind). In that case both While and For are of course redundant. – olegz Sep 05 '22 at 05:51
  • 1
    Correct, that's what I meant with "if you write a DSL", I wasn't very precise in my language ;). – Abel Sep 06 '22 at 10:50