Using ‘switch’ to match arbitrary logic instead of string or number literals
twitter.com
twitter.com
if(githubUrl) {
return config.github;
}
if(twitterUrl) {
return config.twitter;
}
return {
to: '',
...
};
IMO this is both clearer and easier on the eyes. Can anyone show me an example of this `switch` pattern that actually makes it better than the obvious if-based version?I mean, given that they're early returns, you don't need any `else` statements. And in situations where you couldn't use early returns, the `switch` pattern would require `break;` statements. That would make that code uglier to the same extent as adding `else`s above. What am I missing?
The goal is to define a value (a variable or return value) in which a value is always established and it's established through several checks (exclusive or not).
In this case the goal is to the definitively define a config given a URL.
The "switch" and a default condition which I think express better that a value must be defined. With "ifs" we can add more complex logic and avoid the problematic "breaks" but we loose that in your face "default" condition.
In theory it would be better to have a different control structure like "pickJustOne":
return pickJustOne(url) {
githubUrl: config.github;
twitterUrl: config.twitter;
default: config.unknown;
}
"pickJustOne" would have a companion named "pickOneByOrder" that would work like an "if" where the order of evaluation would matter.https://news.ycombinator.com/item?id=20736714
----
Inspired by this, I cooked up a crude imitation in (ugly/functional'ish?) Swift:
let result = [(firstCheck, 1),
(secondCheck, 2),
(thirdCheck, 3)]
.first { $0.0() }?.1
That iterates over an array of (() - > Bool, Value) tuples and returns the second element of the first tuple whose first element is a function that returns true, otherwise nil. The types of all values must be the same. Could shorten .first to .firstTrue or something.And a version that's closer to your idea:
typealias BooleanFunction = () -> Bool
typealias FunctionValuePair<ResultType> = (BooleanFunction, result: ResultType)
func pickOne<ResultType> (_ pairs: FunctionValuePair<ResultType>...) -> ResultType? {
pairs.first { function, result in
function() == true
}?.result
}
let result = pickOne(
(firstCheck, "one"),
(secondCheck, "two"),
(thirdCheck, "three"))
?? "default"
You could make the default result an argument for pickOne so that it never returns nil, but I prefer to keep it an optional.Though it's too early in my day to think of better names. :)
if (xx) {
} else if (yy) {
} else {
// maybe some exception for unhandled value
}
And to align my ifs: if (false) {
} else if (xx) {
} else if (yy) {
} else {
// maybe some exception for unhandled value
}
But then team members and tooling start to complain about dead code.This is all reasoning from the perspective of a compiled language of course. But even in Javascript implementing switch-statements as if-elseif-else seems like an anti-pattern to me.
>> Switch statements are a more like goto, and are more low level.
I know about 'goto considered harmful', but sometimes a simple jump table is just what you need, no need to come up with 'better' ways ;-)
Interestingly, this optimization is relatively new for LLVM:
http://llvm.1065342.n5.nabble.com/llvm-dev-RFC-JumpMaps-swit...
Yes, this is exactly one of the reasons to use switch statements instead of if-elseif-else, were possible.
Even if in the absence of side effects it is still easy to have overlapping conditions in the if-elseif-else, for example misspelling an enumeration value, some copy/paste mistake, etc. A switch statement does not allow this.
>> If you're checking some status code, I still prefer ifs
My philosophy is always to write code that expresses as much as possible its intent, and the sequence of operations the computer has to take. Performing some action based on a status code, in my mind, is like looking a procedure by its label, and executing it. Not looking at all procedures one-by-one until you find one that 'advertises' it applies to your status code. To each their own, I guess ;-)
if (x) {
else if (x) {
// warning
}
>> My philosophy is always to write code that expresses as much as possible its intent
agreed>>, and the sequence of operations the computer has to take. From a performance perspective yes, from a logic / maintanance perspective not really.
Interestingly, neither clang nor g++ give a warning for this case:
int main(int argc, char **argv)
{
int x = argc;
if (x == 0)
{
x = 10;
}
else if (x == 1)
{
x = 20;
}
else if (x == 0)
{
x = 30;
}
return 0;
}
# g++ bla.cpp -o out -Wall
(no warning) # clang bla.cpp -o out -Wall
(no warning)I assume this is because the compiler can never be 100% sure a variable cannot be changed midway through testing each of the conditions because of aliasing, so it won't throw a warning. In this particular example this obviously is not relevant, but I can imagine some pathological example where one of the tests has side effects that change the result of the later tests, and to avoid false positives the compiler simply never warns about unreachable code for these things.
In a sense, this is exactly what I mean by writing code that matches exactly what you want to happen. If I write 'switch (x)', it's pretty clear the test is executed exactly once. If I write a sequence of if-elseif-elseif-else statements, the conditions are evaluated in sequence, which is not what I want.
When I use it, I will extract it into a separate function, and just return the needed condition. That way you don't need to worry about case fall through.
function getCondition(param) {
switch (true) {
case ConditionOne(param): return 'condition one'
case ConditionTwo(param): return 'condition two'
case ConditionThree(param): return 'condition three'
}
}I mean, they're a recipe for disaster in C++, but I don't code much C++ anymore. In JS and similar-enough languages it's lovely.
I kind of see what he's talking about. It seems like it's a little easier to reason with the code and easier to set breakpoints sometimes.
All that's to say: I'm curious as to why early returns would be better in JS.
It seems to me like the exact opposite is true. Early return conditions stand out in your code and so the structure matches the flow. Its much clearer than having error state weaved through the mainline behavior of the function.
I'm pretty sure the pattern of having only one return is popular among older engineers because it makes it easier to use pre-condition/post-condition/invariant style reasoning which used to be popular. Outside of this scenario I see no reason to avoid early returns.
return config.github ?? config.twitter ?? defaultYou refactored the code and changed the logic without noticing.
return (twitterUrl && config.twitter) || (githubUrl && config.github) || default return twitterUrl ? config.twitter
: githubUrl ? config.github
: defaultcase isReadOnly: // change something;
case isAdmin: doAdminStuff();
case hasPaid: updateFinance();
case isActivated: updatedUserInfo();
and so on...
One thing I learned early on - true and false vary across platforms. Hence I learned to define true and false at the start of the code with realTrue=(1=1) and realFalse=(1=0). As some systems have 1 or any positive value as true, some have -1 as true. It can get messy, bit like endians (big or little) the same holds true for true and false.
I'm sure the platforms / language you use make this a sensible choice but I'm not going to lie, if I came across that in someones code I'd be screaming WTF at the top of my lungs.
But nice explanations here: https://stackoverflow.com/questions/17010041/why-define-true...
TL;DR portability
switch (githubURL, twitterURL) {
case (let .some(url), _): return config.github // I prefer GitHub and ignore Twitter
case (.none, let .some(url)): return config.twitter // Otherwise I'll see if I have Twitter
case (.none, .none): return {} // If I have neither I'll create something new
}
But the code looks like JS and I think it doesn't support switching on tuples?Bonus in Swift is that the compiler actually deduces that I handled all possible cases. No surprises here. Much better than if/then
But I don't think you should call any function outside of constructors with more than three-ish parameters to begin with, so probably you would get a dictionary or list of URL's by the time it grows out of control.
SocialUrl = 'github' // or twitter or w/e
return socialConfig[SocialUrl] || defaultSocialConfig
or how about: const getConfig = (key, default) => config[key] || default
Logic per key value isn't necessary.edit: disregard. Misunderstood the code =)
url match {
case GitHubUrl => config.github
case TwitterUrl => config.twitter
case _ => defaultUrl
} switch {
...
}Ruby has it explicitly (case, when, else), and of course functional languages with pattern matching operate like this as a matter of course. A Boolean guard followed by the code to run if the guard is matched.
You also would need a large number of case statements before a modern optimizing compiler will decide a switch statement is worth a jump table rather than conditionals.
Personally I would prefer somthing like this:
return (githubUrl)
? configGithub
: (twitterUrl)
? configTwitter
: configDefault;Every argument you make against using switch statements I would make against doing this as well.
When you consider the semantics of the statements, using switch statements for this makes more sense than chaining ternary statements. Chained ternary statements is just a less readable if/else chain. Personally, if one of my guys chained a ternary like this I would immediately fail the PR.
IMHO at least.
It also uses syntax that's different from regular C-style “I (command you) {to do stuff}”.
All that for very dubious benefit, as in I can't see any. I knew exactly one dude who insisted on writing this way, and he's also the only one I know who supported Putin, so I guess pick your choice.