Skip to content

7ML7W Elm Day 1 Handling the Basics

Paul Mucur edited this page Apr 6, 2018 · 20 revisions

Table of contents

Preamble

After a brief moment of panic involving our traditional club bread knife going missing, we kicked off our first meeting on the third language of our current book: Elm.

While tucking into topic-appropriate Twiglets, we began by discussing how we wanted to run the meeting given that we'd had success previously by concentrating on a particular aspect of the chapter rather than going through the whole thing. As we had no particular area on which to concentrate this time around (and as the first day of each language tends to be a more generic "let's get started"), we decided to go through the chapter collectively led by @tuzz. Though he wasn't able to attend we kept @h-lame's notes on the chapter close to hand for discussion.

The chapter

The chapter began by describing Elm as "strongly influenced by the ML language family" and we wondered what that actually meant: what are the defining characteristics of an ML language?

We looked to resident ML-enthusiast @tomstuart and contrasted Elm with languages such as Ruby, Perl and even JavaScript which have a more C-like background. We discussed whether characteristics might include algebraic data types and pattern matching or whether the focus on preventing run-time errors was somehow key but there was no clear consensus. Anecdotally, we heard about Elm evangelists being particularly proud of having no run-time errors in their production systems.

We took a brief moment to discuss a problem a lot of us had run into: the book uses Elm 0.15 but the current version of Elm is 0.18. What's worse is that Elm 0.16 introduced some major changes to the language including features covered in detail by the book itself. We decided to stick with the book's use of 0.15 for now but acknowledged this may be problematic in future.

As we didn't have someone driving a laptop at first, we attempted to use Try Elm and Ellie as an Elm REPL but, alas, both use a newer version of Elm and don't offer a way to switch versions.

Undetered, we plowed on with the chapter.

"Simple" expressions

Our first introduction to Elm is through its REPL and the venerable number 4. The book told us to expect the following:

> 4
4 : number

However, it didn't mention that there is a good 30 second pause between entering the number 4 and it responding. @lazyatom mentioned that, as he runs Little Snitch on his laptop, he noticed Elm making a large number of network requests when he tried this himself. This fit with @wjlroe's suggestion on Slack that Elm lazily downloads core libraries in the REPL as needed.

Thank goodness we didn't try 5!

After noting that using the string "String" as an example of a string might have been needlessly confusing...

> "String"
"String" : String

We discussed Elm's notion of "strong typing" with the example of concatenation, particularly in contrast to the typical behaviour in JavaScript.

e.g. in JavaScript:

> 4 + "4"
'44'

And in Elm:

> 4 ++ "4"
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm

The left argument of (++) is causing a type mismatch.

3|   4 ++ "4"
     ^
(++) is expecting the left argument to be a:

    appendable

But the left argument is:

    number

We discussed the quality of Elm's error messages and how it was an early proponent of good, human-readable error messages (with an obligatory mention of Rust's).

While discussing concatenation, we were introduced to Elm's concept of "type classes" such as appendable. We read that concatenation with ++ work on appendable data types but this was a little confusing as different appendables cannot be mixed:

> "Foo" ++ "bar"
"Foobar" : String

> [1, 2] ++ [3, 4]
[1,2,3,4] : List number

> "Foo" ++ [3, 4]
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm

The right side of (++) is causing a type mismatch.

3|   "Foo" ++ [3, 4]
              ^^^^^^
(++) is expecting the right side to be a:

    String

But the right side is:

    List number

After getting into quite a mess trying to understand the following code and its utterly baffling type signature (noting that it doesn't work in the REPL), we decided to skip onto Conditionals:

> a = [1, 2, 3, 4]
> a[1] = 2
<function> : List number -> number'

Conditionals

if | x < 0 -> "too small" \
   | x > 0 -> "too big" \
   | otherwise -> "just right"

We met our first if statement in Elm and discussed the use of | characters for breaking up clauses and whether or not whitespace is significant. @lazyatom noted that new lines are not required in an if statement and therefore the above example also works like so:

if | x < 0 -> "too small" | x > 0 -> "too big" | otherwise -> "just right"

Which isn't too shabby at all.

We discussed the significance of otherwise and it was noted that otherwise is simply an alias for True in Haskell much like the usage of the truthy :else in Clojure's cond function.

The book also gives an example of a simpler if:

if x < 0 then "too small" else "ok"

However, we heard that if statements have been somewhat revamped in more recent versions of Elm to make these two forms more consistent.

We then saw our first bit of pattern matching:

case list of \
  head::tail -> tail \
  [] -> []

We went down a bit of a rabbit hole discussing how head::tail might work but, in the end, were content to note that the left-hand side of each case is some sort of pattern (e.g. like a regular expression) to both match and destructure any input. In this case, we're taking a list and splitting off its head and tail and returning only its tail.

As list might have neither a head nor tail, we have to also provide the empty list [] as a case for our pattern match.

In which we get very cross about algebraic data types

The next part of the chapter caused the most consternation as we saw how to construct our own algebraic data types:

type Color = Black | White
type Piece = Pawn | Knight | Bishop | Rook | Queen | King
type ChessPiece = CP Color Piece

Our immediate question was why the definition of ChessPiece used CP and what on earth is Color or Black anyway?

We broke this down a bit to discover that Black is a data constructor for values of the type Color and that CP constructs ChessPieces but takes two arguments: a value of type Color and a value of type Piece.

Still, some of us were angry: could the constructor for ChessPiece be called... ChessPiece?

> type ChessPiece = ChessPiece Color Piece
> ChessPiece Black Knight
ChessPiece Black Knight : Repl.ChessPiece

Yes, it can! In general, it was very hard to keep track of whether we were in the language of types or the language of values.

Then came the most confusing example in the chapter:

type List = Nil | Cons Int List

The explanation for this line of code was as follows:

We define a type of List as either:

  • The type Nil, or
  • A list constructed with a head of type Int and a tail of type List

This immediately enraged several attendees (particularly those fond of Haskell) as, well, is Nil a type?

WRONG.

We slowly realised that the explanation was extremely confusing as there is nothing inherently list-specific about any of the types in the given definition of List: there is no special meaning to Nil and no special meaning to Cons.

In fact, while this seems very list like:

> type List = Nil | Cons Int List
> Cons 1 (Cons 2 (Cons 3 Nil))
Cons 1 (Cons 2 (Cons 3 Nil)) : Repl.List

It also works if you replace the names:

> type Wibble = Flibbertigibbet | Blancmange Int Wibble
> Blancmange 2 (Blancmange 3 (Blancmange 4 Flibbertigibbet))
Blancmange 2 (Blancmange 3 (Blancmange 4 Flibbertigibbet)) : Repl.Wibble

Sick of having Wibbles Lists of only Ints? Fret not, we also saw a unary type constructor for new types of Lists:

type List a = Nil | Cons a (List a)

This allows us to create Lists containing values of any type we specify:

> Cons "Foo" (Cons "Bar" Nil)
Cons "Foo" (Cons "Bar" Nil) : Repl.List String

Maybe Records will calm us down

Skipping over the first paragraph of this section, we encountered Elm's record type, much like the one we'd seen in "Types and Programming Languages":

{ color = Black, piece = Queen }

While these were pitched as an alternative to explicitly defining algebraic data types as we'd seen previously, @mudge wondered whether we could define our own type using a record:

> type ChessPiece = { color: Color, piece: Piece }
-- SYNTAX PROBLEM -------------------------------------------- repl-temp-000.elm

I ran into something unexpected when parsing your code!

2| type ChessPiece = { color: Color, piece: Piece }
                     ^
I am looking for one of the following things:

    a constructor for a union type
    whitespace

Alas, we could not (after the meeting, @marksweston discovered that you can do this with type aliases).

We then discussed how to read the fields of a record:

> blackQueen = { color = Black, piece = Queen }
{ color = Black, piece = Queen } : { color : Repl.Color, piece : Repl.Piece }
> blackQueen.color
Black : Repl.Color

This was explained as syntactic sugar for the following:

> .color blackQueen
Black : Repl.Color

We were intrigued to see the definition of this scope-level .color function:

> .color
<function> : { b | color : a } -> a

We touched on how Haskell has similar behaviour and how that leads to complications when you have multiple records with the same field names but potentially different types of values. This doesn't seem to be an issue in Elm however:

> pieceOfString = { length = "as long as you want" }
{ length = "as long as you want" } : { length : String }
> yearInDays = { length = 365 }
{ length = 365 } : { length : number }
> pieceOfString.length
"as long as you want" : String
> yearInDays.length
365 : number

What's more, it seems that any name preceded by a full stop desugars to a function to project a field out of a record, e.g. with no records in scope at all:

> .iwonder
<function> : { b | iwonder : a } -> a

Functions

> add x y = x + y
<function> : number -> number -> number

We enjoyed how simple Elm's function declarations are, chortled at a supposedly anonymous function named anonymousInc and @jcoglan (now driving a REPL on his laptop) demonstrated how the above add can be defined in several different but equivalent ways:

> add x = \y -> x + y
<function> : number -> number -> number
> add = \x y -> x + y
<function> : number -> number -> number

We marvelled at Elm's Elixir-like pipe operator:

> inc = add 1
<function> : number -> number
> 5 |> inc
6 : number

@mudge noted the similarity to Clojure's threading macros, e.g. -> and we justified their existence as a more readable way to express heavily nested function calls.

e.g.

42 |> a |> b |> c |> d |> e

vs.

e(d(c(b(a(42)))))

We dug into the concept of currying (or, if you prefer, Schönfinkeling) and its distinction from "partial application". In brief, functions in Elm take no more than one argument (ala the Lambda calculus we've seen in previous books); the way functions appear to take multiple arguments is by returning successive functions, again only taking one argument. For example:

add x y = x + y

Is effectively equivalent to the following in JavaScript:

function add(x) {
    return function (y) {
        return x + y;
    };
}

In Elm, we can pass either all arguments or just some of them:

> add 1
<function> : number -> number

In JavaScript:

> add(1)
[Function]

It's only when we pass both arguments that we get a result:

> add 1 2
3 : number

And

> add(1)(2)
3

The final topic we covered was the record subtyping used by Elm when inferring types passed to functions:

> somePoint = { x = 5, y = 4 }
{ x = 5, y = 4 } : { x : number, y : number1 }
> xDist point = abs point.x
<function> : { a | x : number } -> number
> xDist somePoint
5 : number
> xDist { x = 5, z = 100 }
5 : number
> xDist { y = 4 }
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm

The argument to function `xDist` is causing a mismatch.

10|   xDist { y = 4 }
            ^^^^^^^^^
Function `xDist` is expecting the argument to be:

    { a | x : ... }

But it is:

    { y : ... }

And with that, we were done!

Retrospective

  • As may be clear from this write-up, we spent a lot of the time in the meeting being confused and frustrated by the examples given in the book and we wondered whether we should use other source material in future meetings to avoid this;
  • One concern with going off-book more was the increased burden on attendees to prepare whereas we currently rely only on the given chapter;
  • We were also concerned that the next two chapters cover the concept of functional reactive programming (FRP) using "signals" in Elm and this has been totally revamped in newer versions (replaced with the notion of "subscriptions")
  • It was agreed that the meeting improved as soon as @jcoglan started trying things out in a REPL halfway through the meeting: we should ensure that someone can do this in future meetings from the beginning;
  • We were thankful to @tomstuart for having organised the meeting and @marksweston kindly volunteered to organise the next.

Thanks

Thank you to @leocassarani and Geckoboard for hosting the meeting and providing beverages, thanks to @tomstuart for organising the meeting, thanks to @tuzz for "all the bread" and for leading us through the chapter, to all those who contributed snacks and to @jcoglan for driving the REPL.

Clone this wiki locally