Skip to content

Allow empty CE body and return Zero #1232

@SchlenkR

Description

@SchlenkR

I propose we allow having empty computation expression bodies that are evaluated with builder.Zero(). When no Zero method is available on the builder, a specific error is raised (more specific than the current FS0003).

The existing ways of approaching this problem in F# are:

a) Provide a zero/empty function on a module (like Seq.empty).

b) As @abelbraaksma pointed out below, a unit expression as the only element in the CE body can be used, e.g. seq { () }. This works if the builder implements Zero:

type MySeqBuilder() =
    member _.Zero() : seq<'a> = Seq.empty<'a>
let mySeq = MySeqBuilder()
let res = mySeq { () }

Pros and Cons

A real-world use case where this makes sense is this: Imagine a HTML DSL that uses CEs and specific builder instances for each HTML element type. It might look like that:

div {
    p { "some content" }
    p { }
}

I currently cannot see why (from a syntax point of view) that should not be possible (although I don't know if there are some implementation details that lead to not providing that syntax). It feels natural writing empty bodys of a "thing" when that "thing" has a clear definition of an empty instance (i.e. Zero).

Extra information

Hint: Current errors / messages

Seq (propably due to special compiler treatment for seq) gives a more explicit error on empty CE bodies:

seq { }
// error FS0789: '{ }' is not a valid expression. Records must include
// at least one field. Empty sequences are specified by using Seq.empty or an empty list '[]'. 

Due to that, I ask myself if this is not something which has obviously "already been decided", but I didn't find anything.

Other builders (e.g. async or hand-rolled builders) give an error that is not very helpful, especially for beginners:

async { }
// error FS0003: This value is not a function and cannot be applied.

Hint: Generalization

An empty/zero non-function value in a module can have an advantage over the proposed syntax (or the existing builder {()] syntax, because non-function values can be generalized, whereas the builder syntax cannot:

let a: seq<'a> = seq { () }
let a1 = a |> Seq.map ((+) 1)   // 'a is infered to be int.
let a2 = a |> Seq.map ((+) 1.0) // FS0001: Type mismatch between int and float

// all fine.
let b: seq<'a> = Seq.empty
let b1 = b |> Seq.map ((+) 1)
let b2 = b |> Seq.map ((+) 1.0)

Hint: Run/Delay awareness

The way of how builder {} is transformed into basic language constructs should be similar to builder { () }, which is a not alwass builder.Zero(). The (propably very shortened) ruleset for transforming builder { () } seems to be:

  • When there's only Zero available, the result is just Zero.
  • When there are additionally Delay and Run available, then the result is then "delay zero, then run".

Estimated cost (XS, S, M, L, XL, XXL): I can't estimate it based on my knowledge.

Related suggestions: -

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions