tc39 / proposal-do-expressions

Proposal for `do` expressions
MIT License
1.11k stars 14 forks source link

Early return #30

Open qm3ster opened 6 years ago

qm3ster commented 6 years ago

Consider the following:

const boop = x => do {
    const i = x + 1
    if (i > 1) return i
    i * x
}

or even

const boop = x => do {
    for (const el of x) {
        if (typeof el.boop === 'function') return el.boop()
        if (typeof el.boop === 'object') return el.boop
    }
    new VerySadError('It\'s very sad')
}
qm3ster commented 6 years ago

Should return inside a do block:

ljharb commented 6 years ago

Absolutely not 1; either 2 or 3 - and 2 is more useful.

pitaj commented 6 years ago

2 makes most sense for me, and follows with the idea that it's a block.

qm3ster commented 6 years ago

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.

ljharb commented 6 years ago

"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.

loganfsmyth commented 6 years ago

Option 2 is common in Rust, so I think it would find lots of support and not be too surprising for people.

jridgewell commented 6 years ago

This was discussed in the July 2018 meeting, with both Option 2 and Option 3 being acceptable.

zenparsing commented 6 years ago

I'm very sympathetic to option 2, but I worry about a couple of things:

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

ljharb commented 6 years ago

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)

pitaj commented 6 years ago

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.

zenparsing commented 6 years ago

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.

pitaj commented 6 years ago

I wasn't saying it should be an async IIFE. I was saying it should be refactored to use normal async functions instead.

qm3ster commented 6 years ago

@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?

qm3ster commented 6 years ago

@loganfsmyth Rust's loop expression inspired this issue to some extent, hence the second example. In fact, the whole proposal seems quite rustlike.

pitaj commented 6 years ago

@qm3ster I think rust's every-statement-is-an-expression paradigm was a primary inspiration for this. That's my favorite part of rust.

qm3ster commented 6 years ago

But that power comes at a hefty price - semicolons.

hodonsky commented 4 years ago

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"...

tom-sherman commented 4 years ago

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
}
acutmore commented 3 years ago

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.

tintin10q commented 1 year ago

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.