9 ms·
Why I prefer objects over switch statements
- 8lall0 8y agoYou type far more code (which in JS matters A LOT) for a less flexible solution and for such a simple task. I completely disagree.
- lkrubner 8y agoThis wouldn't really be Hacker News unless an arrogant Lisp devotee shows up and claims that Lisp does everything better, so I'll now try to be that arrogant Lisp devotee. Sean Johnson has given a fantastic talk about pattern matching in Clojure: https://www.youtube.com/watch?v=n7aE6k8o_BU 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 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.
- arethuza 8y agoWhat about method dispatch in CLOS, pretty sure that counts as "does everything better" - at least when it comes to mapping tuples of values to methods.
- kazinator 8y agoMethod dispatch is arguably a form of pattern matching: pattern matching on the type signature of the argument tuple. (With eql specializers, on the object/numeric identities of the arguments, not only types).
- Jtsummers 8y agoAnd with some CLOS extensions, it can also be extended to other filters besides just eql specializers (this is also in Clojure's implementation of multiple dispatch).
- Jtsummers 8y agohttp://www.p-cos.net/documents/filtered-dispatch.pdf http://www.p-cos.net/documents/filtered-dispatch.pdf The link I meant to include before.
- bastawhiz 8y agoNo thread on HN would be complete without the JavaScript programmer jumping in to "well actually" your response, so I formally nominate myself to fill that role. ECMAScript is getting pattern matching! It's currently a stage 1 proposal with backing from Microsoft, npm, and Facebook. https://github.com/tc39/proposal-pattern-matching https://github.com/tc39/proposal-pattern-matching
- Scarbutt 8y agoI was researching clojure vs nodejs for a webapp, but found that the benefits clojure provides are very small and not enough compared to the benefits you get with JS the ecosystem. And Javascript 2019 is actually nice.
- jwr 8y agoThe usual problem with such comparisons is that in order to understand the benefits of Clojure/ClojureScript, you have to use those tools to write a reasonably large system. Otherwise the comparisons are superficial. 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.
- Scarbutt 8y agoI agree, Clojure is technically superior in almost every way when compared to JS IMO, and would much prefer to write Clojure than JS. My poor formulated point was that, sometimes the ecosystem is more important. An experienced dev that already knows how to do everything in the domain he's working on won't have much trouble with Clojure, but you don't always know how to do some things and this is were ecosystem(third party APIs, docs, books, libs, forums, etc...) helps greatly. Also for Clojure and Clojurescript you need to at least have some good level read proficiency in Java and Javascript.
- agumonkey 8y agolots of research about the subject lots https://duckduckgo.com/?q=pattern+matching+compilation&atb=v119-3__&ia=web https://duckduckgo.com/?q=pattern+matching+compilation&atb=v... David Nolen publicly used some paper about optimized PM
- mindslight 8y agoI don't think pointing the way to a better understanding of the problem is arrogance, especially when these type of posts effectively just add to other learners' misunderstandings. I can tell the poster is still learning due to that title, and because they call a switch statement on constants "typical" (as opposed to seeing it as a broken implementation of match). 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!
- AnimalMuppet 8y agoHmm. It seems to me that you failed at the "arrogant" part.
- rgoulter 8y agoI mis-read the title as meaning a preference for using classes to handle each case over ML-style switch/case statements. The JavaScript 'object' here is called "map" or "dictionary" etc. in other programming languages. (And the article's technique is fine).
- DaiPlusPlus 8y agoWhile you can use any value as an object property key in JavaScript, I’m not convinced the negative performance tradeoffs are worth the supposed benefits in the article. 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.
- beatgammit 8y agoThis is one nice thing about Go, which uses explicit fall through with break being there default. You can still fall through if you want. 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.
- Izkata 8y ago> While you can use any value as an object property key in JavaScript 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.
- androidgirl 8y agoI've never used this pattern in Javascript, but it's somewhat common in Python because of how handy the dictionary.get() method is. A few problems TFA solves in JS are much easier with .get(), like defaults and false values.
- UncleEntity 8y agoPlus the fact that python doesn't have a switch statement so you kind of have to use a dict if you want that functionality without a long chain of if/elif/else.
- fallingfrog 8y agoAh, but can objects be used to implement Duff's Device?
- pmontra 8y agoInteresting technique https://en.wikipedia.org/wiki/Duff%27s_device https://en.wikipedia.org/wiki/Duff%27s_device
- jerf 8y agoKinda-sorta: 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.
- tengbretson 8y agoAlternatively titled: "How to go to war with your linter, type checker and other static analysis tools and win"
- EpicEng 8y ago≥Object lookups are fast and they're faster as their size grow 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".
- greglindahl 8y agoA lot of C/C++/Fortran compilers have very well-developed handling for huge switch statements because of generated code. Interpreters and other finite state machines, etc etc.
- EpicEng 8y agoSure. I didn't want to go into too much depth on performance here as I'm a C and C<variant> guy primarily, only writing JS when I have to.
- bastawhiz 8y agoIf you're using an object, the object and any closures in it also need to be constructed on each execution. If the object is large, you're not just doing a lookup, you're constructing a full map object. 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.
- hombre_fatal 8y agoOf course, you could allocate once by extracting it or with an IIFE + closure from TFA's functions. lookup = (() => { map = { a: 1, b: 2 } return (key) => map[key] || 'not found' })()
- bastawhiz 8y ago
- tombert 8y agoI remember getting in arguments when I was doing F# over my use of Maps of functions to basically handle dynamic dispatch. I would construct a map like `let handlers = [| "somekey",func1; "otherKey", func2 |] |> Map.ofArray`, and then when I needed to handle something down the line, I would write something along the lines of: 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.
- ravenstine 8y agoI don't think any one pattern rules out any other, but this is a good one for people to keep in mind as I think sometimes it makes more sense. Heck, there are even some cases where an object lookup makes more sense than an if-then-else statement. I'm also a fan of other patterns like early returns, but few people ever seem to do it.
- smhenderson 8y agoI like early returns as well, especially in short functions where there is not a lot of code between returns. In other words if I can see all returns in a function on one screen of text then I'm OK with it and I think it improves readability. 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.
- yoz-y 8y agoFor mapping from one value to another, sure. My irk with this solution is that none of their "object" solutions actually does the same thing as the switch statement, namely logging the state in various manners to console. 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.
- dickeytk 8y agoI love this, but I feel TypeScript would be very unhappy with such a thing.
- y2bd 8y agoType it as `Record<string, R>` where `R` is your return type and you’d be fine in most cases. You could even make `R` a union type if necessary.
- alwaysdoit 8y agoBetween TSLint's no-switch-case-fall-through and an exhaustiveness check[1] I don't think any of these things are really problems with switch statements in TypeScript [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 {}
- bigtech 8y agoThe article provided some good examples of times when code readability can be increased without a bulky switch statement. However, there are times when I find the switch statement most closely communicates the idea of what needs to occur to some developer in the future.
- maxxxxx 8y agoIsn't this basically a convoluted way to end up with a dictionary or command pattern?
- bitwize 8y agoFun fact: There's an optimization technique for OO languages called polymorphic inline caching that boils down to... a switch statement that speeds up method dispatch by branching directly to a method implementation for one of a few common types. If the object is none of those types, it falls through to a more conventional method lookup.
- koboll 8y agoAnother compelling argument for the same position: https://toddmotto.com/deprecating-the-switch-statement-for-object-literals/ https://toddmotto.com/deprecating-the-switch-statement-for-o...
- rovolo 8y agoThe author's basic complaint with the Javascript switch statement is that it's essentially a structured series of 'goto' statements. They think that each 'case' should behave as an 'if-else' where each case has its own lexical scope, and the control flow doesn't leak from one case to another. They think that pattern matching is a better model for 'switch' in practice. 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://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
- ioddly 8y ago> How often do you use a 'switch' statement whose cases don't always end in 'return' or 'break'? I do, occasionally. I prefer the inverted golang switch where fallthrough needs to be specified, since these cases (heh) are generally the minority.
- xahrepap 8y agoI would like it better if switch statements used the keyword `continue` to fallthrough and `break` is implied. 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.
- ivyirwin 8y agoI agree. Their complaint on the use cases (pun!) for switch seem to ignore the flexibility of the syntax. I don't think the requirement to include breaks/returns is a problem if you know how it's intended to work. I prefer switches for the easy to follow flow but also the ability to "waterfall" multiple conditions, which is syntactically clunky with other approaches.
- ricardobeat 8y agoThis is my favorite from a library I wrote ~6 years ago: https://github.com/ricardobeat/require-tree/blob/master/index.js#L16-L22 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 }
- sklivvz1971 8y agoFirstly, switches are arguably way, way (waaaay!) more readable than the code proposed. It's a matter of taste for sure, but arguably a basic construct of all curly languages and more is easier to understand than a rather esoteric code where a pattern is substituted for a keyword. Of course though, 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. 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.
- rojobuffalo 8y agoswitch statements are way more readable than what is demonstrated in the article. It's not even close.
- gramstrong 8y agoI disagree :) It's easier for me to look at an object and its contents and see where it is referenced. I think switch statements are ugly. I think if/else blocks are ugly. I prefer boolean statements when possible, although definitely not nested ternaries. I'm sure I'll get flamed, but I really liked this: const getPosition = position => ({ first: 'first', second: 'second', third: 'third' }[position] || 'infinite');
- lhorie 8y agoI think equating ugly with hard-to-read is a common mistake. Many things are "ugly" but easy to read (e.g. in design circles, newbies often do far-too-small fonts with low contrast because it looks pretty, whereas accessibility guidelines tell you to do the opposite; in programming, readability of point-free style has more to do with the reader's ML-vs-Algol experience than how elegant FP may be, and educators often start w/ procedural style precisely because reading the flow of the program matches the students' expectations that things ought to be read top to bottom, rather than peeled through layers of abstractions) 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.
- ricardobeat 8y agoAs someone who has migrated between heavy use of these patterns in the past (object -> switch), I'd like to provide a few counterpoints. 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.
- kaizendad 8y agoThis is exactly correct. The cost of holding the entire object in memory, for any significantly complex statement, is going to add up quick if you're dealing with large numbers of users/pageviews/etc. The cost of pre-calculating everything in the object for any complex math similarly gets large when you're dealing with something large-scale. Early returns matter a lot with scale, and make [code line of sight](https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88 https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea...) much clearer -- which matters a lot of if your team scale is larger, as in many companies that might have multiple teams working on one shared codebase.
- oflannabhra 8y agoIn Swift, switch is my new favorite tool, combined with Swift's enums and amazingly strong native pattern matching. It has absolutely changed how I write code, and makes me wish it were a tool I could reach for whenever I'm working in another language. Some examples: http://austinzheng.com/2014/12/16/swift-pattern-matching-switch/ http://austinzheng.com/2014/12/16/swift-pattern-matching-swi...
- paultopia 8y agoCame here to say this. Many of the complaints about switch statements are about classic, kinda terrible, implementations of switch statements. Swift gets that right. Like, really super right.
- ken 8y agoI'm conflicted. Swift's switch is powerful, but it's also complex, and even after using it for years, I still have to occasionally look up the syntax for some variant. Other HLLs have other ways of accomplishing these tasks that work just as well. 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 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.
- makz 8y agoI don’t know, feels like a step back to me. It could be as well lookups on a hashmap. Not very expressive.
- tasty_freeze 8y agoI wrote an old computer emulator in javascript. The inner loop is a large switch statement where each case handles one of 256 opcodes. Firefox handled it fine, but chrome performance was poor. It turns out that above a 128-way switch, the jit gives up (or it seemed to). 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.
- yosser 8y agoIronically, that's precisely how a lot of old Z80 and 6502 programs managed conditional execution. We used to call it a 'jump table'.
- chaoticmass 8y agoI prefer if/elseif over switch statements. ok, ok, I'll go sit in the corner.
- chriswarbo 8y agoI like doing this, and find it fits reasonably well with a functional programming style: it is to `switch` what ternaries are to `if/then/else` (i.e. a nice way to select values; not necessarily nice for executing statements).
- jwr 8y agoThe discussion is missing the most important point, IMHO: readability and maintainability of the larger system in the long term. 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.
- yongjik 8y agoI don't know javascript, but in the proposed "structured" code, doesn't it execute initialization statements for every potential cases even when you call it only once? I.e., 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.
- dewaine 8y agoThis is some kind of funny joke article
- perfunctory 8y agoObjects vs switch is not either/or. They both have valid use cases.
- crazygringo 8y agoI mean, they're both useful for different purposes. 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.
- nameiscubanpete 8y agoSpeaking of code readability, the article uses a function in the example called "isNotOpenSource()". If you have a function that returns a boolean, it's best not to put another boolean in the function name i.e. "isOpenSource()" is cleaner and more readable. 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.
- aaaaaaaaaaab 8y agoI pity the devs inheriting his code.
- jpochtar 8y agoTake it one step further, and move the switched-over value to the top: ((o) => o[expr_to_switch_over()])({ opt1: () => { stmts(); }, opt2: () => { stmts(); }, opt3: () => { stmts(); } })
- ezekg 8y agoAnd then you can call it a “switch statement”!
- camgunz 8y agoPolymorphism vs pattern matching: fight