Today I Learned

hashrocket A Hashrocket project

6 posts about #elm surprise

Debugging: Elm evaluates uncalled `let` variables

If you write a function that has a let expression variables like so:

view : Model -> Html Msg
view model =
    let
        logModel = Debug.log "model:" model
    in
      div []
          [ button [ onClick Increment ] [ text "+1" ]
          , div [] [ text <| String.fromInt model.count ]
          , button [ onClick Decrement ] [ text "-1" ]
          ]
Elm

When the view function is called you will see the console log message that logModel writes, even though it was never called from the function's body.

This can be useful for debugging function arguments coming in, or other variables without messing with the function's body.

To avoid the [elm-analyse 97] [W] Unused variable "logModel" warning you can use an underscore instead of naming the variable:

example =
    let
        _ = Debug.log "foo" "bar"
    in
      "function body"
Elm

It is worth mentioning that variables that are called from a function's body will only be executed once.

example =
  let
    foo = Debug.log "foo" "I'm called once"
    
    bar = Debug.log "bar" "I'm called once"
  in
    bar
Elm

will result in only two console log messages, one for foo, and one for bar.

h/t Jeremy Fairbank

What's the Trailing Underscore in Elm?

Variables such as model_, with a trailing underscore, are allowed and conventional in Elm. If you're familiar with a language where _model can mean an unused variable, this can cause a double-take. The trailing underscore is a nod to Haskell, telling us the variable is related, or similar, to a prior variable.

Here's an example: model_ is the argument of the update function in the Elm architecture, and model is the updated model we'll return:

> model_ = { start = "now" }
{ start = "now" } : { start : String }
> model = { model_ | start = "tomorrow" }
{ start = "tomorrow" } : { start : String }
Elm

This Stack Overflow answer summarizes the convention:

https://stackoverflow.com/a/5673954/2112512

A single quote (model') was used similarly in the past; that syntax was deprecated in Elm 0.18. Here's a GitHub issue describing that decision:

https://github.com/elm-lang/elm-plans/issues/4

Destructuring Record in Fn Argument

Elm has destructuring/pattern matching that feels typical for an ML. One pattern matching feature I like is record destructuring in a function argument.

myRecord = {a = 1}

myFunc {a} = a

myFunc myRecord
# 1
Elm

Here, we destructure the key a out of the record. What's cool about this is the record does not need to match exactly, but can be any record with the property of a.

myRecord = {a = 1, b = 2}

myFunc {a} = a

myFunc myRecord
# 1
Elm

This enables us to be able to grow the record over time without changing the signature of the function. In Elm, not having to accomodate that change across the entire program is great.

Formatting Elm Code

Elm comes with it's own formatter.

elm-format src/
Sh

It's got options like --upgrade which will help you get from 0.18 code to 0.19 code, and its got --validate which you can use in continuous integration to ensure all PRs are properly formatted.

In vim, formatting is enabled by default when you use elm-vim.

Where is List.zip in Elm?

unzip is a function available as part of the list package.

List.unzip [(1, 2), (3, 4)]
-- ([1,3],[2,4])
Elm

It's defined as:

Decompose a list of tuples into a tuple of lists.

But there is no corresponding zip function to compose a tuple of lists into a list of tuples. If you just want a list to be zipped with it's index, then you can use List.indexedMap.

List.indexedMap (\x y -> (x, y)) ["a", "b", "c"]
-- [(0,"a"),(1,"b"),(2,"c")]
Elm

And you could substitute (\x y -> (x, y)) with Tuple.pair which does the same thing.

List.indexedMap Tuple.pair ["a", "b", "c"]
-- [(0,"a"),(1,"b"),(2,"c")]
Elm

And if you don't care about indexes but instead have two lists, you can zip those two lists together with List.map2.

List.map2 Tuple.pair [1, 3, 5] ["a", "b", "c"]
-- [(1,"a"),(3,"b"),(5,"c")]
Elm

Happy Zipping!

Random is not pure in Elm

Elm requires that functions be pure, that is, the same arguments should produce the same outputs every time. Random necessarily injects some uncertainty into what the outputs that way and Elm has decided to handle random differently than in other languages.

First, install the random package:

elm install elm/random
Bash

The Random package allows you to create numbers in a couple of different ways, but the most idiomatic is to create a generator:

generator = (Random.int 1 10)
Elm

And then create a message that will let the Elm runtime know to produce a random number with the parameters defined by the generator.

type Msg = ConsumeRandomValue

msg = Random.generate ConsumeRandomValue generator
Elm

This message can then be placed into the (model, msg) tuple that is returned from the update function. The update function is then called to respond to the message, using the message type to wrap the random value that has been produced.

import Random

type Msg = ProduceRandomValue | ConsumeRandomValue Int

update msg model =
    case msg of
        ProduceRandomValue -> 
            (model, Random.generate ConsumeRandomValue (Random.int 1 10))
        ConsumeRandomValue randomValue ->
            ({model | rValue = randomValue}, Cmd.none)
Elm