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'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.
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.
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
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.
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
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.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
Whatever runs the main function should probably do more, like handling rejections.https://nodejs.org/api/timers.html#timerspromisessettimeoutd...
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;
}
}
},
); 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)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.
function doWork() {
const x = run(() => {
if (foo()) return f();
if (bar()) return g();
return h();
});
return x * 10;
} function doWork() {
const x = () => {
if (foo()) return f();
if (bar()) return g();
return h();
};
return x() * 10;
} function doWork() {
function calcX() {
if (foo()) return f();
if (bar()) return g();
return h();
}
return calcX() * 10;
}
Where "calc" can be "gen[erate]" or "find" or at least more descriptive. run(() => { return foo })
looks better than, and assuming pre-existing knowledge of what `run` does, is more understandable than (() => { return foo })()
but this is also a fairly contrived exampleNew 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.
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...