tc39 / proposal-do-expressions

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

Major strengths over IIFE's? #46

Open brennongs opened 4 years ago

brennongs commented 4 years ago

hello 👋🏻

Potential noob question here, and I didn't see it in my brief scan of the open issues, but could someone explain why do statements would be preferable over IIFE arrows? Maybe I'm not understanding the full implication of the proposal, but this:

let x = do {
  let tmp = f();
  tmp * tmp + 1
};

looks a lot like this:

let x = (() => {
  let tmp = f();
  return tmp * tmp + 1;
}())

or, more explicitly, this:

let x = (function() {
  let tmp = f();
  return tmp * tmp + 1;
}())

and the latter has been a language construct since forever?

Not trying to be a jerk, just curious.

avegancafe commented 4 years ago

Not speaking to the implementation, but an IIFE is a bit of a hack for do blocks. Functionally, they're similar, but the preference to use them or not is up to you! 🙂 That being said haha, my personal preference is to not read IIFE's if at all possible and replace them with more expressive syntax

pitaj commented 4 years ago

The main advantage of do expressions is implicit returns. Pretty much everything inside a do block is an expression.

alamothe commented 4 years ago
  1. IIFE are costly. JavaScript engine needs to create a function object, then execute it.
  2. Syntax is cumbersome.
  3. If you're using TypeScript, type narrowing that you performed outside a lambda, does not translate to lambda.
Pajn commented 4 years ago

Except for the pointes already made, await, break, continue, return and yield are broken inside IIFEs

See this (previously) common Rust pattern:

let value = match result {
  Ok(value) => value,
  Err(err) => return Err(err.into())
};
hax commented 4 years ago

@Pajn You could use await in async iife.

About break, continue, return, though it could be see as the benefit, but it also have another side. Flow control were statements, now it could be the part of expression, note the expressions can be very complex and hard to recognize whether it include a flow control.

Pajn commented 4 years ago

@hax I could, but then everything in that IIFE would be async

let value = do { if (task.isAsync) { await task.getAsyncValue() } else { task.getValue() } }

would not require a spin of the event queue in the sync case, but

let value = await (async () => { if (task.isAsync) { return await task.getAsyncValue() } else { return task.getValue() } })()

would.

So while IIFEs in some cases could in some cases do the job of do expression I would argue that they are nearly always a bad choice and ternarys (where possible) and reassignment of variables (where not) would be better. But both are less readable and does a bad job of display their intent compared to do expressions.

dustbort commented 3 years ago

@Pajn You didn't need to make the IIFE async in your example.

let value = await (() => { if (task.isAsync) { return task.getAsyncValue() } else { return task.getValue() } })()

That only puts the async case on the event loop, right? Await is forgiving, and you can await a non-promise, which is just a synchronous pass-through operation, right?

The calling scope would need to be async because we still use await in do.

bakkot commented 3 years ago

Await is forgiving, and you can await a non-promise, which is just a synchronous pass-through operation, right?

No. You can await a non-promise, but it does entail a microtask tick.

(async () => {
  console.log(0);
  console.log(1);
  await null;
  console.log(2);
})();
console.log('outer');

will print 0, 1, outer, 2, in that order.

bakkot commented 3 years ago

That example is just to illustrate that await causes a microtask tick even if the value being await'd is not a Promise.

bakkot commented 3 years ago

I don't want to focus too much on the microtask ticks here. For me the main reason not to use an IIFE here is that it's much harder for a reader to glance at it and understand what's going on. Consider:

let value = do {
  if (task.isAsync) {
    await task.getAsyncValue()
  } else {
    task.getValue()
  }
};

In this example it is very clear which part is asynchronous. By contrast,

let value = await (() => {
  if (task.isAsync) {
    return task.getAsyncValue();
  } else {
    return task.getValue();
  }
})();

does not make that at all clear, and

let value = await (async () => {
  if (task.isAsync) {
    return await task.getAsyncValue();
  } else {
    return task.getValue();
  }
})();

has two awaits (and a bunch of extra syntax) despite only logically having a single asynchronous operation. For me that alone would be sufficient reason to prefer the do form, even without the microtask differences.

brennongs commented 3 years ago

@bakkot couldn't your most recent example be rewritten without the do in the first place?

let value = task.isAsync
? await task.getAsyncValue()
: task.getValue()

Admitting that this is all implementation details, I'm just trying to understand the use case. It seems to see like do blocks are just like... javascript in a box? Isn't all of javascript just one giant do block?

brennongs commented 3 years ago

that said, I do see the use when using control flow statements, and I super appreciate everyone on this thread not tearing me apart! GitHub > StackOverflow <3

bakkot commented 3 years ago

couldn't your most recent example be rewritten without the do in the first place?

Yeah, I was just following an example introduced earlier. The cases where you'd actually want do would generally be more complex, for example involving a temporary variable, a try-catch, or more branches in your if (which you can still do with chained ternaries, of course, but sometimes people find those harder to read than an if-elseif-else).

ghost commented 3 years ago

As a beginner, I'd like to ask something. I see that there are many issues asking about the differences between a do expression and an IIFE. I know that there are cases where a do expression can do things an IIFE can't (return, yield, hoisting a var, etc.), but for that subset of cases where they're interchangeable, are IIFEs truly idiomatic already? That is, could you write something like this in company code?

const myTopLevelVar = (_do => {
    const myShortLifetimeVar = 5
    return myShortLifetimeVar * myShortLifetimeVar
})()
ljharb commented 3 years ago

I wouldn’t call that idiomatic in any way, but it certainly works already.

seiyab commented 3 years ago

Even if IIFE is not idiomatic yet, we can practically write IIFE that is very easy to understand already.

Define

const iife = (f) => f();

Then use it like following

const x = iife(() => {
  if (a % 2 === 0) return 'even';
  return 'odd'
})

If you like the word do, use it instead of iife.

const do_ = (f) => f();

Now, we almost have do expression.

const x = do_(() => {
  if (a % 2 === 0) return 'even';
  return 'odd'
})
seiyab commented 2 years ago

IIFE with README might ease worry that IIFE is not idiomatic. https://www.npmjs.com/package/@seiyab/do-expr