fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Allow empty CE body and return Zero

Open SchlenkR opened this issue 2 years ago • 8 comments

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:

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] 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:

  • [x] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

SchlenkR avatar Jan 10 '23 16:01 SchlenkR

I think this is very reasonable. However, implementation may be a tad trickier than it seems at first, as empty curlies are sometimes valid, for instance:

type C = 
  new() = { }

Though I can’t think of anything offhand that would be truly conflicting in cases where there’s a leading ident.

PS, under the (absent) section “the current way of doing this”, you could mention that it is legal to have a unit expression. I.e., this is fine: let x = seq {()}. Not sure if p {()} is valid, though, it’ll depend on the definition of p.

abelbraaksma avatar Jan 11 '23 00:01 abelbraaksma

BTW, there's an interesting comment in Seq.fsi, which says:

Seq.empty // Evaluates to seq { }

image

SchlenkR avatar Jan 11 '23 10:01 SchlenkR

This needs a language design suggestion - but please consider it pre-approved. It is a missing case that has come up before but which would be really good to complete.

dsyme avatar Jan 11 '23 15:01 dsyme

@dsyme this is a language suggestion / in the right repo already 🙂

cartermp avatar Jan 11 '23 15:01 cartermp

Do you mean a RFC in the fslang-design repo @dsyme?

SchlenkR avatar Jan 11 '23 15:01 SchlenkR

Yes, any approved-in-principle language suggestion needs an RFC, and an RFC discussion thread.

abelbraaksma avatar Jan 12 '23 00:01 abelbraaksma

Do you mean a RFC in the fslang-design repo @dsyme?

That's the next step. I mistakenly thought I was replying to dotnet/fsharp issue

dsyme avatar Jan 15 '23 14:01 dsyme

  • RFC: https://github.com/fsharp/fslang-design/pull/774
  • RFC discussion: https://github.com/fsharp/fslang-design/discussions/775
  • Proposed implementation: https://github.com/dotnet/fsharp/pull/17352

brianrourkeboll avatar Jun 26 '24 16:06 brianrourkeboll