Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow empty CE body and return Zero #1232

Closed
4 of 5 tasks
SchlenkR opened this issue Jan 10, 2023 · 8 comments
Closed
4 of 5 tasks

Allow empty CE body and return Zero #1232

SchlenkR opened this issue Jan 10, 2023 · 8 comments

Comments

@SchlenkR
Copy link

SchlenkR commented Jan 10, 2023

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.

@abelbraaksma
Copy link
Member

abelbraaksma commented Jan 11, 2023

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.

@SchlenkR
Copy link
Author

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

Seq.empty // Evaluates to seq { }

image

@dsyme
Copy link
Collaborator

dsyme commented Jan 11, 2023

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.

@cartermp
Copy link
Member

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

@SchlenkR
Copy link
Author

SchlenkR commented Jan 11, 2023

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

@abelbraaksma
Copy link
Member

abelbraaksma commented Jan 12, 2023

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

@dsyme
Copy link
Collaborator

dsyme commented Jan 15, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants