Recently (or not so recently, depending on when you read this article), I was having a debate with some teammates about how to handle conditions that require multiple evaluations, usually for such cases people love to use a switch statement or a huge if
with multiple else if
conditions. In this article I'm going to focus on a third way (the approach I prefer), we're going to make use of objects for quick lookups.
The switch statement
The switch statement allow us to evaluate an expression and do something especific depending on the value of the expression passed, usually when you learn to write code and algorithms you learn that you can use it specially for multiple evaluations, you start using it, it looks good and you quickly realized that it gives you a lot of freedom, yay!, but be careful, great freedom comes with great responsability.
Let's quickly see how a typical switch statement looks like:
switch (expression) {
case x: {
/* Your code here */
break;
}
case y: {
/* Your code here */
break;
}
default: {
/* Your code here */
}
}
Excellent, Now there are a couple of things you may not know you need to pay attention to:
The break keyword is optional.
The break keyword allows us to stop the execution of blocks when a condition is already met. If you don't add the break
keyword to your switch statement it wont throw an error. Having a break
keyword missing by accident could mean executing code that you don't even know is being executed, this also adds inconsistency to our implementations, mutations, memory leaks and complexity layers when debugging problems. Let's see a representation of this problem:
switch ('first') {
case 'first': {
console.log('first case');
}
case 'second': {
console.log('second case');
}
case 'third': {
console.log('third case');
break;
}
default: {
console.log('infinite');
}
}
If you execute this piece of code in your console you'll see that the output is
firt case
second case
third case
The switch statement executes the block inside the second and third case even though the first case was already the correct one, it then finds the break
keyword in the third case block and stops the execution, no warnings or errors in the console to let you know about it, this is the desired behavior.
The curly brackets on each case are NOT mandatory.
Curly brackets represent blocks of code in javascript, since ECMAscript 2015 we can declare blockscoped variables with the use of keyworkds like const
or let
which is great (but not so great for switch cases), since curly brackets are not mandatory we could get errors because of duplication of variables, let's see what happens when we execute the code below:
switch ('second') {
case 'first':
let position = 'first';
console.log(position);
break;
case 'second':
let position = 'second';
console.log(position);
break;
default:
console.log('infinite');
}
we would get:
Uncaught SyntaxError: Identifier 'position' has already been declared
This returns an error because the variable position
has already been declared in the first case and since it does not have curly brackets it's being
hoisted, then by the moment the second case tries to declare it, it already exists and BOOM.
Now imagine the things that could happen when using the switch statements with inconsistent break
keywords and curly brackets:
switch ('first') {
case 'first':
let position = 'first';
console.log(position);
case 'second':
console.log(`second has access to ${position}`);
position = 'second';
console.log(position);
default:
console.log('infinite');
}
This will console log the following:
first
second has access to first
second
infinite
Only imagine, the amount of errors and mutations that could be introduced because of this, the possibilities are endless... Anyway, enough of switch statements, we came here to talk about a different approach, we came here to talk about objects.
Objects for safer lookups
Object lookups are fast and they're faster as their size grow, also they allow us to represent data as key value pairs which is excelent for conditional executions.
Working with strings
let's start with something simple like the switch examples, let's suppose we need to save and return a string conditionally, using objects we could do:
const getPosition = position => {
const positions = {
first: 'first',
second: 'second',
third: 'third',
default: 'infinite'
};
return positions[position] || positions.default;
};
const position = getPosition('first'); // Returns 'first'
const otherValue = getPosition('fourth'); // Returns 'infinite'
This would do the same job, if you want to compact this implementation even more, we could take even more advantage of arrow functions:
const getPosition = position =>
({
first: 'first',
second: 'second',
third: 'third'
}[position] || 'infinite');
const positionValue = getPosition('first'); // Returns 'first'
const otherValue = getPosition('fourth'); // Returns 'infinite'
This does the exact same thing as the previous implementation, we have achieved a more compact solution in less lines of code.
Let's be a little more realistic now, not all the conditions we write will return simple strings, many of them will return booleans, execute functions and more.
Working with booleans
I like to create my functions in a way that they return consistent types of values, but, since javascript is a dynamically typed language there could be cases in which a function may return dynamic types so I'll take this into account for this example and I'll make a function that returns a boolean, undefined or a string if the key is not found.
const isNotOpenSource = language =>
({
vscode: false,
sublimetext: true,
neovim: false,
fakeEditor: undefined
}[language] || 'unknown');
const sublimeState = isNotOpenSource('sublimetext'); // Returns true
Looks great, right?, but wait, seems like we have a problem... what would happen if we call the function with the argument 'vscode'
or fakeEditor
instead?, mmm, let's see:
- It'll look for the key in the object.
- It'll see that the value of the vscode key is
false
. - It'll try to return
false
but sincefalse || 'unknown'
isunknown
we will end up returning an incorrect value.
We'll have the same problem for the key fakeEditor
.
Oh no, ok, don't panic, let's work this out:
const isNotOpenSource = editor => {
const editors = {
vscode: false,
sublimetext: true,
neovim: false,
fakeEditor: undefined,
default: 'unknown'
};
return editor in editors ? editors[editor] : editors.default;
};
const codeState = isNotOpenSource('vscode'); // Returns false
const fakeEditorState = isNotOpenSource('fakeEditor'); // Returns undefined
const sublimeState = isNotOpenSource('sublimetext'); // Returns true
const webstormState = isNotOpenSource('webstorm'); // Returns 'unknown'
And this solves the issue, but... I want you to ask yourself one thing: was this really the problem here? I think we should be more worried about why we needed a function that returns a boolean
, undefined
or a string
in the first place, that's some serious inconsistency right there, anyway, this is just a possible solution for a very edgy case.
Working with functions
Let's continue with functions, often we find ourselves in a position where we need to execute a function depending on arguments, let's suppose we need to parse some input values depending on the type of the input, if the parser is not registered we just return the value:
const getParsedInputValue = type => {
const emailParser = email => `email, ${email}`;
const passwordParser = password => `password, ${password}`;
const birthdateParser = date => `date , ${date}`;
const parsers = {
email: emailParser,
password: passwordParser,
birthdate: birthdateParser,
default: value => value
};
return parsers[type] || parsers.default;
};
// We select the parser with the type and then passed the dynamic value to parse
const parsedEmail = getParsedInputValue('email')('myemail@gmail.com'); // Returns email, myemail@gmail.com
const parsedName = getParsedInputValue('name')('Enmanuel'); // Returns 'Enmanuel'
If we had a similar function that returns another functions but without parameters this time, we could improve the code to directly return when the first function is called, something like:
const getValue = type => {
const email = () => 'myemail@gmail.com';
const password = () => '12345';
const parsers = {
email,
password,
default: () => 'default'
};
return (parsers[type] || parsers.default)(); // we immediately invoke the function here
};
const emailValue = getValue('email'); // Returns myemail@gmail.com
const passwordValue = getValue('name'); // Returns default
Common Code Blocks
Switch statements allows us to define common blocks of code for multiple conditions.
switch (editor) {
case 'atom':
case 'sublime':
case 'vscode':
return 'It is a code editor';
break;
case 'webstorm':
case 'pycharm':
return 'It is an IDE';
break;
default:
return 'unknown';
}
How would we approach this using objects?, we could do it in the next way:
const getEditorType = type => {
const itsCodeEditor = () => 'It is a code editor';
const itsIDE = () => 'It is an IDE';
const editors = {
atom: itsCodeEditor,
sublime: itsCodeEditor,
vscode: itsCodeEditor,
webstorm: itsIDE,
pycharm: itsIDE,
default: () => 'unknown'
};
return (editors[type] || editors.default)();
};
const vscodeType = getEditorType('vscode'); // Returns 'It is a code editor'
And now we have an approach that:
- Is more structured.
- Scales better.
- Is easier to maintain.
- Is easier to test.
- Is safer, has less side effects and risks.
Things to take into consideration
As expected all approaches have their downfalls and this one is not exception to the rule.
Since we're using objects we will be taking some temporal space in memory to store them, this space will be freed thanks to the garbage collector when the scope in which the object was defined is no longer accesible.
Objects approach could be less fast than switch statements when there are not many cases to evaluate, this could happen because we're creating a data structure and later accesing a key where in the switch we're just checking values and returning.
Conclusion
This article does not intend to change your coding style or make you stop using switch statements, it just tries to raise awareness so it can be used correctly and also open your mind to explore new alternatives, in this case I have shared the approach I like to use but there are more, for example, you may wanna take a look to a ES6 proposal called pattern matching, if you don't like it you can keep exploring.
OK devs of the future, that was it, I hope you enjoyed the article, if you did, you will probably like this article about factory pattern as well. Also, don't forget to share it and suscribe, you can find me on twitter or contact me through my email duranenmanuel@gmail.com, see you in the next one.
Read the original article posted at EnmaScript.com
Top comments (15)
good article, ive been doing similar recently but i used es6 Maps and dynamic keys
i use
_
as a default value and a little helper likeNice.
On the down side, you have to be careful that no more than one condition is verified.
On the plus side,
'_'
looks like a confused face 😐😀Anyway, you can do that with objects, too, but booleans would get converted into strings.
haha just setup a quick example :)
Is there a specific benefit of this functional approach? Seems to accomplish the same thing but it took me longer to work out what it was doing.
I though the same: Clever but without practical use.
Reading the code fluently comes here to an unnecessary halt.
map.has(true) ? map.get(true) : map.get('_')
Reading this in a codebase makes me go WTF?!.it depends on the codebase, again it was a quick example to showcase another alternative way :)
I tend to disagree. It mainly depends on your target audience.
Of course you could have a codebase where you enforce functional paradigms - which in itself might be not the worst idea. But in the context of Javascript this code above looks like shenanigans, stuffing patternmatching down the reader's throat. It is cool, that you found a way making your beloved pattern available in Javascript, but as I said above it disturbs the reading flow, because it makes you spent extra time to decipher what you intended.
A different example is the use of
map
andfilter
, which is native to the language: Although it might take some time for the untrained reader which was socialized with his for-loop to understand; but anybody fluent with the language does immediatly know what was intended.For me it is a real time saver having code which is easy to read, easy to understand and therefore easy to debug.
I know it is fulfilling beating the language and making things happen, but the downside is oftentimes beating the language is beating the reader.
No offense. It might sound a bit harsh, but read it written with a mild undertone ;)
Sure I tend to agree if folks are familiar with functional paradigm and as you mention pattern matching etc then it’s a little easier and sure I probably could have kept the cata out where it looks in the Map and pulls out either when the condition is true or the default value.
I took no offense. Everyone is entitled to their own opinions.
I just got excited when I saw the post to share an alternative, which may not fit your interests or needs etc but I decided to share anyway and would do it again :)
Just a quick example of an alternative to the OPs object hash its similar to clojuredocs.org/clojure.core/cond or if you prefer JS ramdajs.com/docs/#cond
I agree that when a switch statement is used to perform a mapping (which is not always the
case
), then it’s usually better to replace it with an actual mapping. But we should also keep readability in mind. Your last switch statement immediately shows what it’s doing. The object approach is less immediate.(Also, pattern matching is not an ES6 proposal, it’s an ancestral FP principle that’s proposed for addition to ES6.)
My favorite is that if the object is in the outer scope, you can statically link to a value instead of having a function call with a hardcoded string argument.
In many cases this enables making definitions not recursive, like the example in my post:
Another GOTO to avoid
Mihail Malostanidis
I just wish other people were more accepting of this approach.
That's not always the case :v
I am glad you mentioned the pattern matching proposal, in my opinion the closest implementation of it in current JS is actually the early return pattern:
It looks imperative at a glance, but with just a little discipline it serves the role very well.
I tend to do this in JS as well, but it was more of a habit I carried from Python, which does not have
switch..case
and the common way to replicate that behavior is to use objects (dicts).I would put the cases in a closure instead of the function itself to save some memory (granted it's a tiny bit).
It's kind of interesting that the way you use objects here allow you to write code similar to how switch statements work in some languages.
As an added bonus, writing it like this may let you implement a different feature present in some languages, that checks if a switch has covered all possible inputs (usually used with enums). This could prevent stuff like, having one entry misspelled, or somebody adding something somewhere, but not updating all the linked "switch"es.
Though depending on the implementation, this may be even more work or cause more trouble.
I tend to use this method for very predictable objects that otherwise would have been unnecessarily long switch statements. Good stuff
I agree with this. Switch statements have a place but more and more my team has been implementing this kind of strategy pattern instead.