F# "Stateful" Computation Expression

621 Views Asked by At

I'm currently learning F# and hitting a few stumbling blocks; I think a lot of it is learning to think functionally.

One of the things I'm learning at the moment are computation expressions, and I want to be able to define a computation expression that handles some tracking state, e.g:

let myOptions = optionListBuilder {
    let! opt1 = {name="a";value=10}
    let! opt2 = {name="b";value=12}
}

I want to be able to have it so that myOptions is a Option<'T> list, so each let! bind operation effectively causes the builder to "track" the defined options as it goes along.

I don't want to have to do it using mutable state - e.g. having a list maintained by the builder and updated with each bind call.

Is there some way of having it so that this is possible?


Update: The resultant Option<'T> list type is just representative, in reality I'll likely have an OptionGroup<'T> type to contain a list as well as some additional information - so as Daniel mentioned below, I could use a list comprehension for a simple list.

3

There are 3 best solutions below

1
On BEST ANSWER

I wrote a string builder computation expression here.

open System.Text

type StringBuilderUnion =
| Builder of StringBuilder
| StringItem of string

let build sb =
    sb.ToString()

type StringBuilderCE () =
    member __.Yield (txt : string) = StringItem(txt)
    member __.Yield (c : char) = StringItem(c.ToString())
    member __.Combine(f,g) = Builder(match f,g with
                                     | Builder(F),   Builder(G)   ->F.Append(G.ToString())
                                     | Builder(F),   StringItem(G)->F.Append(G)
                                     | StringItem(F),Builder(G)   ->G.Append(F)
                                     | StringItem(F),StringItem(G)->StringBuilder(F).Append(G))
    member __.Delay f = f()
    member __.Zero () = StringItem("")
    member __.For (xs : 'a seq, f : 'a -> StringBuilderUnion) =
                    let sb = StringBuilder()
                    for item in xs do
                        match f item with
                        | StringItem(s)-> sb.Append(s)|>ignore
                        | Builder(b)-> sb.Append(b.ToString())|>ignore
                    Builder(sb)

let builder1 = new StringBuilderCE ()

Noticed the underlying type is immutable (the contained StringBuilder is mutable, but it doesn't have to be). Instead of updating the existing data, each yield combines the current state and the incoming input resulting in a new instance of StringBuilderUnion You could do this with an F# list since adding an element to the head of the list is merely the construction of a new value rather than mutating the existing values.

Using the StringBuilderCE looks like this:

//Create a function which builds a string from an list of bytes
let bytes2hex (bytes : byte []) =
    string {
        for byte in bytes -> sprintf "%02x" byte
    } |> build

//builds a string from four strings
string {
        yield "one"
        yield "two"
        yield "three"
        yield "four"
    } |> build

Noticed the yield instead of let! since I don't actually want to use the value inside the computation expression.

4
On

F# supports list comprehensions out-of-the-box.

let myOptions =
    [
        yield computeOptionValue()
        yield computeOptionValue()
    ]
3
On

SOLUTION

With the base-line StringBuilder CE builder provided by mydogisbox, I was able to produce the following solution that works a charm:

type Option<'T> = {Name:string;Item:'T}

type OptionBuilderUnion<'T> =
    | OptionItems of Option<'T> list
    | OptionItem of Option<'T>

type OptionBuilder () =
    member this.Yield (opt: Option<'t>) = OptionItem(opt)
    member this.Yield (tup: string * 't) = OptionItem({Name=fst tup;Item=snd tup})
    member this.Combine (f,g) = 
        OptionItems(
            match f,g with
            | OptionItem(F), OptionItem(G) -> [F;G]
            | OptionItems(F), OptionItem(G) -> G :: F
            | OptionItem(F), OptionItems(G) -> F :: G
            | OptionItems(F), OptionItems(G) -> F @ G
        )
    member this.Delay f = f()
    member this.Run (f) = match f with |OptionItems items -> items |OptionItem item -> [item]

let options = OptionBuilder()

let opts = options {
        yield ("a",12)
        yield ("b",10)
        yield {Name = "k"; Item = 20}
    }

opts |> Dump