-
Notifications
You must be signed in to change notification settings - Fork 15
7ML7W Elixir Day 1 Laying a Great Foundation
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 Proc
s, 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 Char
s 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 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.
- Home
- Documentation
- Choosing a Topic
- Shows & Tells
- Miscellaneous
- Opt Art
- Reinforcement Learning: An Introduction
- 10 Technical Papers Every Programmer Should Read (At Least Twice)
- 7 More Languages in 7 Weeks
- Lua, Day 1: The Call to Adventure
- Lua, Day 2: Tables All the Way Down
- Lua, Day 3
- Factor, Day 1: Stack On, Stack Off
- Factor, Day 2: Painting the Fence
- Factor, Day 3: Balancing on a Boat
- Elm, Day 1: Handling the Basics
- Elm, Day 2: The Elm Architecture
- Elm, Day 3: The Elm Architecture
- Elixir, Day 1: Laying a Great Foundation
- Elixir, Day 2: Controlling Mutations
- Elixir, Day 3: Spawning and Respawning
- Julia, Day 1: Resistance Is Futile
- Julia, Day 2: Getting Assimilated
- Julia, Day 3: Become One With Julia
- Minikanren, Days 1-3
- Minikanren, Einstein's Puzzle
- Idris Days 1-2
- Types and Programming Languages
- Chapter 1: Introduction
- Chapter 2: Mathematical Preliminaries
- Chapter 3: Untyped Arithmetic Expressions
- Chapter 4: An ML Implementation of Arithmetic Expressions
- Chapter 5: The Untyped Lambda-Calculus
- Chapters 6 & 7: De Bruijn Indices and an ML Implementation of the Lambda-Calculus
- Chapter 8: Typed Arithmetic Expressions
- Chapter 9: The Simply-Typed Lambda Calculus
- Chapter 10: An ML Implementation of Simple Types
- Chapter 11: Simple Extensions
- Chapter 11 Redux: Simple Extensions
- Chapter 13: References
- Chapter 14: Exceptions
- Chapter 15: Subtyping – Part 1
- Chapter 15: Subtyping – Part 2
- Chapter 16: The Metatheory of Subtyping
- Chapter 16: Implementation
- Chapter 18: Case Study: Imperative Objects
- Chapter 19: Case Study: Featherweight Java
- The New Turing Omnibus
- Errata
- Chapter 11: Search Trees
- Chapter 8: Random Numbers
- Chapter 35: Sequential Sorting
- Chapter 58: Predicate Calculus
- Chapter 27: Perceptrons
- Chapter 9: Mathematical Research
- Chapter 16: Genetic Algorithms
- Chapter 37: Public Key Cryptography
- Chapter 6: Game Trees
- Chapter 5: Gödel's Theorem
- Chapter 34: Satisfiability (also featuring: Sentient)
- Chapter 44: Cellular Automata
- Chapter 47: Storing Images
- Chapter 12: Error-Correcting Codes
- Chapter 32: The Fast Fourier Transform
- Chapter 36: Neural Networks That Learn
- Chapter 41: NP-Completeness
- Chapter 55: Iteration and Recursion
- Chapter 19: Computer Vision
- Chapter 61: Searching Strings
- Chapter 66: Church's Thesis
- Chapter 52: Text Compression
- Chapter 22: Minimum spanning tree
- Chapter 64: Logic Programming
- Chapter 60: Computer Viruses
- Show & Tell
- Elements of Computing Systems
- Archived pages