Open brennongs opened 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
The main advantage of do
expressions is implicit returns. Pretty much everything inside a do block is an expression.
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())
};
@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.
@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.
@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
.
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.
That example is just to illustrate that await
causes a microtask tick even if the value being await
'd is not a Promise.
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 await
s (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.
@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?
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
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).
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
})()
I wouldn’t call that idiomatic in any way, but it certainly works already.
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'
})
IIFE with README might ease worry that IIFE is not idiomatic. https://www.npmjs.com/package/@seiyab/do-expr
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:looks a lot like this:
or, more explicitly, this:
and the latter has been a language construct since forever?
Not trying to be a jerk, just curious.