-
Notifications
You must be signed in to change notification settings - Fork 22
Description
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.