DEV Community

Ben Halpern
Ben Halpern

Posted on

I've never become overly convinced that switch statements are that much cleaner than `if else if else if else if else`

I sort of know that switch statements are typically considered a good idea when chaining together a lot of else if statements. I'm not against this refactor in any big way, but I've also never found it overly important.

I tend to find the behavior and syntax of if and else to be pretty consistent regardless of language compared with switch statements so reaching for if is easier, but even when chained doesn't seem that much less clean than switches.

I definitely agree that getting rid of if..else..else chains are worth refactoring away, but I haven't found that a switch statement actually improves the quality meaningfully.

Thoughts?

Top comments (81)

Collapse
 
yaser profile image
Yaser Al-Najjar • Edited

That's why we never have switch in Python 😁

And, the dictionary is always a better and cleaner alternative for the ugly switch.

def my_switcher(x):
    return {
        'a': 1,
        'b': 2,
    }[x]


print(my_switcher('a'))
print(my_switcher('b'))
Enter fullscreen mode Exit fullscreen mode
Collapse
 
giovannicodes profile image
Giovanni Cortés • Edited

Elixir :)

def my_switcher("a"), do: 1
def my_switcher("b"), do: 2
def my_switcher(), do: 3

iex(1)> Example.my_switcher("a")
1
iex(2)> Example.my_switcher("b")
2
iex(3)> Example.my_switcher()
3
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kip13 profile image
kip

Pattern matching to the rescue ?

Thread Thread
 
cescquintero profile image
Francisco Quintero 🇨🇴

Isn't this feature called method overloading ? 🤔

Thread Thread
 
giovannicodes profile image
Giovanni Cortés

No, because overloading is have same parameters with different types, but in patter matching you can have the same type and number of parameters but differs in the content. For example, here!

Collapse
 
seanwash profile image
Sean Washington

I quite like cond as well for certain situations as well.

Collapse
 
renegadecoder94 profile image
Jeremy Grifski

Whenever I think about why I don't like a certain way a language does something, I realize it always goes back to my love for Python. That language has ruined me. haha

Collapse
 
yaser profile image
Yaser Al-Najjar

❤ for the utmost ruining lang.

Collapse
 
lysofdev profile image
Esteban Hernández

How would you handle a default case in this?

My though is to add || defaultValue at the end since a failure to match any property will return undefined. Thoughts?

Collapse
 
yaser profile image
Yaser Al-Najjar • Edited

That would be handy using a try except:

def my_switcher(x):
    switches = {
        'a': 1,
        'b': 2,
    }
    default = 3

    try:
        result = switches[x]
    except KeyError:
        result = default

    return result



print(my_switcher('a'))
print(my_switcher('b'))
print(my_switcher('c'))

I added the extra default variable just to make the code self explanatory.

EDIT: Bad solution, look at the other comments.

Thread Thread
 
rhymes profile image
rhymes

You can also use defaultdict in the standard library :D

Thread Thread
 
pedromendes96 profile image
Pedro Mendes • Edited

This is clean :D

Thread Thread
 
lysofdev profile image
Esteban Hernández

This is the answer to this entire thread.

Thread Thread
 
yaser profile image
Yaser Al-Najjar

@rhymes and @pedromendes96 got it better than me 😁

Collapse
 
kylegalbraith profile image
Kyle Galbraith

Really love this pattern. I have seen various approaches to the default case that vary across languages.

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

The main detractor with if statements is that you have to reduce your logic down to booleans, which can later be hard to trace through. But, switch statements do not go far enough to help the situation either. Reducing your logic down to integers or strings is not that much better of an abstraction. Both force you to reduce the expressiveness of your solution down to a primitive level.

This is a reason I am a big fan of Union types, which allow you to explicitly model your logically different cases along with their different related data. Combined with pattern matching, you can branch off those logical cases as-is. My favorite language which supports this is F#.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

Kasey, I have a question for you since I’m interested in F#. I use union types all of the time in TypeScript, but I often “unpack” (for a lack of a better word) the union by using an if statement. It’s really convenient since TypeScript has excellent control flow analysis and therefore discriminates the union so you know that it is only the one type.

So do you still prefer the functional approach for discriminating the union even in languages that can clarify the resulting type?

(I’m sorry if this question isn’t more clear. I suppose I’m suggesting that if statements are fine since I think the left/right monad thing very confusing)

Collapse
 
kspeakman profile image
Kasey Speakman

Typescript union types are a bit different from those in F#. There is a tradeoff made between them. Union declarations are more or less anonymous in Typescript, whereas F# requires you to create a Discriminated Union with explicit tags. So, the Typescript feels a little nicer on declaration. However, when you unpack the union type, I believe F# has the advantage. Using match you can get auto-completion of the cases and warning when all cases are not exhaustively matched. It also provides access to the value as the appropriate type.

Examples:

// TypeScript
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
}

// usage
let foo = padLeft("padded", 4);
// "    padded"
let bar = padLeft("padded", "bar ");
// "bar padded"
Enter fullscreen mode Exit fullscreen mode
// F#
type Padding =
    | Text of string
    | Spaces of int

let padLeft padding str =
    match padding with
    | Text prefix ->
        prefix + str
    | Spaces count ->
        String.replicate count " " + str

// usage
let foo = padLeft (Spaces 4) "padded"
// "    padded"
let bar = padLeft (Text "bar ") "padded"
// "bar padded"
Enter fullscreen mode Exit fullscreen mode

So, I feel like the latter is a better expression of intent in the long run. Even though it requires an extra type declaration, the type declaration adds some value for readability. It is more clear what the code is going to do with "string | number" when labeled as Text and Spaces.

For F#, I also swapped the order of arguments so that padLeft would be chainable with other string functions using pipe (|>).

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

What a wonderful response. Thank you for being generous with your time to write that thoughtful response. You should post that as it’s own article so more people can benefit from it. :)

Collapse
 
nepeckman profile image
nepeckman

For me its about using the semantics of the language to communicate as much information as possible to other developers (and my future self). Its the same reason I prefer map, filter, reduce over for loops. By using switch instead of else if, I communicate a more specific action is taking place: dispatch over a single value. It's a small thing, but its also easy to do and makes my code a little more compact, so why not?

Collapse
 
samuraiseoul profile image
Sophie The Lionhart • Edited

Switches as well as conditionals with many branches(else ifs) are generally indicative of a different issue all together.

Switches can be a good choice in the case of a clearly defined unchangeable list of things to check against, like days of the week(I pray to god there will only be seven in the future too). They are also sometimes good in the case that you actually have found a use case for the fall through effect you get when you don't use a break. Switches also are very hard to do in a way that enables defensive programming. It's easy for a switch to have 4 or 5 cases, then someone comes in and adds one, comes in and adds another, then another, until you have a giant mess of cases.

Generally, like @rossholloway94 mentioned, Enums are a much better fit. The way that @0xrumple showed in python can be done in many other languages that don't have real Enums, but is essentially an Enum as well. Alternatively, in certain use cases, something like a rule matcher and handler can be a better fit.

class Foo implements RuleHandler {
    public function canHandle(condition) : bool {
        if(condition === something) { return true; }
   }

   public function handle(thing) : Bar {
        return thing.thing() + thing.otherThing();
   }
}
Enter fullscreen mode Exit fullscreen mode

Using a switch can also be indicative that whatever logic you're doing in the switch, is really something that should be on each class to implement which can be similar to the model above. Basically the Strategy design pattern.

//instead of this
function doThingsThing(thing: Thing) : void {
  switch(thing.val) {
    case 1 { thing.a + thing.b; break; }
    case 2 {thing.a * thing.b; break; }
    // more cases
    default { //this should never happen }
    // NEVER DO THIS TO A DEFAULT IF IT SHOULDN'T HAPPEN THROW AN ERROR
  }
}

//this
function doThingsThing(thing : Thing) {
  thing.doThing();
}

abstract class Thing { abstract function doThing() : Thing; }
class Thingy extends Thing { function doThing() : Thing {this.meh = 2; return this; } }
class OtherThingy extends Thing { function doThing() : Thing { this.meh = this.meh * 2; return this; } } 
Enter fullscreen mode Exit fullscreen mode

The reason a switch never feels better than many else ifs is because they are basically the exact same thing. The kind of feel that gets hammered in a lot of beginner tutorials that explain switches is something like "If you have a lot of conditions, use a switch!" and also the examples show how to convert a thing with a lot of else if statements into a switch.

One more time for clarity:

  • Consider using an if in the case that:
    • There are a TRULY finite number of cases
    • The fall through logic will really help(though there are OOP ways of handling this too that I didn't show)
  • Some switch alternative candidates are:
    • Enums
    • Rule Handler matchers
    • Interfaces in classes
Collapse
 
yaser profile image
Yaser Al-Najjar

Enums are also good, but they might not fit in cases where there is no ordinal values.

Don't get offended, but the rule handler matcher / OOP ways to return a value reminds me of this meme:

deep-learning

Collapse
 
samuraiseoul profile image
Sophie The Lionhart • Edited

Haha no offense at all. The rule handler is overkill in A LOT if not most scenarios. That said, it's for sure one of the patterns you would use instead of a switch so I included it. For the trivial example I have though, yeah way overkill. For the slackbot I made a while back I used it and it worked well. github.com/samuraiseoul/starbot/bl... and github.com/samuraiseoul/starbot/bl... are an example where it works well.

Thread Thread
 
yaser profile image
Yaser Al-Najjar

Aha... Heck yeah, this example is pretty solid 👌

Collapse
 
eerk profile image
eerk • Edited

Excellent point! Sometimes you may need lots of if statements, but more often than not, lots of ifs are a sign that your code is just too procedural. By abstracting away code in functions or classes you can often get rid of them.

Collapse
 
sergix profile image
Peyton McGinnis

Having the fallthrough can be nice for certain cases when you want to keep your code DRY or want to avoid a ton of || operators in your if statement.

The switch operator can be much cleaner than if though, notably for small return statements. For example:

switch (x) {
  case 'a': return 1;
  case 'b': return 2;
}
Enter fullscreen mode Exit fullscreen mode

is much cleaner than

if (x === 'a') {
  return 1;
} else if (x === 'b') {
  return 2;
}
Enter fullscreen mode Exit fullscreen mode

Of course, it does come down to personal taste, but since languages provide the operator in the first place there's obviously a reason for it.

Collapse
 
michaelbrooks profile image
Michael Brooks

If you're returning then you don't even need the else.

if (x === 'a') {
  return 1;
}

if (x === 'b') {
  return 2;
}
Enter fullscreen mode Exit fullscreen mode

Actually looks a bit tidier to me and still very explicit.

Collapse
 
andrewharpin profile image
Andrew Harpin

Depends on the language, but for safety critical code, multiple function exits are typically frowned upon.

Thread Thread
 
cheetah100 profile image
Peter Harrison

Nah. Exit early. Makes the code cleaner and clearer.

Thread Thread
 
scott_yeatts profile image
Scott Yeatts

Gotta respectfully throw out a dispute. I've heard this statement used to justify multiple returns before... Looks like

if (x) {
    return a + x
} else if (y) {
    return a + y
} else {
    return a
}
Enter fullscreen mode Exit fullscreen mode

With the justification that it avoids evaluating the additional conditionals if the first proves true.

I just don't think the performance loss is there barring some REALLY funky conditionals or some other code smell...

This is a rule that feels like it came from the old C days and just stuck around past it's expiration date, because that same logic, but expressed as

if (x) {
    a = a + x;
} else if (y) {
    a = a + y;
}

return a;
Enter fullscreen mode Exit fullscreen mode

Is so much more readable and you can look at one line to know what's going to be returned, then you only have to debug/watch one thing, and it's just SO much cleaner... If I run into a problem with evaluating conditionals to get syntax that clean, then I feel like something else is wrong in the code... It's also more bug resistant and ensures that the method is doing ONE thing. Less chance that someone can come along and decide to start returning Strings instead of ints in one of the return statements or something wacky like that...

Thread Thread
 
cheetah100 profile image
Peter Harrison

In the real situations I encounter the issue is that having early returns means being able to avoid huge indents. When reading code where the return is at the bottom of a long complex structure you are forced to parse the entire function to ensure there is no subsequent operation which influences the return value. Having one return value generally means having the return variable declaration at the highest level, and so it could potentially be modified through the function. You might intend for it not to be modified once set, but a later modification might overwrite it. A early return is syntactically a succinct expression of the intent. We are finished. Return this value now. I know that in the early days of programming there was a good reason not to return early, especially in the middle of a loop. Don't think there is any issue at all with this any more.

Thread Thread
 
scott_yeatts profile image
Scott Yeatts

I could easily see if I'm dealing with a returnable statement over 20 lines where the return value is based on multiple conditionals or is impossible to keep in your head all at once, that return early and often would be a good backstop to make sure you know what is returned and when... And I HAVE run into code like this in the wild that I couldn't refactor entirely for stability, but I could make sure it was readable, and that did involve applying Exit Early.

With new code or things I could refactor, I would have to go with Uncle Bob's rule from Clean Code

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. Functions should not be 100 lines long. Functions should hardly ever be 20 lines long.

If you're dealing with multiple nested conditionals or the triangle of death where nesting is making the indents move out and out until your code looks like an arrow pointing off to the right, then the function is probably just too big and needs to be broken up for maintainability...

But now we're going to get so far off the topic we'll need to write a whole new topic to keep this up hahaha!

Thread Thread
 
cheetah100 profile image
Peter Harrison

I broadly agree with the idea that functions should be small, but care should be taken not to break up logically sequential operations that really belong together just to comply with this principle.

More important to me is that a function should have one clearly defined purpose and not mash together a bunch of logically separate operations.

Switches tend to be used for doing multiple separate things based on some value. It is not always bad, but I always evaluate whether there is a better more extensible way to do it than a switch when I see it.

I'm not saying I've never used a switch, but it is generally not one I use unless there is a really good reason.

Collapse
 
sergix profile image
Peyton McGinnis

Ah, true. I'm an idiot.

But for me, I still prefer the inline look of the switch statement. Again it's up to preference though.

Collapse
 
cubiclebuddha profile image
Cubicle Buddha

I don’t think fallthrough in a switch case is ever appropriate due to the potential for buggy side effects. I describe that in this article and I’d love to hear your thoughts on if you agree: dev.to/cubiclebuddha/is-defensive-...

Collapse
 
sergix profile image
Peyton McGinnis

I absolutely agree with your point. default/else cases should be used for error handling in most instances.

However, I should have clarified, so the fault is on my behalf, but when I said "switch 'fallthrough'", I meant when you have a situation like the following:

switch (x) {
  case 'a':
  case 'b':
    return 1;
  default: throw(); break;
}
Enter fullscreen mode Exit fullscreen mode

Here, it's easier (for me, at least) to see which possible values return 1 than this:

if (x == 'a' || x == 'b') {
  return 1;
} else {
  throw();
}
Enter fullscreen mode Exit fullscreen mode

I'm probably missing another thing you can do with if statements to make it more clear. But, after all, this is #healthydebate... So tell me how I'm wrong! :P

Thread Thread
 
cubiclebuddha profile image
Cubicle Buddha

Those examples your provided look great to me. I just don’t like when code assumes that there will never be a new case. I explain that a little better in my article. But it looks like you’re covering the known cases. So great work! :)

Collapse
 
taillogs profile image
Ryland G

In terms of performance, it never practically matters. For sake of the argument, switch is much faster if you have a large number of cases and slightly slower with small number of cases. The compiler is aware of the number of cases and will convert the switch statement into a constant lookup time hash table once it exceeds a threshold. But once again, for 99.999% of code, the difference between the two will never be meaningful.

I personally think that they are used differently, and that using them both is actually the best option. If/Else can be used very broadly, but with switch it's pretty niche. It's essentially a jump table that's useful when you have a ton of outcomes given some input. When I see a switch, I immediately know the class of problem it's trying to solve. With if/else, it could be much broader.

Collapse
 
andrewharpin profile image
Andrew Harpin

A good optimiser will use the optimal output for the size of the operation, irrelevant of the code statement.

Collapse
 
taillogs profile image
Ryland G

I'm sorry but this is a really uneducated comment.

Collapse
 
renegadecoder94 profile image
Jeremy Grifski • Edited

Personally, I oppose switch statements because they force you to opt-in to break rather than breaking by default (i.e. use continue for fallthrough). I feel like every time I write a switch statement I forget a break on one of the cases. Then, I'm stuck exposing that fallthrough case during testing. In contrast, you don't really get that same ambiguity with if/else if/else.

Collapse
 
val_baca profile image
Valentin Baca

Swift's switch is the only one that I think gets it right. It provides enough simplicity and usability to warrant use over if-else chains.

From docs.swift.org/swift-book/Language...

  • compund cases case "a", "A":
  • interval matching case 1..<5:
  • tuples switch point: case (0, 0)
  • value binding case let(x, y)
  • where checks
  • explicit fallthrough

Granted, each of these can still be done with if-else, I like the convenience of it all.

Collapse
 
jeikabu profile image
jeikabu • Edited

This here. switch in some languages (like F#, rust, and apparently swift- amongst others) has additional "powers".

In C# 8 you can also use it as an expression:

return x switch {
  0: something,
  // More
  _: whatever,
};

Otherwise it's main advantage is clarity for large numbers of options and optional fall through (double-edged sword that it is).

if (x == 0) {
  return something;
} else if (x == 1) {
// 10 more cases
else {
  return whatever;
}

// vs
switch (x) {
  case 0: return something;
  // 10 more
  default: return whatever;
}

Regardless what you do with your brackets/whitespace, the switch logic is clearly only about the value of x.

Also, historically compilers were more likely to turn it into a jump table rather than chain of branches.

Collapse
 
theringleman profile image
Sam Ringleman • Edited

Considering it rapidly speeds up your conditional statements, I will always use a switch statement. A switch statement acts as though you are accessing an array via an index. Whereas an elseif statement has to process each conditional statement until it passes.

Collapse
 
andrewharpin profile image
Andrew Harpin

Depends on if scripted Vs compiled.

A compiler will optimise into either the conditions or a jump table depending on the code. Usually for both switch and if else

Collapse
 
theringleman profile image
Sam Ringleman

As someone who has never had the chance to jump into a compiled language, this is wonderful information! Thank you for the correction.

Collapse
 
theringleman profile image
Sam Ringleman

Of course this depends on the context of the application though. If I am checking against static values, then my previous statement is true.

Collapse
 
alanmbarr profile image
Alan Barr

I much prefer pattern matching like in F# to switch statements. Eevee did an interesting blog on the history of the switch statement. I dislike the fallthrough behavior and find it visually displeasing in a code base.

Collapse
 
renegadecoder94 profile image
Jeremy Grifski

Thanks for the article! That was a quality read. Gave me a more clear explanation of why I don’t really like switch statements. Haha

Collapse
 
rossholloway94 profile image
Ross Holloway

I feel that with Java, if you have a switch statement, each condition can probably be modelled by an enumeration. And if that's the case, that can be refactored so that each enum type has a common abstract method to be called.

Collapse
 
aromig profile image
Adam Romig 🇵🇭

For me it depends on the situation really. If I have a definite limited set of values I'm looking for, I may use a switch. To me, it can be easier to read the values down a column with it. If I'm looking for boolean conditions, the if will probably be the best. But just depends.

Collapse
 
andrewbrown profile image
Andrew Brown 🇨🇦

In ruby case statements feel less verbose, less prone to error since the intention is to validate a single statement against multiple outcomes and is more human readable.

With linting case statements are one way to still have compact code.

Everything has its utility.

if    status == :active     { '/active'}
elsif status == :incomplete { '/incomplete'}
elsif status == :suspended  { '/suspended'}
end

case status
when :active     then '/active'
when :incomplete then '/incomplete'
when :suspended  then '/suspended'
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nicpolhamus profile image
Nicolas Polhamus

When I'm adding some new functionality to something the main thing I'm concerned with us maintainability, and a big part of that is code readability. If statements are easier to document, at least in my opinion, and they do a better job at being representative of their purpose and the current context of the code.
Switch blocks can certainly be contextual and representative, but it takes more effort to ensure that the context and logic are properly represented. I will say, that fallthrough can be very useful in the right scenarios.

Collapse
 
gklijs profile image
Gerard Klijs

Since Java 8 I don't use switch case anymore for Java. I find it much more readable to create a functional interface, and add it to the enum. For example if you have an enum with different types of messages, and you want to correct topic for that type depending on some TopicConfiguration class. You can add a lambda config -> config.emailTopic to all the enums. Reducing it to just a single function call on the enum when needed.
In Rust there not even is enum, there is match which is more powerful. And you can use traits to get the same kind of construction as described for Java.