Search code examples
syntaxpattern-matchingelm

Why doesn't the case expression work in this Elm code?


I wrote an Elm code snippet that would draw a square and change the square color between red and black every time when mouse is clicked.

However the case structure in the changeColor function doesn't work as expected, while the changeColor implemented with if structure will work.

What should I do to figure out what's going wrong? Thank you.

import Color exposing (red, black, blue, Color)
import Signal exposing ((<~))
import Graphics.Element exposing (Element, show)
import Graphics.Collage exposing (collage, square, filled, Form)
import Mouse
import Window

main : Signal Element
main = 
  scene <~ (Signal.foldp changeColor black Mouse.clicks)

scene : Color -> Element
scene color = 
  collage 600 600 [ filled_square color ]

changeColor : () -> Color -> Color
changeColor _ color =
  case color of
    black -> red
    red -> black

--changeColor _ color =
--  if | color == black -> red
--     | color == red -> black

filled_square : Color -> Form
filled_square color = square 100 |> filled color

Solution

  • Pattern variables (the short answer)

    Lowercase names in case patterns are always considered variables, never constants. So your case statement will match color against a pattern variable black, which succeeds, and binds the name black to the value of color inside the case branch (-> red).

    In this case the multi-way if that you have in comments is the appropriate way to distinguish the cases.

    Using case expressions

    Case expressions are used to distinguish cases in union types. You could for example explicitly model your program states with a union type like so:

    import Color exposing (red, black, blue, Color)
    import Signal exposing ((<~))
    import Graphics.Element exposing (Element, show)
    import Graphics.Collage exposing (collage, square, filled, Form)
    import Mouse
    import Window
    
    type Model = Red | Black
    
    main : Signal Element
    main = 
      scene <~ (Signal.foldp changeColor Black Mouse.clicks)
    
    scene : Model -> Element
    scene model = 
      collage 600 600 [ filled_square (toColor model) ]
    
    toColor : Model -> Color
    toColor model =
      case model of
        Black -> black
        Red -> red
    
    changeColor : () -> Model -> Model
    changeColor _ model =
      case model of
        Black -> Red
        Red -> Black
    
    filled_square : Color -> Form
    filled_square color = square 100 |> filled color
    

    The reason for doing this could be that you now have a finite, enumerable states for your program in an easy to find place. If you use just the colours, you only know this for sure by looking through the entire program. The case expression is exhaustive, it handles all states your program can be in. Whereas with a multi-way if on colours, who knows if those are the only colours possible to have in your program, especially once it grows larger than toy-example-size. If you don't match all the possible colours, you could run into a runtime crash of your application. (This is one of the few possible ways that could happen in Elm)