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