Open JHawkley opened 5 years ago
FWIW both examples in Other Issues With Do-Blocks
are syntax errors.
I also find that your examples tend towards operations better suited for function bodies anyway.
// There is no reason to use a function here except the status quo
const x = {
if (foo()) {
f();
} else if (bar()) {
g();
} else {
h();
}
};
// use `do` to compartmentalize code while maintaining a clear logical flow
function a() {
const c = do {
const t = acquireThing();
if (t === 1337) {
return t;
}
t * 5;
};
// do something with `c`, and `t` is out of scope
}
It's also worth mentioning that transpilers can do a lot of cool stuff with do expressions.
Thank you @JHawkley for putting together this very thorough analysis! It will take a little bit of time for me to absorb everything, but I'll let you know my thoughts when I do.
Also, another idea that's been floating around for a while is the concept of an "async do". It might be interesting to compare how an "async do-block" might compare with an "async do-arrow".
@devsnek I hope that they are syntax errors, because it seems like that would be for the best.
And yeah, that's a good, simple example of using return
in a do-block
. That is one of the few things a do-arrow
can't do.
Looking at this example, though, it bugs me a little. Usually when you create a variable, you intend to use that variable, but if the do
expression assigned to it causes a return
, that variable may never be used.
It seems to violate my expectations a little bit. This seems like one of those things I would try to avoid doing in my own code, since it seems like a surprise. But I can see how it can still be useful.
Maybe this is why I just couldn't think of an example for this particular use case... I'm biased against doing something like this, and my brain was trying to avoid it.
When I was trying to think up examples of my own, I kept focusing my thoughts on generator functions and using yield
in a do-block
instead of return
. The yield
statement would not violate my expectations as it isn't a terminal statement like return
; the do-block
would still have an opportunity to finish evaluating and a variable assignment would still occur (assuming the generator function's iterator continued to be used).
@zenparsing I did see that idea floated. I will think on it and see if I can find any insights about it for you. But maybe not tonight. I've spent enough time writing about this for now.
@JHawkley
The title of this issue is ”Do-Expressions vs IIFEs”, but you are in fact comparing ”do-blocks” and “do-arrows”.
You’ll say that ”do-arrows” are ”IIFEs”. That’s true semantically, but not syntactically, the big difference being that the latter is part of ECMAScript, the former is not. So, in order to make your intention clear, what are you exactly discussing:
(() => { /* ... */ })()
(not to be confused with IIAFEs: (async function() { /* ... */ })()
))?I presume you are discussing (1)?
That said, here is my critic on the proposed do-arrow syntax:
do => { /* ... */ }
This is very similar to an arrow function with a single parameter:
foo => { /* ... */ }
The only syntactical difference is that the parameter name is replaced by a keyword. However the semantics is quite different.
I have not yet read your analysis thoroughly, but I’m already making the following remark:
Other Issues With Do-Blocks
Given that a do-block just looks like a do-while construct without the while part, you have to ask: should this be permitted?
const result = do { if (Math.random() > 0.5) 42; else 0; } while (false);
Should the engine detect and flag obviously useless expressions as an error unless the do-block is in expression position? The following is almost certainly a bug, after all.
// The result of this block is not being used. do { if (Math.random() > 0.5) 42; // Error? else 0; // Error? }
The ECMAScript grammar has already similar situations, with a known effective solution: see https://github.com/tc39/proposal-do-expressions/issues/31#issuecomment-418514110.
@claudepache
I gave IIFEs a specialized syntax primarily so they're viewed on more level ground. Having to use (() => { ... })();
in the IIFE examples while comparing them to the much cleaner do { ... }
syntax might have created a bias toward the more syntactically pleasing option and therefore colored people's opinions as they read the document.
And since this proposal is about creating new syntax, why not show what such a syntax for an IIFE might also look like?
Well, just know that the do-arrow
syntax was for illustrative purposes only. I regret not making that clearer in the original document. My apologies! 🙇
But that's a very true observation about the syntax I chose for the IIFE; I really overlooked that problem. I'm not too sure how that could be addressed. You definitely would want an arrow there, since it essentially is a function. That's important for indicating where flow-control statements change context.
Maybe drop the do
and use only a thin-arrow for an IIFE instead?
const foo = -> {
if (Math.random() > 0.5) return 42;
return 0;
}
You might say, that function's arrow is so thin that only its return value is tangible! 😄
However, it was brought up in the original arrow-functions proposal that one of the reasons that having a shorthand for a non-binding function
using the ->
syntax was dropped was because, "...it’s confusing to have two arrows..." So using ->
at all might not be popular with the committee for the same reason.
Like all "good" problems, this one doesn't appear to have a perfect solution. Any syntax chosen would probably require an argument of "usefulness over confusion" be made.
@JHawkley break
does not have the black-hole behavior you're describing, at least not in every context. Many statements use UpdateEmpty to copy the preceding completion value: http://jsfiddle.net/xk3854uz/
@gibson042 I don't understand what you're trying to tell me. I appear to have demonstrated that break
works in the same manner you have done in this jsFiddle.
@gibson042 Ahh, wait, I think I understand what you're saying. No, that is true; it is not actually setting the completion value of the switch
statement. It is just exiting the block, and whatever was last evaluated is propagated as the completion value.
What I was trying to say is that the syntax break with <expr>
would really only have a use in a terminal code-path of a do-block
and its only practical use would be for setting the completion value of the whole do-block
. It wouldn't have much of a meaning anywhere else.
It probably could be used outside of a do-block
...
let foo;
switch ( 1 ) {
case 0: break with (foo = 0);
case 1: break with (foo = "this string");
default: break with (foo = "default");
}
And so break with <expr>;
would probably be equivalent to <expr>; break;
...
But why would you use it this way outside a do
expression? It is a syntax change that would only support do
expressions.
The major take away I found from all this is that this proposal will need to do a lot more than just define a syntax for a do
expression. There will need to be many other changes all over the language to make using this new do
construct palatable and friendly.
If you read any part of this document, read the For Loop and Comparisons to Other Languages sections which go into the problems that do
expressions would have with ECMAScript's loops and explains why changes to them would be important to properly support them inside of do
expressions.
I have never thought this proposal needed any signification clarification, and even after reading your OP, and the other discussion around these topics, I still don't understand what people have a problem with.
I feel like possibly one of the reasons people keep bringing up the early-exit issue and comparing to IIFEs and whatnot is because they just don't understand the inspiration behind the proposal and the main support behind it.
One of the main features behind this proposal is statement-as-expression. Without that feature, this proposal has, in my opinion, no reason to exist. If you think requiring a keyword to return a value from a do-expression is a good idea, you are missing the point entirely. Make your own proposal for an IIFE shortcut if you really want that, don't try to co-opt this one.
No disrespect, but I'd go as far as to disagree with @zenparsing. This is a completely useless discussion and I very much wish that this repository had some strong moderation to eliminate issues that are off-topic, misunderstood, and otherwise contrary to the spirit of the proposal.
@JHawkley there is no need for break with …
anywhere AFAICT, because blocks in which break
is semantically relevant all appear to use UpdateEmpty to reach behind it for their completion values. Which is demonstrated by the linked demonstration, because eval
can expose statement completion values.
@gibson042 You are correct, the break with <expr>
syntax is not necessary... But it would be desirable by users of the do
expression for the purposes of code clarity.
@pitaj ECMAScript's loops have problems when used as expressions. One of the purposes of this discussion is to state that clearly with examples. Having statements-as-expressions may be the main feature of this proposal, but I'm trying to demonstrate that there are problems with the concept due to the way ECMAScript has designed the behavior of those statements.
This feature would have a lot of jank to it if adjustments are not made to overcome the problem and I do not believe that this proposal would pass muster without this problem taken into consideration by the proposal.
I don't appreciate you being so dismissive regarding these concerns. This "early-exit issue" keeps being brought up for a good reason; exiting early from a loop is an important ability to have and it is not a graceful process with a do
expression right now.
As I said, other languages that have expression-oriented programming give very special consideration to loops, considerations that ECMAScript has not given because it is not an expression-oriented language.
It is a functional language, and functions work very well with loops because return
is always a terminal statement. And that is why shifting focus to IIFEs over the do
expression is likely a good idea; they actually fit into ECMAScript as it was designed.
I guess I'll deconstruct it bit-by-bit, then. I'm highly aggravated by your general lack of curly braces so I'm going to insert them where they should be, according to every popular JS style guide I know of:
As you pointed out, no meaningful difference.
Your issue is with how switch works, not with how do-blocks work. If JS had a Rust-like match
syntax (there is a proposal for it), then this would be entirely moot.
break with
You don't need it. Just use <expr>; break;
. It's not a problem.
AFAIK, you can just write the first example like this:
const numbers = [1,3,5,2,4,6];
const firstEven = do {
for (const num of numbers) {
if (num % 2 === 0) {
[num];
break;
}
}
} || [];
firstEven.forEach(console.log);
You don't need the increment operator (most style guides ban it anyways), just use two statements instead:
const trailing = { pos: 30, vel: 1.2 };
const leading = { pos: 300, vel: 1.0 };
const timeStepsToIntercept = do {
if (trailing.vel <= leading.vel) {
Number.POSITIVE_INFINITY;
} else {
let steps = 0;
while (leading.pos > trailing.pos) {
trailing.pos += trailing.vel;
leading.pos += leading.vel;
steps += 1;
steps;
}
}
}
console.log(timeStepsToIntercept);
The early-exit point here is moot, because it's likely this kind of expression would be used in a function anyways, in which case return
would be just as useful.
The
while
loop can work well withdo-block
, but the moment you start doing anything more complex, such as using anif-else
to conditionally break out, the same issues that affectfor
can manifest.
You can't just claim this without giving examples and expect to be taken seriously.
Syntax ambiguity has been addressed many times throughout issues on this repository. These things have been conquered before in the language specification. The arguments for alternative syntax should be around brevity or programmer confusion, not about parser issues.
Detecting useless expressions is left up to linters and the like. This is highly unlikely to change.
I though of this example pretty quickly, it may not be the best, but it shows some usefulness:
A do block as the body of an arrow function, which only saves one set of braces, but allows for statement-as-expression anywhere:
const isEven = (number) => do {
if (number === 0) { return true; }
!!(number % 2)
};
The difficulty is in dealing with loops, which have been demonstrated to be a weakness for the do-block.
You've demonstrated no such thing, even without my corrections to your examples.
In Rust, the only type of traditional loop that can be evaluated as an expression is the infinite loop which requires the use of break in order to leave its body.
Rust does not have null
or undefined
. ES does. A simple solution to the loop problem is to make all loops just have a completion value of undefined
unconditionally. However, the current completion value paradigm I have no problem with, personally.
On the other hand, all of ECMAScript's current flow-control structures are designed to be utilized by normal functions and they all work swimmingly with do-arrow with no adjustments necessary.
Wow, if you gut the main feature of this proposal, all of the sudden all of the carefully chosen things you've pointed out become irrelevant. How convenient! Go make your own proposal.
But when loops are involved, a do-block is more likely to cause a programmer to write bad and/or confusing code.
This has been discussed before. We don't need a 3000-word essay to point it out. Contribute to existing issues on the matter if you want. It's a trivially solvable problem even if you consider it a problem. I don't, as it's an optional feature people are unlikely to use, and trivially caught by a linter.
This problem can only be mitigated with a trade-off: the do-block either needs to be less useful or the language needs to be more complex.
No it doesn't. Completion values are not currently exposed in the language with the only exception being eval
, which nobody cares about breaking.
The do-block also requires changes to the syntax of break in order to get cleaner, more readable code in switch statements and loops.
Only if you consider <expr>; break;
particularly gross. It's not required by any means. The frequency at which people will be using the main feature of do blocks (statement-as-expression) is much much higher than they will be using it with loops or switch.
And when you introduce that change to break, all you're really doing is emulating an immediately-invoked function expression, but with a less familiar look and feel.
Except that you still have statement-as-expression, the main feature of this proposal.
The do-block offers little that the do-arrow does not.
Besides its main feature, you mean. The entire reason for its existence.
Implementing do-block would come at significant cost, and all of it just to avoid having to type return in your do expression's body.
In my opinion, do-block isn't worth it.
You just don't value the main feature of this proposal. That's fine, but don't act like you're just proposing an alternate syntax or behavior. Make your own proposal.
IIFEs are already the idiomatic solution to the problem that this proposal is trying solve.
Then create a proposal for an IIFE syntax. There probably already is one. Good luck convincing people that saving four characters is worth it.
Why not just use this proposal to formalize their usage into the language with a slick syntax like that of the do-arrow?
Because this proposal is about statements-as-expressions.
other languages that have expression-oriented programming give very special consideration to loops, considerations that ECMAScript has not given because it is not an expression-oriented language.
This perspective applies to every change you'll ever make to the language. ES wasn't built with functional programming in mind, yet you see a pipeline proposal. ES wasn't built with object oriented programming in mind, yet you see classes.
I think statement-as-expression fits well within ES. I'd love it if ES was built for it in the first place, but here we are, just now getting [].flatMap
.
@pitaj You first say, and I am paraphrasing, "this guy doesn't know what he's talking about. Why can't we shut him up?" And then you belittle me throughout your post "correcting" my code while disregarding many of the points it was fashioned to make.
This is a pretty disrespectful thing to do to someone who just spent their whole weekend studying this and preparing a report to hopefully guide and assist this proposal.
I am really offended by you, @pitaj.
I'm highly aggravated by your general lack of curly braces so I'm going to insert them where they should be...
I'm sorry that you dislike my coding style, but that is neither here-nor-there.
You can't just claim this without giving examples and expect to be taken seriously.
I'll accept that criticism. I should have shown how the same issue that affects for
also affects while
, but I thought the for
loop sufficed for that. A for
loop is just a while
loop with some special mechanisms that occur between iterations of the loop, after all.
I thought people would just know that... You can re-write the for
loop with while
without problems.
When I was doing the while
loop example, it was late and I just went with the first example that came to me.
So, I'm sorry for that. I should have done better.
Rust does not have
null
orundefined
. ES does. (...) However, the current completion value paradigm I have no problem with, personally.
Yes, indeed. The current thinking in language design is that null
was a mistake, and we got undefined
to deal with in ECMAScript too. It is good that Rust has neither.
But since ECMAScript has them, we need to deal with them.
I think it is a very bad idea to allow a do
expression to implicitly resolve to undefined
if no code-path results in a terminal expression. I believe ECMAScript would be unique among expression-oriented programming languages in permitting that. The conventional wisdom is that expressions should have a defined answer and those that do not should be considered errors.
Without a protection like that, you'd be far more likely to cause undefined
to leak into your code. Perhaps @pitaj would be okay with that, but I definitely wouldn't be. We should be doing everything we can to ensure that undefined
is never a surprise. (And the same should apply to null
as well.)
A simple solution to the loop problem is to make all loops just have a completion value of
undefined
unconditionally.
Which is sort of option number 2, except that you still wouldn't want that undefined
to leak out of the do
expression.
If you allow undefined
to leak out of do
expressions, then some of what I said stops being an issue. But the mental stress on the programmer would increase greatly, needing to worry that all code-paths terminate in an expression to prevent an undefined
from suddenly manifesting in their code.
I would hope that others who are here understand the wisdom of not permitting this to happen.
Just leaving a friendly reminder to everyone in this thread equally: all TC39 repos follow our Code of Conduct. Please adhere to it.
I didn't mean any disrespect or offense. I respect the effort you put into this, I just don't like to see this kind of effort wasted on what I see as redundant discussion. I did use the word useless but a better word is redundant.
All of the problems you brought up have been discussed in previous issues on this repository. That includes but is not limited to early-exit, break semantics, loop completion values, and IIFE vs block.
I wish you had, instead of creating a monolithic report about all of your problems with the proposal, contributed your views to those individual discussions. I hope you weren't motivated to centralize your comments in order to push for this IIFE shortcut syntax.
I code in rust a lot, and I really love it, but ES has no idiomatic concept of an Option. null or undefined is used in place of None. Comparing the two languages is probably useful when considering novel, ergonomic language features, but one must consider the idioms of ES when proposing features for it.
The lack-of-completion-value issue is not exclusive to loops. For instance, consider an if without an else. The following do block, under current semantics as I understand them, would result in either 1
or undefined
:
const thing = do {
if (a === 3) {
1;
}
};
Returning undefined in an undefined case seems perfectly reasonable to me. Heck, even the name matches. Considering this, I see no inconsistency when it comes to loops.
Now, maybe you want do expressions to require an explicit value. You want the code above to result in an error. That's a novel concept and one that I support, but it isn't really consistent with the behavior of ES. For instance, functions without a return statement result in undefined automatically.
In ES, all code-paths result in either a "real" value, or undefined. The current semantics of do expressions is consistent with that.
@JHawkley As an experienced contrarian, I knew you were going to get hammered eventually : )
@pitaj As the expression goes, "don't shoot the messenger".
The analysis in this post is very thorough and should be useful when considering the downsides to this proposal. Specifically, it makes a good case that relying on completion value semantics in this manner does not result in more readable code. The expr; break;
pattern in particular is pretty bad.
I believe this question is also raised in #21.
I see nothing in this analysis about the performance gains of do
expressions. I'm no expert in JS execution engines so it might not be an issue but I know that function invocations are expensive in themselves because of having to push everything in the current context to the stack and then pop it. If you really want to work with expressions you would have to put IIFE all over your code-base which means a lot of extra function invocations. Perhaps the engine can inline the calls I don't know but anyway having do
available I think all expenses of function invocations could be avoided?
Do expressions with IIFE semantics would not have to involve function calls, either at a specification level or at an implementation level. You would essentially evaluate them as a block and "catch" any return completions.
@zenparsing I'm not sure what you mean by "Do expressions with IIFE semantics". Do you mean do
expressions as proposed in this repo? In that case are you saying do
expressions as proposed in this repo would have a performance benefit over using the IIFE as available today since the proposed do
expressions do not incur function calls? If so I would say that is relevant info for this comparision.
@jonaskello Sorry, by "do expressions with IIFE semantics" I mean the thing called "do-arrow" in this issue.
@zenparsing Ah, thanks I did not read this issue carefully enough. I got the impression this issue compared this repo's proposal for do blocks to traditional already existing IIFEs. I guess that's what you get for only reading the header and the first lines :-):
Do-Expressions vs IIFEs: A Comparison It has occurred to me that a comparison between the do expression and the currently idiomatic immediately-invoked function expression (IIFE) has not been done yet. Doing this would be important for determining how much is actually being gained from introducing the do expression to the language and what challenges need to be overcome in its implementation.
I see now that this issue actually goes on to propose yet another new syntax called do-arrow that does not exist today and the comparison is actually for two new syntaxes. From above my question is also answered in the section about the new do-arrow syntax:
- The body of the do-arrow is executed immediately when it is being evaluated and is compulsively inline-optimized; there is no actual function-call overhead associated with the use of do-arrow.
My point was that this is not true for the already existing IIFE syntax. Sorry for the confusion.
Never would have guessed do => {}
is not just a plain arrow function expressions.. That's some strange syntax.
Having the characteristics of an arrow-function, flow-control statements (like
return
,yield
, etc.) cannot be used to influence program flow external to the do-arrow. The only way external program flow can be influenced directly is by throwing an exception.
If I'm reading this right, you propose that a do-arrow
within an async function
would not be able to use await
to pause the execution of the outer function (just as a do-arrow
within a function*
would not be able to use yield
to pause the execution of the outer function).
I think that not being able to await
in the body of the do-arrow
would be a major downside. The whole benefit of async
/await
is that it lets you write asynchronous code while structuring your code the same way you would structure synchronous code, but here'd we'd be introducing a new way to structure synchronous code which could not be used by asynchronous code.
I'd be very reluctant to adopt any version of do
expressions which couldn't be used seamlessly in asynchronous code. That is to say, I feel strongly that do
expressions should inherit the ability to await
from the surrounding code, just as most other expressions do (but by contrast to IIFEs and arrows).
Hope the difference between the do expression and IIFE can be documented in the README
It has occurred to me that a comparison between the
do
expression and the currently idiomatic immediately-invoked function expression (IIFE) has not been done yet. Doing this would be important for determining how much is actually being gained from introducing thedo
expression to the language and what challenges need to be overcome in its implementation.Below, a friendly syntax will be defined for each and then tested against ECMAScript's various flow-control structures to see how each performs.
Defining the Do-Block
The
do-block
is the name I will be assigning to the currentdo
expression proposal.It has the syntax
do { <expr> }
where<expr>
is a set of statements that terminate with an expression.For the purposes of this exercise, it has the following characteristics:
return
,yield
, etc.) can affect program-flow external to thedo-block
.throw
, or areturn
from the enclosing function (if applicable).Defining the Do-Arrow
The
do-arrow
is the name I will be assigning to a syntax that produces traditional immediately-invoked function expressions. It will be based on one of the usages of CoffeeScript'sdo
keyword for evaluating an IIFE.It has the syntax
do => <expr>
ordo => { <function body> }
, similar to the arrow-function syntax. Unlike arrow-functions, it supports no parameter list.For the purposes of this exercise, it has the following characteristics:
return
,yield
, etc.) cannot be used to influence program flow external to thedo-arrow
. The only way external program flow can be influenced directly is by throwing an exception.do-arrow
is executed immediately when it is being evaluated and is compulsively inline-optimized; there is no actual function-call overhead associated with the use ofdo-arrow
.return <expr>
statement (the<expr>
is mandatory) or athrow
statement. This keeps an unintendedundefined
from being returned as the result of the expression.The following code:
...is effectively equivalent to:
Key Differences
There are really only two differences between these two constructs.
Result Selection Behavior
The
do-block
implicitly selects the final expression evaluated within it as the result of the block, whereas thedo-arrow
must always have its result explicitly specified withreturn
.Ability to Affect External Program Flow
Because
do-block
is treated as a normal block within its enclosing scope, it can use common flow-control statements likereturn
,yield
,break
, andcontinue
to influence constructs external to thedo-block
. This can give it higher versatility compared todo-arrow
.The
do-arrow
is isolated from its enclosing scope by its function scope. It can only directly influence the flow of the external scope by throwing an exception. This gives thedo-arrow
higher purity compared todo-block
. As its only purpose is to evaluate to a result value, reasoning about thedo-arrow
in code is a little easier.Both constructs can still use and mutate variables that are accessible in the external scope and generate side-effects.
Examples
Now that we know how everything looks and works, it's time to see how they perform against each other. We'll run through a small exercise with each of the common flow-control structures in ECMAScript, except
try-catch
, which is too trivial to gain much insight from.The syntax for each construct will be kept to its current definition, though the
break
statement will have some special considerations when it becomes relevant later.If-Else Chain
For this example, a count-of-things will be mapped to a fuzzy-word that describes the amount, such as "one" or "many".
Block Version
The
if-else
chain is the poster child of thedo-block
, in that it is entirely clear and concise.Arrow Version
Observations
Both versions hold up against the other quite well. The
do-arrow
gets some bad-marks for being slightly longer, but not by much since it was able to exchangeelse
forreturn
. That did introduce a repetitive use ofreturn
, though.However, you can also say the
do-block
has a repetitive use ofelse
...Switch
In these examples, we will use a
switch
to map some game controller input to an action.Block Version
We get a big blemish when it comes to using
break
in ado-block
.The expression we would want to be the result is not the last statement in the block; the
break
statement is. Reading and understanding this code takes more mental effort than should be necessary. It is also much longer, though you could also write the break on the same line, as in:case 1: "walk"; break;
Because each
case
had multiple statements, I decided not to do that for clarity. But this isn't the end forswitch
.There is a proposal to change the syntax of
break [<label>]
intobreak [<label>] [with <expr>]
, where thewith <expr>
is used to explicitly specify the completion-value of a case, loop body, or labeled block when exiting it.That would look like this:
This is better! But it is literally just a re-implementation of
return
that is only useful in ado-block
.Arrow Version
So, why not just use the
return
we have already?Observations
In order to have clear code with the
do-block
, a syntax change is necessary. The proposedbreak [<label>] [with <expr>]
syntax really only has one use: resolve ado-block
to a specified expression.This is almost identical to
return
, which resolves a function to a specified expression.The
do-arrow
demonstrates that a syntax change is not necessary in this context, as a flow-control statement that pretty much does the same thing is already present in the language.For Loop
In the
for
loop examples, we'll be using ourdo
constructs to search an array for the first even number. For purposes of demonstrating a critical weakness indo-block
, the result will be packaged in an array; if the array does not contain an even number, thedo
expression will indicate it with an empty array.In other words, we're treating
Array
as anOption
type.Block Version
We'll start with the intuitive version, using the new
break
syntax to make it look nice:It looks good, but you might be noticing a problem. During the evaluation of this
do-block
, three empty arrays are allocated and discarded before the block provides the result of[2]
. Not every expression is side-effect free, and a programmer will need to recognize and accommodate for it in order to create optimal code within a loop.This leads us to the second version, which extracts the empty array allocation out of the loop:
Better, but we still have one potentially unused array allocation. That's not really terrible and you can likely leave it at this without worrying about your application's performance, but can we get rid of that unnecessary allocation entirely?
Yes we can! ...with a nested
do-block
and a check fornull
. Because it would be an error to have a code-path that does not end in an expression, we need to explicitly providenull
from thefor
loop's block; we can't just allow the block to terminate without a value.There are yet other ways to write this, including doing the sensible thing and using
Array.find
instead, but the point is to show off a worst-case scenario of afor
loop in ado-block
, and these are about as good as you can do while sticking to the constraints of this exercise.Arrow Version
The
do-arrow
version of this is idyllic by comparison.The most obvious solution was the best solution.
Observations
This example was obviously cherry-picked to demonstrate how a
do-block
can push a programmer to write unnecessarily convoluted code. Because ECMAScript's loops were not built with expression-oriented programming in mind, they can often work unintuitively with thedo-block
.Correcting this would require changes to the language that affect these core flow-control constructs or introduce new loop constructs that work better in a
do-block
, both of which are kind of a big deal to do.As an example, introducing a
for-else
construct, which might work like a similar construct in Python, would do a lot to clear this up:However, ECMAScript's loops are already built to be used in functional programming and the
do-arrow
exploits that to have clear, concise code without requiring new syntax.More thoughts on this issue are considered in the "Comparisons to Other Languages" section.
While Loop
In the
while
examples, we're going to run a little simulation, determining how many time-steps it will take for two objects traveling on a one-dimensional line to intercept each other. In each time-step, the object'svel
is applied to the object'spos
and then interception is checked for. An intercept occurs when the objects swap relative positions on the line.For simplicity's sake, we'll assume both objects are traveling in the positive direction. If the trailing object does not have enough velocity to catch up to the leader, the result should be
Number.POSITIVE_INFINITY
.Block Version
This looks pretty good. Since a
while
loop utilizes a simple exit condition, the chances of needing to complicate things withbreak
is much reduced; there is no need to use anif-else
construct to get the result we want in this instance.Arrow Version
Observations
Both of these are nearly identical. Since the
while
loop ends when the desired value is calculated, the loop can be kept simple. Thewhile
loop can work well withdo-block
, but the moment you start doing anything more complex, such as using anif-else
to conditionally break out, the same issues that affectfor
can manifest.The only real "gotcha" in this example is the usage of the increment operator
++
: if the more common postfix operator was used instead of the prefix operator, thedo-block
would have reported the wrong answer.But that is just one of those trivia things you need to be aware of, and not really a fault of the
do-block
.Other Issues With Do-Blocks
Given that a
do-block
just looks like ado-while
construct without thewhile
part, you have to ask: should this be permitted?Should the engine detect and flag obviously useless expressions as an error unless the
do-block
is in expression position? The following is almost certainly a bug, after all.What About the Program Flow Difference?
Earlier, I stated that one of the key differences was that
do-block
can use flow-control statement likereturn
andyield
to affect the program flow outside of its block while thedo-arrow
can not.Truth is, I couldn't think of an example to showcase that. In all the examples I thought up, these statements were always better placed outside the
do
. I couldn't find an example where it would be a clear advantage to have such a thing inside thedo
expression itself, and I didn't want to have a contrived example.If anyone can produce such an example demonstrating this difference, that would be very helpful.
Comparisons to Other Languages
Other languages that offer expression-oriented programming have been built from the ground up to support and utilize it. ECMAScript unfortunately was not and
do-block
would suffer from a lack of support by its language.The difficulty is in dealing with loops, which have been demonstrated to be a weakness for the
do-block
. Other languages consider the way loops work as expressions very carefully.In Scala, the traditional
for-loop
is replaced with thefor-comprehension
and the classicalwhile
loop is one of the very few things that cannot be evaluated as an expression; Scala programmers are meant to use thefor-comprehension
and tail-recursive functions wherever possible for looping instead, only falling back on thewhile
loop when it is absolutely necessary.In Rust, the only type of traditional loop that can be evaluated as an expression is the infinite
loop
which requires the use ofbreak
in order to leave its body. Because abreak
is mandatory to keep the program from becoming stuck, it can safely be used to provide the result of theloop
expression.None of this was a consideration for ECMAScript's loop structures. Some combination of these options would need to be taken in regards to
do-block
:break
syntax.for
andwhile
loops within ado-block
, forcing programmers to use more traditional methods to extract information from loops and mitigating the side-effect issue.do-block
. Perhaps the old array-comprehension idea could be resurrected. Or maybe afor-else
andwhile-else
construct could be introduced, where theelse
block is only executed if nobreak
was encountered in the loop's body. That would be very Python-like.On the other hand, all of ECMAScript's current flow-control structures are designed to be utilized by normal functions and they all work swimmingly with
do-arrow
with no adjustments necessary.There is also prior art for
do-arrow
in ECMAScript's family of languages. CoffeeScript'sdo
keyword is often used in exactly this same way to produce a result from a complicated set of statements and prevent scope pollution. It is why I based thedo => { ... }
syntax on it.Conclusion
When you really dig into it, the
do-block
is at best as good asdo-arrow
when it comes to theif-else
andswitch
constructs. But when loops are involved, ado-block
is more likely to cause a programmer to write bad and/or confusing code.This problem can only be mitigated with a trade-off: the
do-block
either needs to be less useful or the language needs to be more complex. Neither of these are particularly good trades. Thedo-block
also requires changes to the syntax ofbreak
in order to get cleaner, more readable code inswitch
statements and loops.And when you introduce that change to
break
, all you're really doing is emulating an immediately-invoked function expression, but with a less familiar look and feel. Thedo-block
offers little that thedo-arrow
does not. Implementingdo-block
would come at significant cost, and all of it just to avoid having to typereturn
in yourdo
expression's body.In my opinion,
do-block
isn't worth it.IIFEs are already the idiomatic solution to the problem that this proposal is trying solve. Why not just use this proposal to formalize their usage into the language with a slick syntax like that of the
do-arrow
?