6 ms·
Using Dispatch Tables to Avoid Conditionals in JavaScript
- recuter 14y agoThis is an excellent tip and makes one wonder why it isn't more commonly seen in the wild.
- seanalltogether 14y agoI'm guessing its just not a common thing to have a large definition of single line if statements, I'm not sure about other programmers but I don't think I could find much of this in my code.
- MatthewPhillips 14y agoWhile this can be a useful construct when you are doing meta-programming, this particular example is horrible. It's not much more readable (would be less readable in an 80x24 terminal window), and now you have an extra object sitting in memory for the lifetime of your application. If you only have a handful of options, just use switch.
- mistercow 14y ago>and now you have an extra object sitting in memory for the lifetime of your application Worrying about that would be the epitome of premature optimization.
- rco8786 14y agoAlso, there is still a switch needed in the MovePlayer function. So all that extra code only serves to eliminate 2 options from the original switch. I do like the approach in some cases, I just think that the example given probably wasn't the best choice.
- eckzow 14y agoThe example definitely leaves a bit to be desired. One great story for why this way might be better is that with a small tweak (to allow dynamic [de]registration of command functions) it is much more open to extension. To continue using the somewhat creaky "text game" example, if possessing certain items or being in a particular room affects the available command set, this can be easily included: without a magic feather, "fly" returns "You don't know how to do that" but when you pick up said feather a new action is registered, and now "fly" works properly. Now, I'm not sure I'd implement this particular game that way, but such an interface tends to be useful for building plugin systems, etc. It's mainly for this reason that large switch statements, particularly those not doing "math" of some sort, tend to be a code smell.[1] [1] http://en.wikipedia.org/wiki/Open/closed_principle http://en.wikipedia.org/wiki/Open/closed_principle
- mgrouchy 14y agoInteresting. Ive seen something similar to this quite often used in Python because it doesn't have Switch statements.
- gvalkov 14y agoIndeed, it's quite idiomatic. I would have still preferred if there was a switch/case construct that would generate a dispatch table for us (in fact, this was proposed and rejected in pep 3103[1]). A case statement would have been a nice, little addition to the expressiveness of Python, but it's not that big of a deal. Here's another dispatch table technique that I really like[2]. [1]: www.python.org/dev/peps/pep-3103/ [2]: http://code.activestate.com/recipes/577864-fast-and-elegant-switchcase-like-dispatch/ http://code.activestate.com/recipes/577864-fast-and-elegant-...
- chime 14y agoI use something similar when rendering form fields dynamically. draw_checkbox = function() {} draw_radio = function() {} draw_select = function() {} draw_field = function(type) { this['draw_' + type](); } draw_list = function(list) { for(i = 0; i < list.length; i++) draw_field(list[i].type); } For sake of simplicity, I did not include the code to verify that type is proper and parameters sent to each function.
- nahname 14y agoThis is probably the part I dislike most about ruby (I know this is JS). There are some places where this trick makes a painful piece of code go away, so I appreciate why it was selected. The problem is that you now have no reference to draw_checkbox being used in your entire code base and when someone else is refactoring, they think they can remove it. Personally, I would never elect to go this route because it only introduces overhead for refactoring and code maintenance. I would rather see a mess of procedural code, than some silly clever little tricks that will trip up all the new people.
- taf2 14y agoyeah, i kinda agree.. in this case I'd have done something more like: function Renderer() {} Renderer.prototype = { draw: { checkbox: function() {}, radio: function() {}, select: function() {}, field: function(type) { this[type](); }, list: function(items) { return items.map(function(item) { return this.field(item.type) }); } } } Perhaps I'm just comment trolling though - totally depends on how the code would have been used...
- chime 14y agoActually my code ends up looking pretty much like yours. I use CoffeeScript and was writing a simplified JS equivalent off the top of my head since this post was about JS. Similar to your Renderer, I have a DrawForm class that iterates through DrawField class objects which provide the checkbox(), radio() etc. methods. My point was that in the end, you get to do dynamic function instantiation, which is pretty awesome in JS.
- SoapSeller 14y agoYou can further improve dispatch tables in js by using bind. so something like: north: function() { movePlayer("north"); }, becomes: north: movePlayer.bind(this, "north"), or using underscore.js(for greater browser compatibility): north: _.bind(movePlayer, this, "north"),
- taf2 14y agoor instead of a whole library use mozilla's compatibility when using bind... see the function here: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind https://developer.mozilla.org/en-US/docs/JavaScript/Referenc...
- k3n 14y agoBecause modifying object prototypes is the way to go?
- taf2 14y agoDepends if you're in an environment you control then yes - when embedding your scripts, don't do it. Create your own isolated functions and assume anything and everything can be broken e.g. Array.prototype is often modified with broken or semi incomplete functions like broken versions of indexOf
- jstanley 14y agoHow is this an improvement? It looks basically equivalent but more confusing to me.
- xhrpost 14y agoIt's just a way of getting rid of the redundant "function()" syntax. Unfortunately with bind(), we have to set the scope even though it isn't always necessary. So in this case, the first parameter "this" is also probably redundant. The old PrototypeJS curry() would have been the least redundant solution: movePlayer.curry("north")
- avolcano 14y agoHey, I used a modified version of this pattern without knowing what it was called a few months ago: https://github.com/thomasboyt/Noted-App/blob/master/app/assets/javascripts/views/note_view.js#L52 https://github.com/thomasboyt/Noted-App/blob/master/app/asse... Really useful when you have lots of keybindings.
- ldh 14y agoI think the article does a poor job of describing why the dispatch table version is better, or why you'd want to avoid conditions in the first place.
- johnnymonster 14y agoThis example is great to point out that javascript is a dynamic language and should be used as such whenever possible. obviously he only used a handful of options and maybe a switch would work better for this exact example, but it should get the reader thinking about where else this type of pattern could be used.
- obeattie 14y agoIs it just me, or for this example does the "dispatch table" seem way more complex (and likely less efficient) than the switch statement? The original is not difficult at all to interpret.
- taf2 14y agoIt depends, sometimes if you have a few different context e.g. multiple click handlers that are being bound in different contexts of your app, that a table makes a lot more sense then repeating the switch or even wrapping the switch in a function...
- bjourne 14y agoYes it is just you. :) The original becomes horrible when the number of cases grows, when you want a default and can introduce subtle bugs if you forget a break. In the second example, there is a clean separation between processing commands and dispatching on the input. For example, you might want to extend processUserInput with "invalid command" handling, commands with arguments or even add commandTable as an argument to it so that you can reuse it for all your dispatching needs. And to top it of, you can dynamically add new commands to the dispatch table which you can't to a switch.
- artificialidiot 14y agoI'd like to see you hunt for the dynamically added case to the dispatch table. Maybe then, you'll change your mind. There is no fundamental difference between either approaches except the runtime cost. If you need conditonals, use them. If you need a lookup table, use them. Keep the code predictable.
- nahname 14y agoThe dispatch table is shorter than the switch, so there is a lot less noise. The invocation is a little clever, but overall I would consider them about equally complex. Maybe you meant unconventional? The performance is substantially different. The dispatch table is ~75% slower (in chrome). http://jsperf.com/switchvsdispatchtable http://jsperf.com/switchvsdispatchtable One thing I really like about dispatch tables is that it forces the developer to only be able to handle the decision logic. The number of times I've had to cleanup a switch with 20-30 lines under each condition (sometimes including another switch) is too many to count.
- binarymax 14y agoFor more complicated scenarios (where you find yourself making trees of conditionals), I suggest reading up on finite state machines, and for js checkout machina: https://github.com/ifandelse/machina.js https://github.com/ifandelse/machina.js
- serbrech 14y agoI don't see the benefit. it really is just a disguised switch statement, unless you do something more dynamic with the mapping between the command name and the function to call...
- arianvanp 14y agoI've done this before in scenarios of packet handling. e.g. packets.incoming[16] = { login_size: {type: 'byte'}, magic_code: {type: 'byte'}, client_version: {type: 'short'}, high_detail: {type: 'byte'}, archive_crc: {type: ['int', 9]}, reported_size: {type: 'byte'}, block_opcode: {type: 'byte'}, client_key: {type: ['int', 2]}, server_key: {type: ['int', 2]}, uid: {type: 'int'}, username: {type: 'string'}, password: {type: 'string'} }; packets.outgoing[253] = packets.outgoingMessage = function (message) { var length = message.length + 1; return { opcode: {type:'byte', value: 253}, length: {type:'byte', value: length}, message: {type:'string', value: message} }; };
- vivin 14y agoI've done something similar in my form-validation library. I have a function that has three inputs that basically end up behaving like flags. So there are 8 possibilities in total. I ended up creating a function that would return a three-bit binary number based on the inputs, and I also created a jump-table that maps the three-bit binary values to specific handlers.
- davidkellis 14y agoI think the real win with a technique like this is that, if you store the bindings in a place that is globally accessible, it is open to extension. For example, if a JSON library author implemented an encode(object) function using a dispatch table holding a set of object-type/JSON-encoding-function pairs, a client could extend the encode function by registering additional object-type/JSON-encoding-function pairs in the dispatch table. Edit: Furthermore, I believe this is how clojure protocols work - when a new type participates in a protocol, the type-specific function implementation gets added to a dispatch table keyed on the datatype's java Class. Then, anytime you invoke the function, the class of the first argument is identified and the corresponding type-specific function is pulled out of the dispatch table based on the class.
- jcampbell1 14y agoYou have to be careful when doing this type of stuff. For instance, the switch case is actually doing both validation and switching, where as the dispatch table does no validation. Consider: processUserInput('toString'); That has very different behavior in the two different styles.
- robmcm 14y agoThe switch statement offers the ability to provide a default opperation, such as tell the user the command was not found. You could add an undefined property check on the dispatch table, but then you have as much and more complex code IMO.
- Xcelerate 14y ago"Dispatch table"? I'm surprised everything in programming has a name associated with it. I just called these Javascript objects representing a map of functions...
- Leszek 14y agoI don't know, in this case, with a bit of reformatting the switch can look pretty similar: function processUserInput(command) { switch (command) { case "north": movePlayer("north"); break; case "east": movePlayer("east"); break; case "south": movePlayer("south"); break; case "west": movePlayer("west"); break; case "look": describeLocation(); break; case "backpack": showBackpack(); break; } } Although that's to be expected, a switch on a string value isn't that much different to a string lookup in an associative array.
- Tactic 14y agoExcept with an array you can change things programmatically on the fly. Or have it setup in a configuration file or database table. Very useful.