Civet
Civet copied to clipboard
Is there any way to fall through in a switch?
There are cases where falling through in switch statements keeps code DRY; for example, if there is some processing needed when a special condition is met and further processing when a more general condition is met. As a toy example,
description .= ''
switch n
2
description = 'prime'
// FALLTHROUGH
% 2 is 0
description = 'even ' + description
else
description = 'odd'
description += ' number'
As far as I can see/understand there isn't anything I can currently write in place of // FALLTHROUGH to have the desired effect that when n is 2, the description will be even prime number. I guess it's not 100% clear what JavaScript this would compile to, maybe
let description = ''
if (n === 2) {
description = 'prime'
// FALLTHROUGH
description = 'even ' + description}
else if (n % 2 === 0) {
description = 'even ' + description}
else {
description = 'odd'}
description += ' number'
where at least Civet is doing the code duplication for me so that I can specify the behavior in a DRY way. Is there any fallthrough mechanism that I missed, or possibly interest in adding one? I certainly agree that breaking should be the default and falling through would require some special notation if allowed at all.
There's not currently any way to fallthrough using Civet's pattern matching. You can fall through using the JS style switch and case clauses but they won't be able to mix and match with pattern matching.
Having some kind of fallthrough keyword and duplicating the block in pattern matching seems like a reasonable feature to add eventually.
For these kinds of things I often use a pattern like this if I have many checks and am trying to go super DRY:
checks := [
[(& is 2), "prime"]
[(& % 2 is 0), "even"]
]
checks.reduce ([v, descriptions], [check, description]) ->
[v, [...descriptions, ...(check(v) ? [description] : [])]]
, [n, []]
// then fill in else description if descriptions list is empty
... which has some tradeoffs of its own. But I think that the [(& % 2 is 0), "even"] predicate trick is pretty handy.
Inspired by continue label and #362 (which offers e.g. for as shorthand for the nearest for loop you're currently in), perhaps a reasonable notation for "fall through" would be continue switch, given that we want the opposite behavior of break? (I checked and switch is not a valid JS label, so this currently has no meaning.)
We could even do crazy things like continue switch in the middle of a case:
switch x
when 0, 2, 4
console.log 'even'
continue switch if x is 0
when 1
console.log 'odd or zero'
---
switch (x) {
case 0: case 2: case 4: {
if (!(() => {
console.log('even')
if (x === 0) return true
})) break
}
case 1: {
console.log('odd or zero')
}
}
I'm a little less clear on how to fall through with pattern matching, but we can presumably do it with additional conditionals in the if chain (and it will no longer be else if in all cases). Might be worth asking whether this kind of thing is worth doing, but I think it's doable. 🙂
I really like the syntax continue switch to go on to the next test. Very readable/intuitive as to the intended semantics.
As for a use case, I usually use fallthrough when there are two similar cases, one of which just needs a little preprocessing to put it in the form of another. Do I have a specific example with pattern matching in mind right now? No, but I am pretty sure I wanted it at one point, which is why I filed this proposal. Next time I wish I had it, I will try to post as real an example as I can here.
As for how to implement continue switch with generic conditions, seems like the path of least resistance would be to count up the number of conditions, first test each of the conditions to find the first true one, giving you a "case number", and then dispatch on the case number to the desired code. That is, just reduce to ordinary switch on an integer:
switch x
/pat1/
behavior 1
/pat2/
behavior 2
continue switch
else defaultBehavior()
would become
let freshCaseVariable = 0
if (typeof x === 'string' && /pat1/.test(x)) freshCaseVariable = 1
else if (typeof x === 'string' && /pat2/.test(x)) freshCaseVariable = 2
else freshCaseVariable = 3 // since the else behavior is always chosen if none of the patterns match
// if there was no else in the switch then there just wouldn't be a final else in this if...else if...else if... block.
switch (freshCaseVariable) {
case 1:
behavior(1)
break
case 2:
behavior(2)
// break suppressed because of continue switch
case 3:
defaultBehavior()
}
and of course you just generate the fancy code you showed above in the proper case number, in the situation that one of the civet cases has a "continue switch" in the middle of it. As far as I can see, there's barely any overhead in this object code over what you might write by hand.
I think continue switch would be a very nice addition to Civet!
I just noticed this issue exists.
Swift has continue <label> and break <label> for nested loops, but for switch statements it literally just has a dedicated keyword fallthrough. fallthrough is a little more explicit than continue switch, but I suppose at the expense of an extra keyword.
The main thing I don't like about repurposing continue is the control-flow implications. fallthrough is only allowed as the last statement in a branch; allowing continue switch followed by other statements could be confusing to navigate.
I searched my entire codebase (several thousand lines, mostly C++) for places that I'm intentionally falling through a switch statement, and I'm only doing it twice. Both times the branch it's falling into is only a single statement (plus break), so make of that what you will.