fslang-suggestions
fslang-suggestions copied to clipboard
Inline bind operator for computation expressions
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:
if (f(bind! x) && bind! y) then // ...
would be lifted to something equivalent to:
let! x' = x
let! y' = y
if (f x' && y') then // ...
There are many more cases where this should likely work, such as:
// right-hand of let-expr
let x = bind! expr
// deep inside right-hand of let-expr
let x = (bind! expr) |> List.map string
// match expr
match bind! expr with
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:
async {
let f = (fun () -> bind! expr) // compiler error -- not allowed inside lambdas
let g x =
bind! expr // compiler error -- not allowed inside nested functions
}
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
!
Postfix bang would look something like:
if (f(a!) && b!) then // ...
Pros and Cons
The advantages of making this adjustment to F# are:
- More parity with C#'s deeply nestable
await
keyword - Highly useful because it allows the elimination of many otherwise unnecessary variable names in source code
- Reduces the urgency for more bang-keywords
The disadvantages of making this adjustment to F# are:
- more complexity
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:
- [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
- [x] 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.
Which syntax would you most prefer (vote using reactions)?
I believe that bang
and await
(without the bang) would both be backward-incompatible, so I have left them out of this vote.
-
bind!
- 👍 -
await!
- ❤️ - postfix
!
- 🚀
As an implementation detail, I think that the amount of binds in a complex expression could be reduced by using the let!/and!
form if the CE builder supported it, because in the expression if f(a!) && b! then ... else ...
, a's bind and b's bind are independent operations. I'm sure there are other opportunities for this kind of expansion as well, we've just been focusing on if
here.
Advantages of each potential operator/keyword:
-
bind!
- Same name as the CE Bind method, hinting to the monadic heritage
- Has a symmetry with
let!
in that the bang comes before the expression it is binding
-
await!
- Similar to C# and Javascript's async/await syntax -- would be a more familiar experience for C# devs learning F#
- Symmetry with
let!
- postfix
!
- Less verbose
- May reduce parentheses
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 ...
Which syntax would you most prefer (vote using reactions)?
We would likely make it CE-configurable. But votes on preferred syntax for async
, task
, option
, asyncOption
, asyncSeq
etc. would be welcome.
Which syntax would you most prefer (vote using reactions)?
We would likely make it CE-configurable. But votes on preferred syntax for
async
,task
,option
,asyncOption
,asyncSeq
etc. would be welcome.
So more like a custom operation? Sounds interesting. How are you envisioning that to work, specifically?
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 check
as the keyword for the imaginary option and result builders).
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 check
for other builders:
elicit
verify
attain
resolve
solve
scan
tap
I kinda like tap
from both a semantic standpoint (tap def: "exploit or draw a supply from a resource") and an aesthetic standpoint (use of 3 letters seems to adhere to an F# keyword naming rhythm). To be honest though, I actually wouldn't mind using bind
for every/any kind of builder just for consistency.
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 bind
could go beyond binding single value:
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 bind
and and
is any expression that can be bound with let!
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 bind
's to invoke Bind
and use tailing inline and
's to invoke applicative methods (MergeSources
/BindReturn
)? Being a bit of a novice with computation expressions, I wonder if that would be:
- A useful capability to have?
- Congruent with the current behavior of applicative expressions?
What I wonder, though, is whether the above should automatically use applicative methods (MergeSources / BindReturn) if they are defined, or be restricted to Bind.
@Tarmil, it should not automatically use MergeSources
because developer may rely on sequential execution, e.g. createUser
and then setAdditinalInfo
. If last is going to execute before first (which may happen with Task.WhenAll
) then it may fail
Love this suggestion. Found it after working with task CEs and feeling the need for new bang syntax like while!
and if!
. Then thought rather than making a bang version of everything, why not have 1 keyword that semantically unwraps the value like await
does in C#? (Recognizing that it's no less complicated to implement behind the scenes.)
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 x!
is subtle / easy to miss. Also consider multi-term expressions:
while (channel.Reader.WaitToReadAsync())! do ...
while await! channel.Reader.WaitToReadAsync() do ...
@jwosty The side-by-side examples seem to use extra parenthesis: if (await! x) && y then ...
. Do CE keywords have/follow order of precedence rules? If so, if await! x && y then ...
should work.
Love this suggestion. Found it after working with task CEs and feeling the need for new bang syntax like
while!
andif!
. Then thought rather than making a bang version of everything, why not have 1 keyword that semantically unwraps the value likeawait
does in C#? (Recognizing that it's no less complicated to implement behind the scenes.)
@kspeakman Agreed, this proposal could obsolete all the special-case bang keywords
The side-by-side examples seem to use extra parenthesis:
if (await! x) && y then ...
. Do CE keywords have/follow order of precedence rules? If so,if await! x && y then ...
should work.
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 =! [ "foo" ]
Gold =! Error [ "bar" ]
Stone =! Ok 3
}
}
The code is more concise and we don't risk muddling up the intermediary bindings.
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 ?