Open qm3ster opened 6 years ago
Should return inside a do
block:
do
block?do
block?Absolutely not 1; either 2 or 3 - and 2 is more useful.
2 makes most sense for me, and follows with the idea that it's a block.
I only included 2 for completeness. I think it's immoral and is probably the hardest to implement. If you have such a visceral reaction to 1, it would probably be unpopular, so I suggest it be illegal instead.
"immoral" is pretty vague and melodramatic, can you explain a bit more?
Option 1 wouldn't make any sense because return
is for functions, blocks aren't functions, and do
blocks are blocks.
Option 2 is common in Rust, so I think it would find lots of support and not be too surprising for people.
This was discussed in the July 2018 meeting, with both Option 2 and Option 3 being acceptable.
I'm very sympathetic to option 2, but I worry about a couple of things:
async
expression or block (e.g. async { … }
)?Also, does this proposal provide a way to break out of the block body with a value?
let value = do {
if (something) {
// I want to break out here with the value `42`
}
// lots of code
};
assert.equal(value, 42);
Or am I forced to nest?
let value = do {
if (something) {
42;
} else {
// lots of code
}
};
cc @dherman
I’d assume you’d be forced to nest, and I’d assume it’d be forbidden in an async block too (im skeptical about async blocks at all, due to the likely need for them to behave as functions inside promises)
Is there a strong intuition about it?
Yes, do
is a block.
What about intuition in the corner cases mentioned in the presentation, like parameter default expressions?
It's a corner case for sure. It depends on whether the do expression is evaluated in the context within the function, or outside the function. I think there's been discussion on this.
How might it interact with a potential async expression or block
A async do
block would only be useful within non-async functions (as otherwise you can just use the function's await keyword), and in cases where it would be useful, it would probably be better for the function to be async instead. And yes, return in that case would be very odd, which is just another reason to avoid it.
Also, does this proposal provide a way to break out of the block body with a value?
There's been discussion around the behavior of break
.
Or am I forced to nest?
Only if you don't want to return from the whole function, which is often what you want anyways. Otherwise, the else { ... }
is more expressive as you aren't representing a fail-early. A case where do if blocks are used and you have vastly different amounts of code in the different cases sounds like a code smell.
in cases where it would be useful, it would probably be better for the function to be async instead
Sure, but AIIFE's are a pain.
I wasn't saying it should be an async IIFE. I was saying it should be refactored to use normal async functions instead.
@zenparsing the outer, actual function can be async, which means that you can have an await
expression within the do
block, just like in any other expression inside the async function.
Do you mean async do
blocks would be used to create a promise value within a synchronous function?
@loganfsmyth Rust's loop
expression inspired this issue to some extent, hence the second example.
In fact, the whole proposal seems quite rustlike.
@qm3ster I think rust's every-statement-is-an-expression paradigm was a primary inspiration for this. That's my favorite part of rust.
But that power comes at a hefty price - semicolons.
Each do block should be its own return as it's originally spec'd and it should not interact with the surrounding scope except to inherit ( also I saw something about it being lazy and I almost cried ). I'd say we stick with option #3 here and free the do block from any return because it really already does that and adding to that could be confusing conceptually and then we will have "engineers" substituting do blocks and normal functions all over the place because they like how it looks and "it doesn't take any argument anyways"...
One of the use cases I've been thinking about is combining do expressions with pattern matching to mimic Rust's error handling. This use case would require option 2.
// data: { ok: number } | { err: string }
function foo(data) {
// Unwrap the result `ok` or return the err
const result = case (data) {
when { ok } -> ok
when _ -> do {
// assuming option 2
return data
}
}
// Safely perform calculations on the wrapped ok property
return result * 5
}
When reading code it will be required to know if the line I am scanning is within a do { ... }
block to know the current semantics. The larger the block the harder it is to achieve this.
With the main example for early return being a large do block makes me think that it is a good thing that code authors will be encouraged to break do blocks down into smaller chunks.
i.e. the limitations of what you can do within a do-block likely helps prevent them from growing too large. Making them easier to read and reason about.
Another intresting idea is that the return
in a do block actually sets the value that the do block will resolve to but it doesn't change the control flow. This is how it works in haskell.
b = true;
const a = do {
return null;
if (b) return b;
}
Now the if at the end is allowed without the else because the value that the do will resolve as is already set.
So this just resolves to 3
const a = do {
return 1
return 2
return 3
var b = 4; // this is fine now
}
Setting a return value early seems to me like a good solution to the limitations mentioned in the proposal like declaring variables at the last line.
Consider the following:
or even