-
Notifications
You must be signed in to change notification settings - Fork 21
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
Inline bind operator for computation expressions #1070
Comments
Which syntax would you most prefer (vote using reactions)? I believe that
|
As an implementation detail, I think that the amount of binds in a complex expression could be reduced by using the |
Advantages of each potential operator/keyword:
Some side-by-side comparisons of each, for reference: let! x = f expr
if x && y then ...
if (bind! x) && y then ...
if (await! x) && y then ...
if x! && y then ... |
We would likely make it CE-configurable. But votes on preferred syntax for |
So more like a custom operation? Sounds interesting. How are you envisioning that to work, specifically? |
Yes I'd imagine either a new method or a new attribute on the CE builder. I've not thought about it more than that though. |
Related to #1000 |
Gotta say, I'm really liking the idea of this proposal quite a bit. Here's what I imagine it might look like in use for a couple of diff builders (I picked type FooBar = {
Foo: obj
Bar: obj
}
let taskDemo = task {
let foo = await getFoo ()
let bar = await getBar foo
return {
Foo = foo
Bar = bar
}
}
let asyncDemo = async {
return {
Foo = await getFoo ()
Bar = await getBar ()
}
}
let resultDemo = result {
let foo = check getFoo ()
let bar = check getBar foo
return {
Foo = foo
Bar = bar
}
}
let optionDemo = option {
return {
Foo = check getFoo ()
Bar = check getBar ()
}
}
let taskResultDemo = taskResult {
let foo = awaitCheck getFoo ()
let bar = check (await getBar foo)
return {
Foo = foo
Bar = bar
}
}
let asyncOptionDemo = asyncOption {
return {
Foo = awaitCheck getFoo ()
Bar = check (await getBar ())
}
} Here are a couple of other candidate words that could be used instead of
I kinda like |
Also, would it be worth considering allowing devs to define their own binding terms for custom builders? I'm imagining a scenario like the following: let demoAttempt = attempt {
let v = bind getThing() // returns Async<Option<Result<'T>>>
let asyncV = bindAsync getThing()
let optionV = bindOption asyncV
let resultV = bindResult optionV
return v, asyncV, optionV, resultV
} That way it seems like one could create builders that would allow the user to control to what degree stacked values are bound? |
It would be awesome if let await2Tasks t1 t2 = // Task<'a> -> Task<'b> -> Task<('a * 'b)>
task {
let (r1, r2) =
bind t1
and t2
return (r1, r2)
}
// which is identical to
let await2Tasks t1 t2 = // Task<'a> -> Task<'b> -> Task<('a * 'b)>
task {
let! _ = Task.WhenAll(t1, t2)
return (t1.Result, t2.Result)
} Right hand side of let get42Async () =
task {
let (four, ten, two) =
bind Task.Run (fun () -> 4)
and Task.Run (fun () -> 10)
and Task.Run (fun () -> 2)
return four * ten + two
} |
@jl0pd I don't think this would need anything extra if there's a bind operator, you could just do this: task {
let (four, ten, two) =
(bind Task.Run(fun () -> 4),
bind Task.Run(fun () -> 10),
bind Task.Run(fun () -> 2))
return four * ten + two
} What I wonder, though, is whether the above should automatically use applicative methods (MergeSources / BindReturn) if they are defined, or be restricted to Bind. |
Hmm, @Tarmil and @jl0pd, would it make sense to use tailing inline
|
@Tarmil, it should not automatically use |
Love this suggestion. Found it after working with task CEs and feeling the need for new bang syntax like Re names. "bind" is an obstacle for newcomers. As a vague/archaic word, it harms readability in a similar way to using symbols. And its meaning has more to do with its implementation than its apparent usage. "await" is fantastic for readability of asyncs, less for other things. A more general fit for the user's intent might be "unwrap". Postfix bang while (channel.Reader.WaitToReadAsync())! do ...
while await! channel.Reader.WaitToReadAsync() do ... @jwosty The side-by-side examples seem to use extra parenthesis: |
@kspeakman Agreed, this proposal could obsolete all the special-case bang keywords
That might be true; I just included them for clarity (I tend to overuse parenthesis :) ) |
I have marked this as approved-in-principle. We should do this in some form |
I was considering opening a new ticket, but I think it falls under here. Consider this applicative Computation Expression. The idea is to collect errors, rather than stop on the first one: type ResultApplicativeBuilder() =
member this.Return(x) =
Ok x
member this.MergeSources(a, b) =
match a, b with
| Ok x, Ok y -> Ok (x, y)
| Error e, Error d -> Error (e @ d)
| Error e, _ -> Error e
| _, Error d -> Error d
member this.BindReturn(m, f) =
Result.map f m
let resultA = ResultApplicativeBuilder()
type Resources =
{
Wood : int
Food : int
Gold : int
Stone : int
}
let resources =
resultA {
let! w = Ok 1
and! f = Error [ "foo" ]
and! g = Error [ "bar" ]
and! s = Ok 3
return
{
Wood = w
Food = f
Gold = g
Stone = s
}
}
printfn $"Resources: %A{resources}" It would be great if listing out the intermediate variables were optional. (Hypothetical syntax) let resources =
resultA {
return
{
Wood =! Ok 1
Food =! Error [ "foo" ]
Gold =! Error [ "bar" ]
Stone =! Ok 3
}
} The code is more concise and we don't risk muddling up the intermediary bindings. With let resources =
resultA {
return
{
Wood = !! (Ok 1)
Food = !! (Error [ "foo" ])
Gold = !! (Error [ "bar" ])
Stone = !! (Ok 3)
}
} |
This is the most wanted feature of me. It would simplify the code I write for some libraries a lot. Example: Signal processing, where parameters have to be modulated often: let! mod = Osc.sine(frq = 120.0)
return! Osc.rect(frq = 4000.0 * mod)
// ...with inline bind (here using a prefix op "!!" just for demo)
return! Osc.rect(frq = 4000.0 * (!!Osc.sine(frq = 120.0))) Another example: Arithmetic operations, which would reduce the need for custom operators and / or SRTP overload "hacks". The advantage here is the inline-style of writing the operands as one would expect it in an equation. // nice: we can use "+" as we would expect it to be used :)
let result1 = !!Osc.square(frq = 100.0) + !!Osc.sine(200.0)
let result2 = 100.0 + !!Osc.sine(200.0)
// current alt. 1:
let! a = Osc.square(frq = 100.0)
let! b = Osc.sine(200.0)
let result1 = a + b
// current alt. 1 using custom op (here: ".+", ".+." or "+."), which can a pain regarding all the complexity that numbers introduce
let result1 = Osc.square(frq = 100.0) .+. Osc.sine(200.0)
let result2 = 100.0 +. Osc.sine(200.0)
// ... Currently, I am currently developing a UI library that works like react, and uses only Skia under the hood, where one can define triggers, animations, whatever kind of state-preserving functions. Using animations inline, e.g. for the x-coord of a UI element, would reduce the brain-load, make it so nice to write just in one-shot. That would be great. I like how C# async is being able to be inlined; it's a quite seamless integration and feels really natural. I don't care if it's an operator or a keyword, but usable as simple as inlined async in C#. If this could be defined, I would perhaps dare to implement it. cc @vzarytovskii @dsyme ? |
I'm marking this as approved-in-principle as a language design item. I think Implementing it requires a significant amount of work. |
Please note that, |
I propose we add an operator or keyword which can be used to bind values in computation expressions deep inside expressions (potentially arbitrarily), similar to C#'s await keyword, which can be used in the middle of expressions, and is not limited to top-level expressions in the computation expression. The specific syntax is of lesser issue than the form of these expressions themselves. This proposal has been alluded up many times within the F# community.
For sake of discussion, I am going to use a
bind!
syntax in this proposal (which could easily be replaced with one of the alternative choices).The following code:
would be lifted to something equivalent to:
There are many more cases where this should likely work, such as:
And so on -- there are likely more cases to consider and decide on.
However, this would not be allowed arbitrarily deep, as the following should obviously be disallowed:
There are also probably other exclusions that should be fleshed out.
The current way to approach this is to manually lift the bindable expression and bind it using
let!
.Alternative keywords
bind
bind!
await
await!
!
Postfix bang would look something like:
Pros and Cons
The advantages of making this adjustment to F# are:
await
keywordThe disadvantages of making this adjustment to F# are:
Extra information
Estimated cost (XS, S, M, L, XL, XXL): L (I'm guessing)
Related suggestions: #572 (match-bang), #651 (finally-bang), #791 (pipe-bang), #863 (if-bang), #974 (function-bang), #1038 (while-bang)
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
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.
The text was updated successfully, but these errors were encountered: