1

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

Thomas
  • 10,933
  • 14
  • 65
  • 136
  • 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. – Brian Berns Feb 11 '21 at 16:37
  • 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 Feb 11 '21 at 16:58
  • 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. – Brian Berns Feb 11 '21 at 17:00
  • that's weird; I added a screenshot of the error on my end – Thomas Feb 11 '21 at 17:05
  • 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 Feb 11 '21 at 17:07
  • OK, I'm seeing the same thing now, at least. Will investigate. – Brian Berns Feb 11 '21 at 17:08

1 Answers1

2

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"
Brian Berns
  • 15,499
  • 2
  • 30
  • 40
  • That’s specifically what I would like to know. There is something not clear about why this is happening since the expression is returning something. – Thomas Feb 11 '21 at 18:54
  • OK, I figured out what's going on and updated my answer. – Brian Berns Feb 11 '21 at 19:38