A Strongly Typed Typing Tutor

My project for the February language of the month was really exciting to work on, because it incorporates two of my favorite things: programming, and learning (natural) languages.

Last week, I used Perl 6 to find the most common n-grams in on HabraHabr, a Russian news site. At the end of the post, I said I would do something with this data to help improve my Russian typing speed. What I needed to do next was to build a UI to display n-grams and allow me to type them in. Hmm…what technology could I use to build a UI quickly?

I know…Elm!

Demo

The cool thing about writing a program in Elm and blogging about it is that since it's browser-based, I can show it off right here, right now! Go ahead, try typing in some characters:

This example uses English instead of Russian, because I don't expect many of you to know how to type Russian just yet. =) If you want to try the Russian version, it's here.

It's nice under the shade of the Elm tree

Writing Elm is a pleasure, but there are a few things that I particularly enjoyed.

Small functions are encouraged

Because of the way Elm's syntax works, long functions tend to look very ugly, so I ended up refactoring into a lot of smaller functions. I look at this as an advantage, because it encourages the developer to deal with things at the proper level of abstraction when writing a function, and smaller functions are easier to reason about.

<| and |>

Elm has two operators that help you when chaining calls together; they are similar to Haskell's $ operator. Their names are <| and |>. If they remind you of shell pipes, I'm sure that's intentional; they work like pipes, and the angle bracket character in each indicates in which direction values flow. I built up my program's state changes as small functions that do one thing; using <| makes them easy to chain together. For example, I lock the UI for one second when the user presses the wrong key. When it comes time to unlock the UI, I need to update the state so that the UI knows it's no longer locked, but also I need to start the user over in typing the current n-gram. I wrote two functions that achieve this: clearLock and resetCard. If I want to update the state, I could do it this way:

clearLock (resetCard state)

…but using <|, in my opinion, is so much clearer, especially when you need to call even more update functions:

clearLock <| resetCard state

HTML library

Elm's HTML library makes it easy to generate chunks of HTML to render parts of your application. For example, this is the code that renders a card, which is my program's abstraction over testing the user against a single n-gram:

showCard : Card -> Html
showCard (Card target typed) =
  let targetDisplay = text target
      ruleDisplay = hr [] []
      typedDisplay = renderTyped target typed
      styleAttr =
    style [("width", "400px"),
           ("height", "400px"),
           ("border", "thin solid lightgray"),
           ("box-shadow", "5px 5px lightgray")]
  in div [styleAttr] [ targetDisplay, ruleDisplay, typedDisplay ]

Pretty straightforward, huh?

Caveat Elmtor

While I enjoy Elm overall, I did encounter some quirks in the language. Here are the more signficant ones I discovered.

foldp doesn't get initial signal values

I wanted to use the current timestamp as a seed for the RNG, and I only wanted to seed the RNG once. This seemed pretty simple:

type alias State = { seed : Maybe Random.Seed }

let startingTimestamp = Signal.map (round << fst) <| Time.timestamp <| Signal.constant ()
    update = \timestamp state -> { state | seed = Just <| Random.initialSeed timestamp }
in Signal.foldp update { seed = Nothing } startingTimestamp

However, there's a caveat with foldp: it won't use the first signal value, so the seed is never initialized in this case! Fortunately, I did some searching and stumbled upon this StackOverflow post that recommends using Signal.Extra.foldp' . I didn't end up needing to use Signal.Extra, but it's good to have that in my back pocket for future Elm applications.

Overuse of Debug.crash

A lot of Elm's functions, such as getting the first element of a list, return a Maybe result; that is, you need to use a case statement to extract the actual result and handle the case where there is no result. However, sometimes, I know there's a result. one example of this is if I generate a random index into a list, and I then want to retrieve the item given that index. The index is guaranteed to be within the bounds of the list, but I would still need to handle the Nothing case. As a result, I ended up calling Debug.crash way more than I care to admit.

Debug.crash is Elm's panic button; it terminates the application with no chance of recovery. I think that its usage is acceptable in sections of code where its invocation is impossible, but the compiler still needs to be satisfied.

Function signatures must be exhaustive

My program has two types of inputs it needs to respond to: timer updates and key presses. I represented this via the following datatype:

type Event = Clock Time
  | Keypress Char

…which means my update function looks something like this:

update : Event -> State -> State
update event state =
  case event of
    Clock t -> -- handle clock update
    Keypress c -> -- handle key press

The logic for each type of event is pretty involved, so I figured that it would make sense for each to live in its own function. However, when I tried this:

handleClockUpdate : Event -> State -> State
handleClockUpdate (Clock t) state = ...

The compiler yelled at me:

-- PARTIAL PATTERN ---------------------------------------------------- Main.elm

This pattern does not cover all possible inputs.

54│ handleClock (Clock t) state =
                 ^^^^^^^
You need to account for the following values:

    Main.Keypress _

Switch to a `case` expression to handle all possible patterns.

While this error message is understandable and clear, I'm only ever going to call handleClockUpdate with a Clock event. This is somewhat similar to my gripe above about having to use Debug.crash to address scenarios that I'm confident will never happen; I'm grateful that the compiler is so careful, though. I would like to be able to express functions in multiple variants like I can in Haskell or Perl 6; hopefully this will come in a later version of the Elm language.

If you're curious about the full program, I uploaded it to GitHub. If you're an experienced Elm user, please let me know if there are ways to address the issues I found above, or if there's some other ways I can improve my code!

Published on 2016-03-07