2

I am trying to make a builder using FSharp Computation Expression, but get error FS0039:

type UpdatebBuilder() =
    member this.Yield (x) = x
    member this.Return (x) = x
    member this.Bind (x, cont) = cont(x)
    member this.Quote (x) = x
    member this.For (x, a) = x

    [<CustomOperation("set", MaintainsVariableSpace =true,AllowIntoPattern=true)>] 
    member this.Set (x, a, b) = x

let update = UpdatebBuilder()

let testUpdate () =
    update {
        for x in [| 1; 2 ; 3|] do
        set x 123  // Compile Error FS0039: The value or constructor 'x' is not defined.
    }

What I want to implement is something like query expression:

query {
    for x in collection do
    where x = 2 // Why no FS0039 error here?
    select x
}

Also tried MaintainsVariableSpaceUsingBind=true, and get same error. What should I do to make it compile?

Chen
  • 1,654
  • 2
  • 13
  • 21

1 Answers1

2

To me it looks like you are trying to define a State monad and implementing the Set operation as a custom operation.

I will admit I never fully got my head around custom operations in F# (and I used F# alot). IMHO it feels like custom operations had one purpose; enable a LINQ like syntax in F#. As time goes in it seems few C# developers are using the LINQ like syntax (ie from x where y select z) and few F# developers are using the query computation expression. I have no data here but just goes from example code I see.

This could explain why the documentation on custom operations are often succinct and hard to grasp. What does this even mean? MaintainsVariableSpaceUsingBind: Indicates if the custom operation maintains the variable space of the query or computation expression through the use of a bind operation.

Anyway, so in order to learn a bit more about custom operations I tried to implement the state monad with a custom operation for set and I got a bit farther but ran into a problem which I think is an intentional limitation of the compiler. Still thought I share it with the hope that it helps OP get a bit further.

I chose this definition for State<_>:

type [<Struct>] State<'T> = S of (Map<string, obj> -> 'T*Map<string, obj>)

State<_> is a function that given a global state (a map) produces a value (that could derive from the global state but not necessarily) and a potentially updated global state.

return or value as I tend to call it as return is an F# keyword is easy to define as we just return v and the non-updated global state:

let value v         = S <| fun m -> v, m

bind is useful to bind several state computations together. First run t on the global state and from the returned value create the second computation and run the updated global state through it:

let bind  uf (S t)  = S <| fun m -> 
  let tv, tm  = t m
  let (S u)   = uf tv
  u tm

get and set are used to interact with the global state:

let get k : State<'T option> = S <| fun m ->
  match m |> Map.tryFind k with
  | Some (:? 'T as v) -> Some v, m
  | _                 -> None, m

let set k v = S <| fun m ->
  let m = m |> Map.add k (box v)
  (), m

I created some other methods as well but in the end the builder was created like this:

type Builder() =
  class
    member x.Bind       (t, uf) = bind    uf t
    member x.Combine    (t, u)  = combine u  t
    member x.Delay      tf      = delay   tf
    member x.For        (s, tf) = forEach s  tf
    member x.Return     v       = value   v
    member x.ReturnFrom t       = t             : State<'T>
    member x.Yield      v       = value   v
    member x.Zero ()            = value   ()

    [<CustomOperation("set", MaintainsVariableSpaceUsingBind = true)>] 
    member x.Set (s, k, v)      = s |> combine (set k v)
  end

I used MaintainsVariableSpaceUsingBind because otherwise it doesn't see v. MaintainsVariableSpace yields strange errors asking for seq types which I vaguely suspect is an optimization for computations based around seq. Checking the generated code is seems to do the right thing in that it binds the custom operations together using my bind function in the proper order.

I am now ready to do define a state computation

state {
  // Works fine
  set "key" -1
  for v in 0..2 do
    // Won't work because: FS3086: A custom operation may not be used in conjunction with 'use', 'try/with', 'try/finally', 'if/then/else' or 'match' operators within this computation expression
    set "hello" v
  return! State.get "key"
}

Unfortunately the compiler stops me from using custom ops in conditional operations like if, try and also for (even though it's not in the list it's conditional in some sense). This seems to be an intentional limitation. It's possible to workaround it but it feels meh

state {
  set "key" -1
  for v in 0..2 do
    // Meh
    do! state { set "key" v }
  return! State.get "key"
}

IMHO I prefer just using normal do!/let! over custom operations:

state {
  for v in 0..2 do
    do! State.set "key" v
  return! State.get "key"
}

So not really a proper answer to the question from OP but perhaps it can help you get a bit further?

Full source code:

type [<Struct>] State<'T> = S of (Map<string, obj> -> 'T*Map<string, obj>)

module State =
  let value v         = S <| fun m -> v, m

  let bind  uf (S t)  = S <| fun m -> 
    let tv, tm  = t m
    let (S u)   = uf tv
    u tm

  let combine u (S t) = S <| fun m -> 
    let _, tm   = t m
    let (S u)   = u
    u tm

  let delay tf  = S <| fun m -> 
    let (S t) = tf ()
    t m

  let forEach s tf  = S <| fun m -> 
    let mutable a = m
    for v in s do
      let (S t)   = tf v
      let (), tm  = t m
      a <- tm
    (), a

  let get k : State<'T option> = S <| fun m ->
    match m |> Map.tryFind k with
    | Some (:? 'T as v) -> Some v, m
    | _                 -> None, m

  let set k v = S <| fun m ->
    let m = m |> Map.add k (box v)
    (), m

  let run (S t) m = t m

  type Builder() =
    class
      member x.Bind       (t, uf) = bind    uf t
      member x.Combine    (t, u)  = combine u  t
      member x.Delay      tf      = delay   tf
      member x.For        (s, tf) = forEach s  tf
      member x.Return     v       = value   v
      member x.ReturnFrom t       = t             : State<'T>
      member x.Yield      v       = value   v
      member x.Zero ()            = value   ()

      [<CustomOperation("set", MaintainsVariableSpaceUsingBind = true)>] 
      member x.Set (s, k, v)      = s |> combine (set k v)
    end
let state = State.Builder ()

let testUpdate () =
  state {
    // Works fine
    set "key" -1
    for v in 0..2 do
      // Won't work because: FS3086: A custom operation may not be used in conjunction with 'use', 'try/with', 'try/finally', 'if/then/else' or 'match' operators within this computation expression
      // set "hello" v
      // Workaround but kind of meh
      // do! state { set "key" v }
      // Better IMHO
      do! State.set "key" v
    return! State.get "key"
  }

[<EntryPoint>]
let main argv =
  let tv, tm = State.run (testUpdate ()) Map.empty
  printfn "v:%A" tv
  printfn "m:%A" tm
  0
  • 2
    Ok, the key point is `[]` attribute. I have to add this attribute to the second parameter of `set` method to make it compiles. Your answer also helps alot, thank you! – Chen Dec 28 '19 at 14:51