7ML7W Elixir Day 1 Laying a Great Foundation

Paul Mucur edited this page May 29, 2018 · 14 revisions

The meeting

After a bread-less meeting was narrowly avoided due to the heroic efforts of @tuzz and @charlieegan3, @tomstuart kicked off the meeting in our new surroundings of the Unboxed office.

We began by running through the initial "Ruby++" examples given in the book:

IO.puts "It's B-29s, bub."

@tomstuart was hoping this would work verbatim in Ruby but, alas:

> IO.puts "It's B-29s, bub."
NoMethodError: private method `puts' called for IO:Class
from (pry):1:in `__pry__'

A little cheating produced what we had hoped for:

> IO.send :puts, "It's B-29s, bub."
It's B-29s, bub.

We took the example of Enum.at [], 0 as an opportunity to flag the fact that Elixir is a functional language and, unlike Ruby, functions are typically defined in top-level modules rather than as methods on objects directly (that is, the example is not [].at(0)).

We glossed over the one-line if statement syntax (if 1 < 2, do: IO.puts "Woo") which looked mighty like a single function call with some sort of hash as a second argument, e.g.

if(1 < 2, [ do: IO.puts "Woo" ])

However, this immediately raises questions about the evaluation strategy of Elixir (e.g. why doesn't that immediately print Woo to the screen?). We contented ourselves that our adventures with macros in the next chapter might reveal all so we needn't dwell yet.

It was time for adventures in pattern matching.

We had much fun figuring out the rules of pattern matching in Elixir, e.g. that matching only works on the left hand side of an expression:

iex(4)> 10 = foo
** (CompileError) iex:4: undefined function foo/0

iex(4)> foo = 10
10
iex(5)> 10 = foo
10

We had even more fun pattern matching tuples:

iex(6)> {city, :uk} = {:london, :uk}
{:london, :uk}
iex(7)> {city, :usa} = {:london, :uk}
** (MatchError) no match of right hand side value: {:london, :uk}

We also discussed whether the use of the underscore _ was somehow special in a pattern match or just another arbitrary identifier. We suspected that, like in other languages, it was handled especially so that duplicates are permitted. Typically, using the same unbound variable multiple times on the left-hand side of a match places a constraint on those variables all having the same value, e.g.

iex(9)> {clothing, fruit, fruit} = {:hat, :apple, :banana}
** (MatchError) no match of right hand side value: {:hat, :apple, :banana}

iex(9)> {clothing, fruit, fruit} = {:hat, :apple, :apple} 
{:hat, :apple, :apple}

However, an underscore _ can be used multiple times to represent different values:

iex(10)> {clothing, _, _} = {:hat, :apple, :apple}        
{:hat, :apple, :apple}

We briefly discussed the chapter's coverage of re-using the same local variable to represent different values but no one seemed to think it was particularly interesting or noteworthy and so we merrily moved on! (It did later seem that this detour was meant to set up a discussion of whether Elixir's ergonomics/"sugar" are worth it or not.)

We now found ourselves in familiar land: functions!

We noted the calling syntax for anonymous functions in Elixir seemed very familiar to Ruby's for Procs, e.g.

iex(11)> inc = fn(x) -> x + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(12)> inc.(1)
2
[1] pry(main)> inc = ->(x) { x + 1 }
=> #<Proc:0x00007fa0e9d98140@(pry):1 (lambda)>
[2] pry(main)> inc.(1)
=> 2

We encountered the first obvious influence from Clojure: the short-hand anonymous function syntax:

iex(3)> &(&1 + 1)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(5)> (&(&1 + 1)).(2)
3
user=> #(+ % 1)
#object[user$eval1775$fn__1776 0x60d2c344 "user$eval1775$fn__1776@60d2c344"]
user=> (#(+ % 1) 2)
3

We then encountered another powerful feature inspired by Clojure's threading macros: Elixir's pipe operator |>:

10 |> inc.() |> inc.() |> dec.()
(-> 10 inc inc dec)

We had a brief discussion about the odd syntax here: are we literally calling inc.() with no arguments? We wondered whether this would have been simpler if we'd seen named functions first which would lead to this slightly less noisy version:

10 |> inc |> inc |> dec

Again, we were pretty confident this was some macro shenanigans. Perhaps the example above is literally rewritten by the pipe operator into the non-pipelined version we saw earlier in the chapter:

dec.(inc.(inc.(10)))

Yes it is a macro, but it's not just rewriting quite like that, because it works on non anonymous functions. The pipe is also an infix operator, there are a limited number of them that you can define for yourself.

Eager to see named functions, we moved onto modules and flexed our pattern matching muscles by looking at one of the example modules and wondering if we could improve it:

defmodule Square do
  def area({w, h}) when w == h do
    Rectangle.area({w, w})
  end
end

Our suggested alternative:

defmodule Square do
  def area({w, w}) do
    Rectangle.area({w, w})
  end
end

It worked!

We then had even more pattern matching fun with maps by trying to pull out sub-maps. @tomstuart rolled up his sleeves and immediately tried this beauty:

iex(13)> book = %{title: "Programming Elixir", author: %{first: "David", last: "Thomas"}}
%{author: %{first: "David", last: "Thomas"}, title: "Programming Elixir"}
iex(14)> %{author: author = %{last: "Thomas"}, title: "Programming Elixir"} = book
%{author: %{first: "David", last: "Thomas"}, title: "Programming Elixir"}
iex(15)> author
%{first: "David", last: "Thomas"}

As we might expect from a functional language, it seems Elixir's data structures prefer to be immutable so rather than updating a map in-place, we prefer to return a new map with our changes. In Clojure, this is typically done with assoc or assoc-in but Elixir has the very nifty-looking put_in:

iex(1)> book = %{title: "Programming Elixir", author: %{first: "David", last: "Thomas"}}
%{author: %{first: "David", last: "Thomas"}, title: "Programming Elixir"}
iex(4)> put_in book.author.first, "Dave"
%{author: %{first: "Dave", last: "Thomas"}, title: "Programming Elixir"}

This was surprising as the first argument seems to be an expression returning the value in the map and we don't even pass the map at all!

A little digging revealed that this may be yet more macro shenanigans as there is a three-argument version of put_in which is slightly less magical:

iex(5)> put_in book, [:author, :first], "Dave"
%{author: %{first: "Dave", last: "Thomas"}, title: "Programming Elixir"}

This matches Clojure's assoc-in exactly:

user=> (def book {:title "Programming Elixir" :author {:first "David" :last "Thomas"}})
#'user/book
user=> (assoc-in book [:author :first] "Dave")
{:title "Programming Elixir", :author {:first "Dave", :last "Thomas"}}

We wondered what the limits of the two-argument put_in might be: would it work with book[:author][:first]?

iex(16)> put_in book[:author][:first], "Dave"
%{author: %{first: "Dave", last: "Thomas"}, title: "Programming Elixir"}

We suspected that this is the limit: that it would not work with arbitrary expressions but, again, we'll wait until we get to macros proper to explore further. We were particularly interested to explore the fact that one form of put_in is a macro and the other is a more typical function and what effect that might have on the programmer: in contrast, Rust macros always end in an exclamation mark (e.g. println! and vec!) to distinguish them from function calls.

Following maps, it was time to meet our old friend: the list!

Elixir has a list type that is basically a linked list (think Lisp cons cells) optimised for head-first traversal and construction. Note these are not like Clojure's "vectors" which are really Hash-Array Mapped Tries and so make insertion at random locations efficient.

We had fun constructing and destructuring lists using the | operator:

iex(19)> [0 | [1, 2]]
[0, 1, 2]

It seemed like the proper way to construct a list was to pass a single head value on the left and then another list on the right. However, it did seem to work even when passing something that isn't a list on the right:

iex(25)> [0 | 1]
[0 | 1]

@tomstuart explained that this was still a cons cell: we're used to storing other cons cells (e.g. lists) in the right hand side but that doesn't need to be the case.

iex(26)> [head | tail] = [0 | 1]
[0 | 1]
iex(27)> head
0
iex(28)> tail
1

We pondered the syntax for constructing lists with more than one element in the "head" section and deduced it was sugar for the longer form way of specifying this with nested lists:

iex(29)> [0, 1, 2 | [3]]
[0, 1, 2, 3]
iex(30)> [0 | [1 | [2 | [3]]]]
[0, 1, 2, 3]

One clear win by allowing this syntax is its use in pattern matching:

iex(31)> [first, second, third | rest] = [0, 1, 2, 3]
[0, 1, 2, 3]
iex(32)> first
0
iex(33)> second
1
iex(34)> third
2
iex(35)> rest
[3]

@tomstuart then dazzled us with his knowledge of ASCII as it turns out that Chars in Elixir are considered lists:

iex(24)> [104 | 'ello'] 
'hello'

Fun with lists over, we briefly marvelled at Elixir's for comprehensions which are extremely similar to Clojure's:

iex(13)> for x <- [1, 2, 3], do: x
[1, 2, 3]
iex(14)> for x <- [1, 2], y <- [3, 4], z <- [5], do: {x, y, z}
[{1, 3, 5}, {1, 4, 5}, {2, 3, 5}, {2, 4, 5}]
iex(15)> for x <- [1, 2], y <- [3, 4], z <- [5], x + y < 5, do: {x, y, z}
[{1, 3, 5}]
user=> (for [x [1 2 3]] x)
(1 2 3)
user=> (for [x [1 2] y [3 4] z [5]] [x y z])
([1 3 5] [1 4 5] [2 3 5] [2 4 5])
user=> (for [x [1 2] y [3 4] z [5] :when (< (+ x y) 5)] [x y z])
([1 3 5])

There were general murmurs of approvals and we reasoned these were very similar to nested for loops in other C-based languages.

We then encountered our last data structure of the chapter: keyword lists. Very similar to maps, the book seemed to push these as being somewhat deprecated in newer versions of Elixir. The interesting part is that the curious one-line do syntax we'd seen with if statements and function declarations actually does desugar to a keyword list as we wondered earlier:

iex(36)> if(1 < 2, [do: IO.puts "Woo"])
Woo
:ok

@tomstuart showed us a Ruby equivalent to keyword lists: using assoc and rassoc on arrays:

[3] pry(main)> [[:name, 'Alice'], [:age, 42]]
=> [[:name, "Alice"], [:age, 42]]
[4] pry(main)> [[:name, 'Alice'], [:age, 42]].assoc(:name)
=> [:name, "Alice"]
[5] pry(main)> [[:name, 'Alice'], [:age, 42]].rassoc('Alice')
=> [:name, "Alice"]

Finally, we stared quizzically at the syntax for default arguments (and had a brief debate why you would need default arguments at all if we can specify multiple versions of a method):

def foo(x \\ 0), do: x

We had just enough time to do one of the exercises together so we decidedly to recursively find the maximum of a list:

defmodule MyFunctions do
  def maximum([head]) do
    head
  end

  def maximum([head | tail]) do
    maximum_of_tail = maximum(tail)

    if head > maximum_of_tail do
      head
    else
      maximum_of_tail
    end
  end
end

Thanks

Thanks to @elenatanasoiu and Unboxed for hosting and providing beverages, to @tuzz, @charlieegan3 and @dkandalov for bread, dips and snacks and to @tomstuart for driving and REPL shenanigans.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.