1
votes

I have this code:

Ok stringBuffer {
    let r = get some list....
    match r with
    | [] -> "no active tasks"
    | r  -> String.Join("\n", r)
}

with stringBuffer defined as:

[<AutoOpen>]
module StringBuffer =
    type StringBuffer = StringBuilder -> unit

    type StringBufferBuilder () =
        member inline this.Yield (txt: string)           = fun (b: StringBuilder) -> Printf.bprintf b "%s" txt
        member inline this.Yield (c: char)               = fun (b: StringBuilder) -> Printf.bprintf b "%c" c
        member inline this.Yield (strings: #seq<string>) = fun (b: StringBuilder) -> for s in strings do Printf.bprintf b "%s\n" s
        member inline this.YieldFrom (f: StringBuffer)   = f

        member this.Combine (f, g) = fun (b: StringBuilder) -> f b; g b
        member this.Delay f        = fun (b: StringBuilder) -> (f()) b
        member this.Zero ()        = ignore

        member this.For (xs: 'a seq, f: 'a -> StringBuffer) =
            fun (b: StringBuilder) ->
                use e = xs.GetEnumerator ()
                while e.MoveNext() do
                    (f e.Current) b

        member this.While (p: unit -> bool, f: StringBuffer) =
            fun (b: StringBuilder) -> while p () do f b

        member this.Run (f: StringBuffer) =
            let b = StringBuilder()
            do f b
            b.ToString()

    let stringBuffer = StringBufferBuilder()

    type StringBufferBuilder with
      member inline this.Yield (b: byte) = fun (sb: StringBuilder) -> Printf.bprintf sb "%02x " b

I am not the author of the StringBuffer module. I'm using it regularly as it makes using StringBuilder super convenient to use in F#

I can mix strings and logic easily:

stringBuffer {
    "hello"
    if x = 3 then "world"
}

but, in the example at the beginning of this post, I am getting the following compilation error:

[FS0708] This control construct may only be used if the computation expression builder defines a 'Zero' method.

In the computation expression, the Zero method is defined as ignore so the problem is probably there. But my question is:

What is this error about? why does this specific use case require the implementation of the Zero method?

My understanding is that the Zero method is used if the expression would return nothing, as it is not valid for a computation expression; But since I specify a string, why would this execution path return nothing?


Edit:

Screenshot of the error (Rider / dotnet 5)

enter image description here

Now, the error is reduced to this scenario:

Ok stringBuffer {
    let r = get some list....
    match r with
    | [] -> "no active tasks"
    | r  -> String.Join("\n", r)
}

trigger the error, but

let s = 
    stringBuffer {
        let r = get some list....
        match r with
        | [] -> "no active tasks"
        | r  -> String.Join("\n", r)
    }
Ok s

does not

1
I'm not able to reproduce what you're describing. When I run the example at the top of your question, it compiles successfully and produces a string. (I'm using F# 5.0 and .NET Core 5.0, if that matters.) However, I noticed that you don't have a return or other computation keyword in the computation expression, so I'm confused about what it's trying to do. - brianberns
this allows me to concatenate strings using a StringBuilder; so let x = stringBuffer { "hello" \n "world" } will return me a string that was built through a StringBuilder; as it is a computation expression, I can add all sort of logic to output, or not, some extra strings without having to worry about the StringBuilder syntax - Thomas
OK, that makes sense. I think you need an explicit yield keyword for F# 4.x, but your example works fine in F# 5.0. I'm not getting a compiler error. - brianberns
that's weird; I added a screenshot of the error on my end - Thomas
I found how to trigger the issue, but now I'm even more confused: "Ok stringBuffer..." gives an error, but "let s = stringBuffer..." and then "Ok s" doesn't give the error!, I had omitted the "ok" from the question which was a mistake, I'll edit - Thomas

1 Answers

0
votes

It works for me if I add parens:

let result =
    Ok (stringBuffer {
        let r = [1;2;3]
        match r with
        | [] -> "no active tasks"
        | r  -> String.Join("\n", r)
    })
printfn "%A" result

Without the parens, the "Zero" error message occurs because function application is left-associative, so the compiler thinks you mean something like this: (Ok stringBuffer) { "str" }. Since Ok stringBuffer is an expression that lacks a Zero member, this does not compile.

Amusingly, if you define your own Ok operator that returns a valid builder, it will compile fine:

let Ok (sb : StringBufferBuilder) = sb
let result = Ok stringBuffer { "hello "; "world" }   // produces: "hello world"