Why I prefer objects over switch statements
enmascript.com
enmascript.com
Most importantly though, the performance profile of the solution proposed scares me a lot. To understand why, consider that it is not uncommon for switches to be JITted as:
- If statements and gotos for small number of options or - Collections of lambdas for high number of choices (note, much more optimized than the lambdas proposed, very likely!)
The reason they are built this way is performance (another commenter ...commented that performance doesn't matter - they are wrong, a switch can be nested in a hot loop ran millions of times and they do matter). Therefore, it's easily arguable that the presented pattern will significantly worse in some cases (few choices) and that should not be underestimated.
The point in the article is not that switches are less readable, but that switches can be "less" readable. Or—more accurately—they can look like they're readable but obscure unexpected subtleties
The object syntax may be slightly less readable than the switch in its simplest, most well-written form, but the point is that unlike the switch, it always unambiguously does what you expect.
It's also worth noting that the object syntax is extremely idiomatic in modern JS. It may look less readable to a generalist, but to anyone regularly maintaining JS it's far more familiar than the switch. (I guess this isn't so much a point in it's favour, it's more a point against modern JS being easy to pick up, but hey).
> it is true that switch statements in JavaScript have issues with breaks and code blocks, but any half decent linter will tell you about those.
While you have a point about linters, and I do use a strict one in every project, I'm much more comfortable with it being a safety net than the first line of defence.
Also, those problems with breaks and code blocks are hardly unique to JavaScript.
I'm sure I'll get flamed, but I really liked this:
const getPosition = position =>
({
first: 'first',
second: 'second',
third: 'third'
}[position] || 'infinite');Honestly, I find that rating the readability of the object versions vs the switch statement is bikeshedding. This is perfectly readable and doesn't involve going back and forth with my eyes to figure out the data flow or figure out if I have mismatched brackets or what have you:
const getPosition = position => {
switch (position) {
case 'first':
case 'second':
case 'third':
return position;
default:
return 'infinite';
}
}
Another thing that is worth mentioning is that objects consume memory and allocating memory in JS for this is just wasting orders of magnitudes more cycles and sacrificing throughput for no good reason - other than to try to be clever and avoid an idiomatic and optimizable construct.This is just a semantics argument. I can replace "ugly" with "hard-to-read" if you want. I think it's both, in this instance.
>Honestly, I find that rating the readability of the object versions vs the switch statement is bikeshedding.
I agree in the context of code review, but since this is what we're talking about I figured I'd share my opinion.
>Another thing that is worth mentioning is that objects consume memory and allocating memory in JS for this is just wasting orders of magnitudes more cycles and sacrificing throughput for no good reason - other than to try to be clever and avoid an idiomatic and optimizable construct.
Premature optimization. If you need to optimize, then do it. Otherwise you should prioritize what you consider understandable and easy-to-write code. I guarantee the vast majority of javascript written would not be ill-affected by this.
A switch statement is a pretty idiomatic choice for the example. Merely knowing more about the low level cost of different options does not make a snippet an optimization.
Examples of actual optimizations (from real world code I've seen) would include using bitmaps, lookup tables, large regexps, tries and binary search. All of these have the property of obscuring the search space in the name of performance, and many come with trade-offs such as increased startup cost or code size. Choosing to use a trie here, for example, without considering its trade offs would be a premature optimization. A switch statement is at worst a refactor that maintains the same algorithm.
{'a': foo(), 'b': bar() }[...]
If foo() or bar() ever introduce side-effects or become expensive, you're gonna have a hell of a time figuring out what's going wrong.I'm curious how often you find yourself dealing with loops that run millions of times? I think the majority of loops I've written don't need to deal with millions of iterations; most of them probably only rarely break 1000 iterations, and I know for sure that a lot of them can't exceed 100 iterations because of limits in the data.
Seems to me that using a switch over another structure for performance at the expense of readability or maintainability is an example of premature optimization unless you're positive the condition is going to be in a hot loop.
But is not more readable or even more maintainable.
Plus there is absolutely no optimization involved, it's just another way of expressing it.
> But is not more readable or even more maintainable.
Both are a matter of opinion and depend on the specific use-case.
It's trivial to come up with 80s-level counter-examples to this.
A megapixel image, for example, is tiny.
Iterating over a megapixel image isn't a common scenario unless you're processing a lot of large images. Obviously you should optimize hot code paths.
There are plenty of reasons to iterate a switch statement, even much more than millions of times.
You get that these are outliers right? The majority of software doesn't need to concern itself with these problems. They're examples of software that deals with analyzing and running other software.
I'm not sure why I'm bothering to respond to these comments. This whole discussion has apparently been system developers telling app developers they need to start micro-optimizing their cold loops because "what if your user clicks the button 12 million times in under a minute".
Switches can sometimes be optimised away into O(1) using clever static compile time tricks, but if you rely on jump indirection you get branch misdirection penalties, or even worse for a ladder of if's, you are doing O(N) work (where N is number of cases).
Sean Johnson has given a fantastic talk about pattern matching in Clojure:
https://www.youtube.com/watch?v=n7aE6k8o_BU
He offers some interesting comparisons between Clojure and Erlang.
Going even further, I recently discovered Dynamatch:
https://github.com/metasoarous/dynamatch
"Dynamatch addresses these challenges by enforcing a certain structure in our ordering of match clauses such that many of the incidental complexities of order dependence in a dynamically extensible pattern matching system become more manageable."
Sometimes it seems like Erlang or Haskell has the last word in Pattern Matching, but I'm not aware of anything like Dynamatch in those languages.
- select for a map with key "a" and not "b"
- select for a map with key "a" and possibly "b"
- select for a map with key "a" and possibly any other key.
The other problem is that most comparisons are apples-to-oranges. Does core.async sound kind-of-like async/await? Write a checkmark in a comparison table and move on. But reality is nothing like it.
Every time I have to write some JavaScript (interop) code, I am amazed that people put up with all the incidental complexity. I mean, just recently I had to implement three different ways of accessing data which was essentially in an array. Three various pieces of imperative code with iteration. In ClojureScript that would have been zero code, because everything that is "array-like" can be accessed as a seq. That sort of thing does not come up in superficial comparisons.
Also for Clojure and Clojurescript you need to at least have some good level read proficiency in Java and Javascript.
The fundamental issue is called the "expression problem", and arises because the problem of assigning behavior is two dimensional (one dimension is the types/cases, the second dimension is the methods/operations), and possibly open along either dimension. Match works better when the methods/operations are open. Objects work better when the types/cases are open. If they're both open, then you need to figure out which one to make less open. At best, you can carve off partial sections where one particular dimension is open by fixing the other dimension, etc.
CLOS kind of punts and has you express each element of the matrix on its own. Which doesn't actually solve the problem, but at least makes it symmetrical.
That is the state of the art, AFAIK. Fixing the problem along either dimension is enough to make a workable language, but neither one is "better". There could be something better, but we haven't found it, and we're certainly not going to find it if people don't appreciate the whole problem!
The link I meant to include before.
David Nolen publicly used some paper about optimized PM
It's difficult to imagine a switch large enough where the performance difference would matter, but this ignores the memory required to store the lookup in the first place. In all of the switch examples a switch is more straightforward.
In the latter examples (see the Boolean example) we now perform the lookup twice if the value is present. I feel like this is just another case of "use the right tool for the job".
With a switch, there is no up-front allocation, case expressions are only evaluated if the case is reached, and the body of a case is only evaluated if the case is executed. The lookups are hardly the performance concern.
lookup = (() => {
map = { a: 1, b: 2 }
return (key) => map[key] || 'not found'
})()The JavaScript 'object' here is called "map" or "dictionary" etc. in other programming languages. (And the article's technique is fine).
I appreciate `switch` - with its case-fallthrough surprises beginners but most C-style languages all share this quirk - and modern-day compilers and linters will gladly remind you that usung Duff’s Device-type tricks in JS don’t work.
As an aside, in C#, a string-based switch statement is actually compiled to a lazily-initialised hidden Dictionary<String,Int32> object where the values are the real integer case values - so kinda similar to the linked article - except without the runtime possibly reallocating and reinitialising the dictionary object on every invocation.
However, I've come to love pattern matching in Rust. It prevents fall through entirely, which makes it unnecessary to check if the code is doing something clever with fall through, which makes the code more simple to parse.
Strings and Symbols only. Everything else is converted to a string, which works most of the time for lookups, but can fail depending on the stringification or if you're pulling keys back out of the object.
How often do you use a 'switch' statement whose cases don't always end in 'return' or 'break'? The "coroutines in C" [0] article is a clever use of switch-case as a goto, but it seems like you need to invent new types of control flow to use 'goto' properly. Does anyone have other clever uses of 'switch'?
[0] https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
https://github.com/ricardobeat/require-tree/blob/master/inde...
The goal is to accept a 'filter' argument that can either be a string, an array of strings, a regular expression, or a filter function. It fully uses fall-through and the multiple entry points. I find it magical, in that it turns all of those into a function so the remainder of the code doesn't have to care, and it's not any less efficient. Similar feeling to finding a use case for 'Infinity' :)
switch (type(filter)) {
case 'array' : filter = filter.join('|')
case 'string' : filter = new RegExp('^('+filter.replace('*', '.*')+')$', 'i')
case 'regexp' : filter = filter.exec.bind(filter)
case 'function' : return filter
}Switch statements are much easier to maintain if you keep them one or two lines long, and just have it immediately delegate to a function call then break. I think that's true for almost any code-flow syntax: if/elseif/else blocks, various loops. They all break down quickly if you have too much in them.
I do, occasionally. I prefer the inverted golang switch where fallthrough needs to be specified, since these cases (heh) are generally the minority.
When you're dealing with 4 possible values, each of which will result in wildly different code (e.g. evaluating the value of a options variable, or handling error codes that mean very different things), then switch is clearly the way to go.
When you're dealing with 20 or 200 different values, all of which map to a few similar variations, then defining an object or array lookup is clearly preferable.
"Preferring" objects over switch statements is like saying you prefer bitmaps over vector drawings -- it's nonsensical. Different tools are better for different jobs.
let myHandler = defaultArg (Map.tryFind theKey handlers) (fun x -> //default stuff)
myHandler theValue ....
I liked this approach, since I could dynamically add functionality, and it could be completely decoupled from the business logic, and I didn't have to use strings for keys, but my coworkers didn't like that how dynamic it was, since in fairness, it did sometimes make it a bit more difficult to figure out which path the code was going to go down.
Never really determined who was "right" in this case, but this post reminded me of that.
First I tried doing what the author suggested -- having 256 routines, and a dispatch table. Chrome performance got better, and Firefox performance got worse.
In the end, the fastest thing to do was to have "if (opcode < 128) { 128-way switch } else { 128-way switch }".
That was 2014, so likely things have changed.
The whole language feels like they crammed every possible feature into every other feature, as a cartesian product of syntax, rather than as Lisp or Tcl or Forth does, with simple syntax that's flexible so everything naturally works everywhere. Someone even made an http://fuckingifcaseletsyntax.com for Swift, so I don't think I'm alone here.
You can really see the C legacy by the name and overall structure. I still miss the simplicity and flexibility of COND, and :keywords. It's nice that Swift can identify unhandled enum cases, I guess, but I can't say that's ever been a problem I've run into.
Most of the examples here I would prefer to write as a dictionary literal (more declarative), or possibly a method on the enum (easy in Swift). It's only single-dispatch, but it's still much better than burying functionality inside single-use, untestable switch statements in the middle of a func. If something is useful enough to justify writing 8 or 10 lines of code to handle a set of cases, then I guarantee I'm going to want to evaluate it in the debugger next week.
The older I get, the less Turing-complete code I want to write. Code is a liability. Constant tables are pure value. Switch, then, is the worst: it takes something which looks very much like a constant table, and forces it to be code.
First, and this is more of a general observation for any kind of programming content, these pompous-sounding abstract value judgements need to stop:
1. Is more structured.
2. Scales better.
3. Is easier to maintain.
4. Is easier to test.
5. Is safer, has less side effects and risks.
Regarding `switch`, only the last is a fact and that's because of the `break` statement peril. Still there aren't really side effects or other 'risks' involved. Everything else is completely subjective and not supported by the examples above - I, for example, find switch easier to maintain as you don't need to juggle variables defined outside the object to keep it clean.Second, these articles use innocuous examples that don't reflect real use cases, and hence fail to demonstrate their utility. You'll find a ton of switch statements in any kind of parser since it's the perfect construct for the occasion where each branch can wildly differ in content and complexity, and might embed flow control that would complicate the object-based version:
switch (node.type) {
case "Identifier":
case "ObjectPattern":
case "ArrayPattern":
break
case "ObjectExpression":
node.type = "ObjectPattern";
for (var i = 0; i < node.properties.length; i++) {
...
}
break
case "ArrayExpression":
...
}
Finally, `switch` is wonderful when paired with `return`, since it eliminates point 5 above. Sample taken from a project I have lying around: switch (unit) {
case 's': return value * 1000;
case 'm': return value * 1000 * 60;
case 'h': return value * 1000 * 60 * 60;
case 'd': return value * 1000 * 60 * 60 * 24;
default : return null;
}
With the key lookup, you'd also end up precomputing all of those values (imagine that's a slightly more expensive operation than simple math), or turning each one into a function. Another good example is the state reducer pattern: switch (action.type) {
case 'ADD':
return state.concat(action.payload);
case 'REMOVE':
return state.filter(item => item.id !== action.payload.id);
default:
return state;
}
The key lookup pattern can hold its own in the simple cases, but it's hard to justify it with anything more than stylistic preference. const getValue = type => {
const email = () => 'myemail@gmail.com';
const password = () => '12345';
....
Now "const email = ..." will be executed every time even when you're just asking for password. Eventually, such a code "scales", become a bloated behemoth with twelve cases, called a hundred time deep inside a server, initializing everything every time it is called, with potential side effects......and then one day a starry-eyed new hire looks at the top-level code, thinking "Heh, this is an internal graph server, why does it need customer email addresses?", removes the top-level config line, and then suddenly all internal dashboards go blank because they can't read email addresses.
...Yeah, you can probably tell that I'm not a fan of this technique.
I used to love objects and multimethods (or single-dispatch multimethods for those more limited languages). But then I ended up debugging a large code base which used them extensively. It is a nightmare: by reading the code, there is no way to find out what all the dispatch options are, and without interactively debugging it there is no way to see which code will get called (inheritance messes things up greatly).
I think performance is secondary to these problems, so these days I prefer switch statements (or pattern matching), for their simplicity and reliability.
It could be as well lookups on a hashmap.
Not very expressive.
I'm also a fan of other patterns like early returns, but few people ever seem to do it.
On the other hand if I have to scroll up and down a lot to see all return paths then that's a problem. But I generally find the problem is that the function is too long/does too much and should be broken down into smaller pieces.
https://toddmotto.com/deprecating-the-switch-statement-for-o...
function psuedoduff(count) {
var x = 0;
var n = Math.floor((count + 7) / 8);
var o = {
0: function() { x++; o[7]() },
7: function() { x++; o[6]() },
6: function() { x++; o[5]() },
5: function() { x++; o[4]() },
4: function() { x++; o[3]() },
3: function() { x++; o[2]() },
2: function() { x++; o[1]() },
1: function() { x++; n--; if (n > 0) { o[0]() }},
}
if (count > 0) {
o[count%8]();
}
return x;
}
That just increments to show the point, it would be easy to make it do some sort of real work.Of course you'll run out of recursion at some point. Trampolining could fix that. I leave it as an exercise for the reader whether that makes it more or less silly.
I'm also of course aware that Duff's device doesn't make function calls, to which I vigorously handwave while chanting "Monads Are Programmable Semicolons" in my "Lambda The Ultimate" T-Shirt.
Lambda.
Now, they could, of course, do it using the function method, but they did not. If the functional code had logs in them it would become quite apparent that a bunch of if-else statements would probably do the job better.
Honestly, it's a code smell that makes me trust my initial reaction that the switch statement is indeed more readable than the authors solution.
((o) => o[expr_to_switch_over()])({
opt1: () => {
stmts();
},
opt2: () => {
stmts();
},
opt3: () => {
stmts();
}
})I completely disagree.
[1] Putting a function like this in a default case will enforce that all cases are declared at compile time:
function exhaustive(exp: never): void {}ok, ok, I'll go sit in the corner.