I was wondering about Elm developers’ opinions on the merits of using if-then-else as opposed to a case expression.
Over the past few months I’ve stopped writing if-then-else expressions and just used case expressions, the reasons being:
People seem a bit undecided on the best way to format an if-then-else, but there is a clear convention for case expressions.
You can re-order the branches of the condition, I often suggest putting the “easy” case first. With an if expression to do this you (may) have to negate the condition, but with a case expression you can of course order at will.
Finally, often sometimes the boolean is actually better expressed as a custom type, and I feel that using a case expression in the first instance lessens any friction to making such a change. This is obviously something of a vague point.
The only advantages I can think of for using an if-then-else expression are:
Might be a bit more familiar for someone coming from another language who is reading my code
Occassionally you can in-line an if-then-else for a bit of brevity
I suppose ultimately one might even consider removing the if-then-else construct from the language altogether, aside from the advantages listed above this would also mean:
The language is slightly simplified as there is only one way to make conditional expressions
Minor: it would free up the keywords either for variable names or for some future syntax.
Obvious disadvantages would be:
It would break a lot of existing code, though automatic translation would be relatively straightforward (I think there are probably some places in the grammer where you require parentheses around a case expression that you do not for an if expression but I’m not sure.)
It could be argued that if-then-else is easier for beginners. Personally I think either they are new to programming in general and hence giving them one fewer construct to learn is a bonus, or they are just new to Elm, in which case stating that if-then-else is written as case is probably not a major hurdle. Though arguably if-then-else is easier to teach, and that in turn makes case expressions easier to teach as you have an example for which you can show an equivalent and simpler expression.
It might make inconvenient some simple expressions particularly in a record expression such as:
{ id = old.id
, name = if String.isEmpty new.name then old.name else new.name
, ...
}
Thoughts on if-then-else? Am I missing good reasons to be using them? I haven’t used them in a while and I don’t seem to be missing them.
Here are the else-ifs from a Minesweeper game I built. Not sure how to do them with case-of.
movement =
if onlyOneModifier then
if ctrlKey then
EdgeMovement
else if shiftKey then
SkipBlanksMovement
else
FixedMovement (4 + jumpHack)
else
FixedMovement 1
"Tab" ->
if List.all not modifiers then
( model, View.focusControls Forward )
else if onlyOneModifier && shiftKey then
( model, View.focusControls Backward )
else
( model, Cmd.none )
gameState : Bool -> Grid -> GameState
gameState givenUp grid =
if givenUp then
GivenUpGame
else if isGridNew grid then
NewGame
else if isGridWon grid then
WonGame
else if isGridLost grid then
LostGame
else
OngoingGame
Just (Cell cellState Mine) ->
if cellState == Revealed then
detonatedMine
else if gameState == WonGame then
autoFlaggedMine
else if
(gameState == LostGame)
|| (gameState == GivenUpGame)
|| debug
then
mine
else
secret
Hi, thanks this is the kind of uses I don’t really have and hence haven’t really seen as a use-case for if-then-else.
This is a good example where I personally would get the easy case out of the way first, so that the reader has “less on their stack as they read”. So translating it into a case allows this without negating the condition:
movement =
case onlyOneModifier of
False ->
FixedMovement 1
True ->
case ctrlKey of
True ->
EdgeMovement
False ->
case shiftKey of
True ->
SkipsBlanksMovement
False ->
FixedMovement (4 + jumpHack)
I personally prefer the case version, but I can see why some people would prefer to avoid the ever-increasing indentation. The other examples are the same, in that the ‘use’ of if-then-else is to avoid the ever-increasing indentation, which is great if you don’t like that.
I’ve found that I rarely write boolean expressions in my Elm code anymore due to the powerful modeling capabilities of custom types. This means I’m almost always using case instead of if-then-else.
Some interesting stats on recent projects I’ve done:
When faced with with a complex condition like the one shown by @lydell, I tend to try to change my data modeling to allow flatter conditions with pattern-matching. For example:
Nice statistics thanks. I’m the same, I’m finding I’m using booleans less and less.
It does seem that you are using if-then-else when they do arise. Is that just out of habit, never having really thought about it, or do you really prefer that over a case-on-a-boolean?
I just randomly looked at a couple of ifs in your “Safe Tea” game that you linked to, here is one where your use of if avoids having to increase the indentation to the right:
toEntity : Bullet -> Entity
toEntity { position, status } =
case status of
Exploding diff ->
if ceiling (diff / explosionSpeed) == 1 then
explosionPhase1 position diff
else if ceiling (diff / explosionSpeed) == 2 then
explosionPhase2 position diff
else
explosionPhase3 position diff
_ ->
regularBulletEntity position
But wouldn’t this be better written as this:
toEntity : Bullet -> Entity
toEntity { position, status } =
case status of
Exploding diff ->
case ceiling (diff / explosionSpeed) of
1 ->
explosionPhase1 position diff
2 ->
explosionPhase2 position diff
_ ->
explosionPhase3 position diff
_ ->
regularBulletEntity position
Nice games.
Btw, off-topic: did you just manually count those statistics or do you have some cool elm-analsying software?
I don’t have a strong feeling either way. if-then-else has the advantage of allowing you to use a completely new expression in an else if clause, while keeping the conditional flat.
Agreed, the case is better. This part of the code was written when I was up against the game jam deadline so it was a bit rushed. Sometimes, when I can’t think of a good way to model a problem I’ll start by writing out a traditional boolean if-then-else and see if any patterns stand out.
I used a combination of cloc for lines of code and ag for occurrences of a keyword.
That looks correct, but there’s nothing saying that the list contains the modifiers in that order. Or somebody might change the order by mistake in the future.
That’s not a problem with treating this as a case expression, that is a plain old developer introduced bug. If you really need that level of compile time checking then it might help to convert your possible modifier key combinations into a union type and case off of the union type. Something like:
type Modifier
= OnlyControl
| OnlyShift
| Multiple
| None
toModifier : List Key -> Modifier
toModifier keysPressed =
case (List.length keysPressed, List.head keysPressed) of
(1, Just ctrl) ->
OnlyControl
(1, Just shift) ->
OnlyShift
(0, _) ->
None
(1, _) ->
None
(_ ,_)
Multiple
This is the same as the following but i find it a little more readable:
[ctrl] ->
[shift] ->
[] ->
[_]->
_ ->
Treating it this way also exposes that there are edge cases that are hard to tease out from your original solution. Like zero modifiers and more than one modifier have the same behavior. Is that appropriate? if a modifier is pressed that is neither control or shift, what happens is hidden in the body of the second if.
case (f < 0.1, f < 0.5) of
(True, _) -> "very small"
(False, True) -> "medium"
(False, False) -> "very big"
If you’re arguing that this is a reason to keep if/else, I agree with you! Encoding priorities is more naturally expressed with if/else in cases like this.
Obviously that’s pretty terrible for this simple example, but you can make a utility function:
nestedIf : a -> List (Bool, a) -> a
nestedIf default conditionals =
Maybe.withMaybe default <|
Maybe.map Tuple.second <|
List.Extra.find Tuple.first conditionals
Again overkill if that was your one example and for a list so small, but if you had a large list it could be a bit nicer than even the if-then-else.
All that said, I’d agree that this is kind of the killer feature for if-then-else, and agree with Brian’s conclusion.
If ‘case’ statements allowed for guards then obviously it would be trivial and arguably better expressed, but obviously they don’t.