Stack Builders News

A collection of thoughts and notes by our team


Juan Pedro Villa

The Hang of Elm: Hangman and functional programming with Elm


One of my favorite ways to introduce (pure) functional programming is to use an example of imperative functional programming. I like using hangman because it's simple and very effective in showing some of the best features of functional languages.

Hangman is a game in which “someone tries to guess the letters of a word, and failed attempts are recorded by drawing a gallows and someone hanging on it, line by line” (Oxford Dictionaries).

A program to play hangman is easy to write as a command-line application, but it's just as easy to write as an HTML application with a functional programming language such as Elm.

All applications in Elm follow a basic pattern that consists of a model to describe the application, a way to view the model, and a way to update it given an action.

The model of each game of hangman should include at least the letters of a word and whether they've been guessed or not, and the number of failed attempts.

If we ignore failed attempts, the model is just a list of characters and booleans:

type alias Model =
  List (Char, Bool)

The model of a hangman game is initialized by setting all the letters of the word as not guessed:

init : String -> Model
init word =
  String.foldr (::) [] word
    |> List.map (\letter -> (letter, False))

Strings in Elm are not lists of characters, unlike strings in Haskell. For that reason, given a string, we have to transform it into a list and then map a function that creates tuples with the guessed value set to False.

In order to define how to update the model, we need to define what an action is. In this case, a character is enough -- it represents that the player pressed a key or made a guess:

type alias Action =
  Char

When the player makes a guess, the list of letters and guessed values has to be updated if there's a match:

update : Action -> Model -> Model
update guess letters =
  let
    match guess (letter, guessed) =
      (letter, guess == letter || guessed)
  in
    List.map (match guess) letters

To update the list, we map a function over each letter and set its guessed value to True if it matches the guess or if it has already been guessed.

Every time the model is updated, a new model should be displayed to the player, but we haven't defined what the model would look like in HTML:

view : Signal.Address Action -> Model -> Html
view address model =
  Html.div
    [ ... ]
    [ Html.h1 [] [ Html.text "Hangman" ]
    , Html.h3 [] [ Html.text (toString model) ]
    , Html.input
        [ ...
        , Html.Events.onKeyPress address Char.fromCode
        ]
        []
    ]

This is basically an HTML document inside Elm. The toString function turns the word into a string with hyphens for unguessed letters:

toString : Model -> String
toString letters =
  let
    fromLetter (letter, guessed) =
      if guessed then letter else '-'
  in
    List.map fromLetter letters
      |> List.foldr String.cons ""

Again, a string is not a list of characters, so we map a function over the letters to choose the letter or a hyphen, and then build the corresponding string with a fold.

But the most important part of the view function is that it takes an address and creates an event that reports to that address. If the player presses a key inside the input, that's reported to the given address and used for updating the model.

All applications in Elm follow the model/view/update pattern, which is actually known as the Elm architecture. The nice thing about it is that the three components are pure and completely separate -- we haven't done anything to connect view and update.

Until now:

main : Signal Html
main =
  StartApp.start
    { model = init "supercalifragilisticexpialidocious"
    , view = view
    , update = update
    }

And that's it. The StartApp package allows us to start an application by simply saying what model, view, and update are. To see it in action, get the code, and paste it and compile it in the Elm online editor.

This hangman game is very basic, though. We're ignoring failed attempts and nothing happens if the player wins. Let's take a look at the model if we were to extend it:

type alias Model =
  { letters : List (Char, Bool)
  , status : Status
  }

Instead of just a list of letters and guessed values, we include a status that says whether the game is over (lost or won) or in progress with a number of lives:

type Status
  = Lost
  | Playing Int
  | Won

Even though Elm is just in version 0.15, I think it's excellent for showing the best of pure functional programming -- basically, fewer headaches thanks to a clean separation of pure (model/view/update) and impure (main) functions.

comments powered byDisqus

Do You Have What it Takes To Be a Stack Builder?