'Do' More with 'Run'
maxgreenwald.meFeels like code golf. The two run examples are basically the same, but now I have to reason about what ‘run’ is and I’ve made my stack trace more complicated.
I am in love with “everything is an expression” from my time with Rust. I regularly use a ‘let’ and wish I could just have the entire conditional feed into a ‘const’ given it’s never going to change after the block of code responsible for assignment.
I wish there were more generations of ‘use strict’ so that we could make bolder, more breaking changes to the language without breaking stuff or dying in a horrible fire of “just roll your own dialect using compiler plugins.”
> I regularly use a ‘let’ and wish I could just have the entire conditional feed into a ‘const’ given it’s never going to change after the block of code responsible for assignment.
I'm currently doing a lot of audio work, where I kind of want to define some parameters early on that won't change, except if something "magic" happens. So, a kind of "unlockable" constant. Think in terms of, a bunch of filter coefficients that are predetermined by the design of the filter but need to be calculated to match a given sample rate.
Just a kind of "set and forget" variable, that ought not be written to more than once, or maybe only written to by the thing that first wrote it.
> I am in love with “everything is an expression” from my time with Rust
Totally agreed! I wish JS had if expressions (maybe in the future?). It doesn't seem like such a huge change if it were rolled out slowly like other new syntax features but maybe I have no idea what I'm talking about.
Hopefully things like `run` can help move the needle on this. I like it because it feels more FP and intentional than IIFE's everywhere.
Not even code golf given that the example is a single character longer. Naming it "r" would be code golf (and "r" is definitely worse than "run" for reasoning).
What some people call "code golf," others call "syntactic sugar." :-)
I appreciate the sentiment, but i do not agree with the solution.
The actual solution is to extract a function. What is the legitimate excuse for not extracting a function (other than being lazy, which i will not accept as an argument)
Edit: Just to enhance my comment, having a separate function with a distinct name enhances readability and maintainability. Readability is enhanced because a clear name lets the reader understand what is happening in the function (this attribute is lost with self calling nameless functions). Also, less lines need to be read to understand the wider context. Maintainability is enhanced because 1) more readable code is easier to reason about and change and 2) extracting a function means it can be reused
Try using `run` in the return of a React (or similar) component and you'll never go back ;)
There is always a balance here. I'm not saying to never extract a named function, and there is certainly good reason to do that, especially if the function is called elsewhere or is quite complex.
But, in many cases, the inline logic is more readable because it's right there, and the function really doesn't need a name.
I will once more agree with the reasoning on the high level, but:
From my experience working in big react+ts codebases devs are nesting components and logic way to much, resulting in unmaintainable messes that neeed hours to refactor. This kind of utility enhances this mental model of nesting stuff instead of extracting. I am not suggesting the run utility will break the world, and maybe there are quite a lot legit usecases. But it is the equivalent (exaggerating a bit here) of giving every untrained person a bazooka. They're lack of proper use will cause caos
Extracting to a function means you need to give it a name. There is a middleground where you might want a function for control-flow purposes, but giving it a name makes the code look more complicated than it needs to be. Personally I just use an IIFE in those situations - and I don't see much benefit from run() as compared to an IIFE.
It already has a name, even in the contrived example OP named it "x".
The extracted function can simply be getX() or calcX() or generateX() - which verb chosen can tell the reader roughly the origin/complexity of X without having to read the function body: Does it already exist or are we creating it here? If we're creating it, is it internal or is it likely to require other resources like an API call?
In a more concrete example I'm sure it can get a better name than that, too.
That's true when you're assigning the result of the function to a variable, but what about the use case where you're replacing a ternary within JSX code? You could extract the evaluation to an assignment of a variable outside of the JSX, but that's the kind of unnecessary complexity that IMO is sometimes worth avoiding.
We are being quite abstract here since we are lacking real-life examples here, but using a variabe assignment intead of an inline if/else assertion does not add any complexity at all. It adds more code, yes. More complexity? No. Is it good or bad to add more code? I would answer: the more easily readable and maintainable version of the code is the better version of the code. More times than not, more code is the better version (when it is dry, with correct abstractions and good names).
If you're only consuming the variable once, and it's inside a declarative syntax like JSX, then I'd argue it's more readable to use an anonymous function, because the reader doesn't need to look elsewhere, and the function is defined in the same place where it's evaluated. And you don't need to bother thinking up a name for a variable, which could lead you to prematurely generalize.
Personally, most of the time I assign to a variable. But I do think there are cases where the IIFE is an appropriate and more elegant approach.
I do not see anything wrong with what you are describing, but in my experience, going into that excercise of extracting a function and giving it a name is an insightfull process. It gives you time to reason about what you are building instead of going full coding-monkey mode. Instead, you pause a bit and reflect. To this day I have never regreted doing that.
I like it! Another one-liner I'm constantly adding is
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
Whatever runs the main function should probably do more, like handling rejections.Same, I've written this so many times. Node just added this as a built-in utility FWIW
https://nodejs.org/api/timers.html#timerspromisessettimeoutd...
One control flow function I use often in JavaScript is my `runIfUnhandled` function:
export const UNHANDLED: unique symbol = Symbol("UNHANDLED");
export function runIfUnhandled<TReturn = void>(
ifUnhandled: () => TReturn,
run: (unhandled: Unhandled) => TReturn | Unhandled,
): TReturn {
const runResult = run(UNHANDLED);
if (runResult === UNHANDLED) {
return ifUnhandled();
}
return runResult;
}
I often use it to use guards to bail early, while keeping the default path dry. In particular, I used it heavily in a custom editor I built for a prior project, example: runIfUnhandled(
() => serialize(node, format),
(UNHANDLED) => {
if (
!Element.isElement(node) ||
!queries.isBlockquoteElement(node)
) {
return UNHANDLED;
}
switch (format) {
case EditorDocumentFormat.Markdown: {
const serialized = serialize(node, format) as string;
return `> ${serialized}`;
}
default: {
return UNHANDLED;
}
}
},
);Another short utility I often add (that I wish were a language feature):
function given<T, R>(
val: T|null|undefined,
fn: (val: T) => R
): R|null|undefined {
if (val != null) {
return fn(val)
} else {
return val // null|undefined
}
}
function greet(name: string|null) {
return given(name, name => 'Hello ' + name)
}
This is equivalent to eg. Rust's .map()Can also do a version without the null check for just declaring intermediate values:
function having<T, R>(
val: T,
fn: (val: T) => R
): R {
return fn(val)
}
const x =
having(2 * 2, prod =>
prod + 1)If I ever see this shit in code review I’m flagging it. IIFEs exist to avoid polluting global space in JS - its not something you are often dealing with in modern development. Especially the react example - make a new component, this prevents better readability like a new component or using an inline function.
Additionally for ever developer that hasn’t seen this, they have to look up the run function and try to grasp WTF it is doing. And god forbid someone modifies the run function.
In the "Use as a `do` expression" section, the example which uses `run` does not need the `else` cases and could be simplified:
function doWork() {
const x = run(() => {
if (foo()) return f();
if (bar()) return g();
return h();
});
return x * 10;
}Can also get rid of the `run` and move parens to simplify even further:
function doWork() { const x = () => { if (foo()) return f(); if (bar()) return g(); return h(); }; return x() * 10; }And a step further into the past, no need for a lambda - I find this clearer:
Where "calc" can be "gen[erate]" or "find" or at least more descriptive.function doWork() { function calcX() { if (foo()) return f(); if (bar()) return g(); return h(); } return calcX() * 10; }Well you definitely can do that to my somewhat contrived example that is loosely based off of an example from the `do` expression proposal, but I'm not sure its better.
Part of the beauty of `run` is that you don't have to declare and name a function `calcX`. In longer and more complex examples, declaring a function inline like this is potentially confusing because you don't get to see where it is used, whereas with `run` you assign the return value immediately to a variable.
But then there's mental overhead figuring out / remembering what the function does every time you go through the parent function. Once you have it in your head, may as well just label it so you don't have to do that every time.
> In longer and more complex examples, declaring a function inline like this is potentially confusing because you don't get to see where it is used
It only exists in the scope of the parent function, the only place it can be used is right next to where is declared. Unless you're in the habit of making functions hundreds of lines long, I guess...
Presumably this is a simplified example and "x" is intended to be used more than once?
Completely agreed, and I use this if-based early return syntax frequently! That being said, I like using `if` and `else if` and `else`, but maybe that's just me! I don't think there's a substantial difference in readability or utility.
Parentheses phobia strikes again. This is 1 character longer than an IIFE (since you replace "()" with "run"
i'd say that it's a bit more readable:
looks better than, and assuming pre-existing knowledge of what `run` does, is more understandable thanrun(() => { return foo })
but this is also a fairly contrived example(() => { return foo })()I agree that `run()` is more readable than an IIFE if you remove all context and history from the analysis. But the IIFE is a well-known idiom to JavaScript programmers, so readers will not have to pay a cognitive "what is `run()` do?" penalty in order to understand the code.
New abstractions have a cost, and "clever" abstractions tend to confuse the average developer more than the benefit they provide.
If there's a problem with an IIFE (yes, they can be abused), the usual approach is to replace it with a named function definition. This works in their React example as well--rather than (necessarily) creating a new component as they suggest, the standard approach is to add a named rendering helper in the function closure that returns JSX.
Yes, I do think the extra parens are less readable.
Its not about number of characters, its about reasoning that the inline function that you just wrapped in parens is then called later, potentially after many lines. At least with `run` it's immediately clear based on the name that you are running the function.
Edit: This is pretty funny. I'm a parensaphobe! (not really) https://www.reddit.com/r/ProgrammerHumor/comments/qawpws/the...