If-then-else versus case expressions

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:

  1. People seem a bit undecided on the best way to format an if-then-else, but there is a clear convention for case expressions.
  2. 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.
  3. 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:

  1. Might be a bit more familiar for someone coming from another language who is reading my code
  2. 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:

  1. The language is slightly simplified as there is only one way to make conditional expressions
  2. Minor: it would free up the keywords either for variable names or for some future syntax.

Obvious disadvantages would be:

  1. 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.)
  2. 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.
  3. 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.

3 Likes

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
2 Likes

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.

2 Likes

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:

Name Size Uses of if Uses of case case on a boolean
closed source ~ 3000 lines of Elm 0 33 0
closed source ~ 1500 lines of Elm 3 14 0
Safe Tea ~ 1300 lines of Elm 19 18 0
Down the River ~ 1200 lines of Elm 4 16 0
3 Likes

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:

case modifierKeys of
  [ Ctrl ] ->
    EdgeMovement

  [ Shift ] ->
    SkipBlanksMovement

  [ _ ] ->
    FixedMovement (4 + jumpHack)

  _ ->
    FixedMovement 1
7 Likes

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.

1 Like

@joelq Thanks for the advice about modeling better, btw! Though in this specific case, using a list invites for mistakes like this:

case modifierKeys of
  [ Ctrl ] ->
    EdgeMovement

  [ Shift ] ->
    SkipBlanksMovement

  -- Oops!
  [ Ctrl, Shift ] ->
    NewFeatureMovement

  [ _ ] ->
    FixedMovement (4 + jumpHack)

  _ ->
    FixedMovement 1

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.

1 Like

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.

3 Likes

I also find the tuple pattern matching more intuitive. Thanks for posting it.

1 Like

I use a bunch of ifs to compare against a float:

foo : Float -> String
foo f =
  if f < 0.1
    "very small"
  if f < 0.5
    "medium"
  else
    "very big"

There should be a way to figure it out using ‘case’! How would you go around this one?

1 Like

It’s possible but it’s not very natural.

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.

1 Like

Brian’s translation is good. You can also do it in the more 'formulaic manner" with the usual disadvantage of increasing indentation:

foo : Float -> String
foo f =
    case f < 0.1 of
       True ->
            "very small"
       False ->
            case f < 0.5 of
                  True ->
                         "medium"
                  False ->
                         "very big"

Another possibility is to use a list of tuples:

foo : Float -> String
foo f =
    Maybe.withMaybe "very big" <|
         Maybe.map Tuple.second <|
               List.Extra.find Tuple.first
                   [ ( f < 0.1, "very small")
                   , (f < 0.5, "medium")
                   ]

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.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.