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

Computation expressions in function declaration #1147

Closed
3 of 5 tasks
lucasteles opened this issue May 22, 2022 · 25 comments
Closed
3 of 5 tasks

Computation expressions in function declaration #1147

lucasteles opened this issue May 22, 2022 · 25 comments

Comments

@lucasteles
Copy link

lucasteles commented May 22, 2022

Computed expressions in function declaration

I propose a way to set a computation expression as default in a function declarations:

let asyncId value = task {
    let! x = Task.FromResult value
    return x
}

let asyncId2  = fun value -> 
  task {
      let! x = Task.FromResult value
      return x
}

// I am proposing a syntax sugar, or something like 

task let asyncId value = 
   let! x = Task.FromResult value
   return x

let asyncId2  = 
  task fun value ->  
    let! x = Task.FromResult value
    return x

Pros and Cons

The advantages of making this adjustment to F# is a simple way to define function scoped in computation expressions

The disadvantages of making this adjustment to F# are more than one way to write the same thing

Extra information

Estimated cost (XS, S, M, L, XL, XXL):

Related suggestions: (put links to related suggestions here)

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.

@BentTranberg
Copy link

BentTranberg commented May 22, 2022

I feel it would be more natural to swap around the keywords, so that, with the given example, it would be

let task asyncId value =

Not only does it feel more natural when comparing with the English language, but also the let keyword would come first, making it easier to see that this is indeed a let statement, just as when the mutable keyword is used with let.

(edit: Note that I am not arguing pro here, but rather how I think the syntax should be if implemented. Hopefully thumbs up or down reflect that.)

@BentTranberg
Copy link

BentTranberg commented May 22, 2022

It will be very logical to also treat async in the same way. Otherwise people would really wonder what's been going on.

(edit: While checking my posts for sanity, I see now that this post was unneeded, because the proposal is about computation expressions, and not just task. Not sure if I should just delete this.)

@lucasteles
Copy link
Author

lucasteles commented May 22, 2022

I feel it would be more natural to swap around the keywords, so that, with the given example, it would be

Yes, I considered this, but it would be a breaking change if the name of the function name is the same as the CE

I am not confident with the syntax I proposed, would be nice to think of a simpler way to define this type of computation block, even if it would be only for async and task which are really common cases

@pbiggar
Copy link

pbiggar commented May 22, 2022

I am not confident with the syntax I proposed, would be nice to think of a simpler way to define this type of computation block, even if it would be only for async and task which are really common cases

In the lisp world, they have if-let and when-let, so perhaps something similar: task-let and async-let?

@BentTranberg
Copy link

BentTranberg commented May 23, 2022

What about an attribute?

[<task>]
let asyncId value =

and my favorite way to place such attributes, in order to avoid code drift, and a little bit in order to save lines

let [<task>] asyncId value =

edit, add:

But again, the suggestion I believe is about computation expressions in general, isn't it? So perhaps something like this is needed instead, if attributes are to be used:

[<ComputationExpression(task)>]
[<ce(task)>]

Again, I'm not arguing pro yet, but just thinking about how it might be done.

@voronoipotato
Copy link

voronoipotato commented May 23, 2022

CE are already the one of the more magical things in F#. I don't know if making them less obvious, or having more ways of creating them is in the benefit of clarity.

@Demuirgos
Copy link

Keeping them separate and clear keeps the language simple and IMO this would confuse new comers if not old devs as well, also this would alter the ML simple syntax andin troduce unnecessarily complexity.

@piaste
Copy link

piaste commented May 30, 2022

It seems to me that functions don't really have much to do with this suggestion, they're just a special case of 'CEs should use the same scoping syntax as the rest of the language, i.e. indentation instead of braces'.

Something like

let answer = 
    async!
        do! something()
        return 42

// or with undentation support
let answer = async!
    do! something()
    return 42

@vzarytovskii
Copy link

It seems to me that functions don't really have much to do with this suggestion, they're just a special case of 'CEs should use the same scoping syntax as the rest of the language, i.e. indentation instead of braces'.

Something like

let answer = 
    async!
        do! something()
        return 42

// or with undentation support
let answer = async!
    do! something()
    return 42

Personally, I don't see how is it different (let alone better) than existing syntax with builder {}, just another way of doing same thing.

@BentTranberg
Copy link

I never liked the braces that much, and with piaste's suggestion I'm turning my vote in favor. To me, the braces seem like an odd syntax borrowed from C/C++/C#/Java and mixed into the otherwise elegant F# syntax. To me, the braces are slightly annoying to edit and read. Btw, that's one reason why we like F# over C#.

Yes, it'll be another way of doing the same thing, just as F#'s light syntax is also another way of doing the same thing. In this case I think we'd actually be doing away with an odd way of doing a thing, and doing it in a more normal way instead.

@realparadyne
Copy link

realparadyne commented May 30, 2022

Personally, I don't see how is it different (let alone better) than existing syntax with builder {}, just another way of doing same thing.

It would get rid of the ugly curly braces surrounding the code, especially the trailing one at the end.

How about:

let {async} answer = 
    do! something()
    return 42

The curly braces say "this is a CE" and the use of them should preclude confusion with existing code binding to a name that matches a CE name.

@voronoipotato
Copy link

@realparadyne wouldn't that clash or be confused with record deconstruction for parameters?

@LyndonGingerich
Copy link

LyndonGingerich commented May 31, 2022

Brackets look odd and clunky, but I think that's the point. Significant whitespace is more elegant for scoping than explicit delimiters within the same syntactical structure, but CEs use significantly different syntax from regular F#, even implementing a return keyword. Just as F# uses explicit delimiters for code quotations, so it should also for snippets from other syntaxes, including computation expressions. Imagine the confusion if F# were to implement quotations from, say, Python without explicit delimiters!

@Demuirgos
Copy link

Demuirgos commented May 31, 2022 via email

@woojamon
Copy link

woojamon commented Jun 1, 2022

There's already the function keyword which abbreviates a match expression:

let isEven = function
    | x when x % 2 = 0 -> true
    | _ -> false

What if the abbreviated CE keyword was in the same location?

let asyncId value = task
    let! x = Task.FromResult value
    return x

@LyndonGingerich
Copy link

There's already the function keyword which abbreviates a match expression:

let isEven = function
    | x when x % 2 = 0 -> true
    | _ -> false

What if the abbreviated CE keyword was in the same location?

let asyncId value = task
    let! x = Task.FromResult value
    return x

So what @piaste said, but without the exclamation mark?

@woojamon
Copy link

woojamon commented Jun 1, 2022

So what @piaste said, but without the exclamation mark?

I was actually just editing my comment to say that I just saw @piaste 's comment ha.

Consider my comment a vote for that syntax then 👍

@Tarmil
Copy link

Tarmil commented Jun 1, 2022

What if the abbreviated CE keyword was in the same location?

let asyncId value = task
    let! x = Task.FromResult value
    return x

CE builders aren't keywords though, they're normal values. So this is very ambiguous.

@voronoipotato
Copy link

voronoipotato commented Jun 2, 2022

if it were to be done, and while I am passionate about removing braces/brackets/parens, I don't think it should... but if it were to be done it would need a new keyword to indicate that a CE were starting and then the ce builder.

let asyncID x = Compute task
    let! y = Task.FromResult x
    return y

now if it implicitly bound/returned like function does, that sounds more enticing.

let asyncID = Compute task 
    Task.FromResult

especially if you could do more than one line with implicit let! between each

let asyncID = Compute task
    Task.FromResult
    someOtherFunction

//could be equivalent to
let asyncID x = task {
    let! y = Task.FromResult x
    let! z = someOtherFunction y
    return z
}

While to me this provides much more value because now you have a brief notation that has a specific use akin to function, rather than two different ways of doing the same thing. I still don't think it's worth it because its extremely magical and it's basically the same as using a bind operator.

@lucasteles
Copy link
Author

let asyncID = Compute task
    Task.FromResult
    someOtherFunction

I like the idea... mainly the implicit return, in this context makes a lot of sense

I dislike the implicit let! tho,
maybe for this case would be nice to use let! for this definition, which would make a lot of sense considering we already use it in the body of the CEs

something like this for the same purpose:

let! task ayncID x = 
    Task.FromResult x
    !> someOtherFunction

let asyncID' = fun! task x ->  Task.FromResult x  !> someOtherFunction

Maybe it could infer the CE from usage or context, (even if it was only for the native CE)

let! ayncID x = 
    Task.FromResult x
    !> someOtherFunction

let asyncID' = fun! x ->  Task.FromResult x  !> someOtherFunction

For me looks more fsharp-ish

Another option is to throw the let of, like use:

task! ayncID x = 
    Task.FromResult x
    !> someOtherFunction

let asyncID' = task! x ->  Task.FromResult x  !> someOtherFunction

other ideas:

compute task ayncID x = // ...
process task ayncID x = // ...
with task ayncID x = // ...
begin task ayncID x = // ...
do task ayncID x = // ...

@voronoipotato
Copy link

voronoipotato commented Jun 3, 2022

If you were okay with operators you can just make a local bind operator, so no CE needed.

let (>>=) = TaskBuilder.bind

@lucasteles
Copy link
Author

If you were okay with operators you can just make a local bind operator, so no CE needed.

let (>>=) = TaskBuilder.bind

Yes I am, maybe this should be a new discussion because it is not correlated with the CE function declaration, but with expressiveness inside it

defining and using bind >>= is great, but what I mean to say is to have a CE contextual bind operator, to express code more like outside of it, same way we have let! for let, I see value to have a !> or |>! for |> mainly because it is a F# idiomatic code signature at this point

@jl0pd
Copy link

jl0pd commented Jun 5, 2022

Operators like |>! were discussed in #791 and alternative to let!, do!, match! was discussed in #1070

@voronoipotato
Copy link

@lucasteles have you tried out F#+ yet?

@dsyme
Copy link
Collaborator

dsyme commented Jun 14, 2022

I will close this for reasons like this

I do get the suggestion - TypeScript has async x => ... for example, and it's not wrong to consider this for F#. However all in all fun x -> async { ... } seems good enough given the expression-oriented nature of F#, rather than fun async x -> ... or async fun -> .... And the complications for the let-binding and member-binding syntaxes just look too much.

@dsyme dsyme closed this as completed Jun 14, 2022
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