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" ]
          ]

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"

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

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 }

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

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

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.

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])

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")]

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")]

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")]

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

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)

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

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)