Skip to content

Early return #30

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

Open
qm3ster opened this issue Aug 30, 2018 · 20 comments
Open

Early return #30

qm3ster opened this issue Aug 30, 2018 · 20 comments

Comments

@qm3ster
Copy link

qm3ster commented Aug 30, 2018

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
Copy link
Author

qm3ster commented Aug 30, 2018

Should return inside a do block:

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

@ljharb
Copy link
Member

ljharb commented Aug 30, 2018

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

@pitaj
Copy link

pitaj commented Aug 30, 2018

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

@qm3ster
Copy link
Author

qm3ster commented Aug 30, 2018

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
Copy link
Member

ljharb commented Aug 30, 2018

"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
Copy link

loganfsmyth commented Aug 30, 2018

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

@jridgewell
Copy link
Member

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

@zenparsing
Copy link
Member

zenparsing commented Aug 30, 2018

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

@ljharb
Copy link
Member

ljharb commented Aug 30, 2018

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
Copy link

pitaj commented Aug 30, 2018

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
Copy link
Member

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
Copy link

pitaj commented Aug 30, 2018

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

@qm3ster
Copy link
Author

qm3ster commented Aug 30, 2018

@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
Copy link
Author

qm3ster commented Aug 30, 2018

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

@pitaj
Copy link

pitaj commented Aug 30, 2018

@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
Copy link
Author

qm3ster commented Aug 30, 2018

But that power comes at a hefty price - semicolons.

@hodonsky
Copy link

hodonsky commented May 25, 2020

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
Copy link

tom-sherman commented Jun 22, 2020

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
Copy link

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
Copy link

tintin10q commented Nov 3, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants