proposal-do-expressions icon indicating copy to clipboard operation
proposal-do-expressions copied to clipboard

Early return

Open qm3ster opened this issue 6 years ago • 20 comments

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 avatar Aug 30 '18 16:08 qm3ster

Should return inside a do block:

  • resolve the do block?
  • resolve the function containing the do block?
  • be totally illegal?

qm3ster avatar Aug 30 '18 16:08 qm3ster

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

ljharb avatar Aug 30 '18 16:08 ljharb

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

pitaj avatar Aug 30 '18 17:08 pitaj

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.

qm3ster avatar Aug 30 '18 17:08 qm3ster

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

ljharb avatar Aug 30 '18 17:08 ljharb

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

loganfsmyth avatar Aug 30 '18 17:08 loganfsmyth

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

jridgewell avatar Aug 30 '18 18:08 jridgewell

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

  • Is there a strong intuition about it? What about intuition in the corner cases mentioned in the presentation, like parameter default expressions?
  • How might it interact with a potential 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

zenparsing avatar Aug 30 '18 18:08 zenparsing

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)

ljharb avatar Aug 30 '18 18:08 ljharb

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.

pitaj avatar Aug 30 '18 19:08 pitaj

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.

zenparsing avatar Aug 30 '18 20:08 zenparsing

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

pitaj avatar Aug 30 '18 20:08 pitaj

@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 avatar Aug 30 '18 22:08 qm3ster

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

qm3ster avatar Aug 30 '18 22:08 qm3ster

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

pitaj avatar Aug 30 '18 23:08 pitaj

But that power comes at a hefty price - semicolons.

qm3ster avatar Aug 30 '18 23:08 qm3ster

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

hodonsky avatar May 25 '20 16:05 hodonsky

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
}

tom-sherman avatar Jun 22 '20 16:06 tom-sherman

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.

acutmore avatar Jan 30 '21 18:01 acutmore

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.

tintin10q avatar Nov 03 '22 01:11 tintin10q