Elixir has been getting a lot of interest recently. It's a functional, concurrent language that compiles to bytecode for the Erlang VM. As well as concurrency, it has strong support for metaprogramming with hygenic macros — think Scheme but without all the parentheses.
The syntax is really pretty. If you like how Ruby looks then you’ll find Elixir pleasant to work with. However, the semantics are very different from Ruby.
I have played with Elixir a little before when reviewing the O’Reilly book Introducing Elixir, by Simon St. Laurent and J. David Eisenberg, for FLOSSUK — not a bad introduction, incidentally. So I have a bit of an head start.
Elixir doesn’t get in your way. The dynamic type system is refreshing after Elm’s ML-like pernicketyness. It’s a tradeoff, of course; using dynamic types often means you’re storing up debugging pain for later. And there is something undeniably satisfying about how the (presumably monadic) side-effect of wrestling Haskell’s type system into submission can be a program that is in all likliehood correct.
I digress. Elixir is fun to write. If you aren’t yet convinced, have a look at Dave Thomas’s talk Elixir: the power of Erlang, the joy of Ruby which gives a good introduction Elixir and why it is worth being excited about.
Exercises
Easy exercises
Express some geometry objects using tuple: a 2d point, a line, a circle, a polygon, a triangle.
I first of all declared a module to wrap this lot up in and then made a point that was just a 2-tuple of x and y co-ordinate. A line is a pair of points. A circle is a point and a radius. An ngon is a list of connected lines, in this case there are 4 of them. A triangle is the same but can only have 3 lines.defmodule Shapes do
import :math
point = {1,9} # x,y coords
line = {{1,1}, point} # 2 points in cartesian space
circle = {{6,6}, 5} # centre point, radius
ngon = {line, {point, {15,9}}, {{15,9}, {15,1}}, {{15,1},{1,1}}}
# lines between vertices
triang = {line, {point,{15,1}}, {{15,1},{1,1}}} # lines between vertices
Write a function to compute the hypotenuse of a right angle triangle given the length of 2 sides.
I wasn’t able to find an exponentiation operator or the square function in the standard libraries (could be just failing google-fu). So I first implemented sq and then implemented my hypo function in terms of sq. I was able to use the sqrt function from the standard :math library, hence the import earlier. I read compute the hypotenuse
as compute the length of the hypotenuse
and I assumed that neither of the 2 lengths I was handed were the hypotenuse. # First implement square
def sq(x) do
x*x
end
# We can use the :math version of sqrt and our own sq to work out
# the square root of the sum of the squares of the two non-hypotenuse sides
# Which is the length of the hypotenuse.
def hypo(a,b) do
sqrt(sq(a) + sq(b))
end
Convert a string to an atom.
This seemed kind of trivial. Just use the library function to_atom# Convert string to atom
IO.inspect String.to_atom("HalloElixir")
Test to see if an expression is an atom.
Similarly, I just used library functions# test to see if it is an atom
IO.inspect is_atom(String.to_atom("HalloElixir"))
Medium exercises
Given a list of numbers, use recursion to find:
1. The size of the list
2. The maximum value
3. The minimum value
This is bread and butter functional programming. Start with a module to wrap stuff in, then implement size, min and max. I prepended c to my implementations to avoid names clashing with the standard libraries.
I use the hd function in the single arg cmin and cmax implementations. If xs is an empty list, this will fail. Otherwise I would have to implement the functions as monoids with an identity of -Infinity for max and Infinity for min. That is what happens in Javascript. I wrote about that in Why is Math.max() less than Math.min()? recently. defmodule Medium do
import Keyword
# Given a list of numbers use recursion to find:
# size of list
def csize([]), do: 0
def csize([_|t]), do: 1+csize(t)
# max value
def cmax(xs), do: cmax(xs,hd xs)
def cmax([],n), do: n
def cmax([h|t],n) do
if(h>n) do cmax(t,h) else cmax(t,n) end
end
# min value
def cmin(xs), do: cmin(xs, hd xs)
def cmin([],n), do: n
def cmin([h|t],n) do
if(h<n) do cmin(t,h) else cmin(t,n) end
end
Given a list of atoms, build a function called word_count that returns a keyword list, where the keys are atoms from the list and the values are the number of occurences of that word in the list. For example word_count([:one, :two, :two]) returns [ one: 1, two: 2 ]
Sounds like a fold, no? We use reduce from the Enum library to do the folding and the Keyword.get and Keyword.put to build ourselves a Keyword list. def word_count(xs) do
Enum.reduce(xs, [], &(put(&2, &1, 1 + get(&2,&1,0))))
end
Hard exercises
Represent a tree of sentences as tuples. Traverse the tree, presenting an indented list. For example, traverse({"See Spot.",{"See Spot sit.","See Spot run."}}) would return:
See Spot.
See Spot sit.
See Spot run.
This was very much the easier of the hard exercises in my opinion. You can use a couple of mutually recursive functions indent and the poorly but briefly named t.defmodule SentenceTree do
# Note that this only works for 2-tuples, which I am assuming was
# what was intended.
# Note also that we print things rather than constructing a list
# which is a bit impure. Whatever.
def traverse(st) do
t(0, st)
end
def indent(n,x) do
if is_tuple x do
t(n+1, x)
else
IO.puts String.duplicate(" ",n) <> x
end
end
def t(n, {x,y}) do
indent(n,x)
indent(n,y)
end
end
stup = {"See Spot.", {"See Spot sit.", {"See spot run.", "See spot code."}}}
SentenceTree.traverse(stup)
Given an incomplete tic-tac-toe board, compute the next player’s best move.
This was significantly harder than the previous exercise. I feel that the two_lines function could be more elegant. But it is done enough to sort of work. I call the game noughts and crosses, UK style. I represent the board as a list of chars that are either 'O', 'X' or ' '.
I first check if the game is over (i.e. the board has no ' 's) and print the result if it is. Next I create a list of ‘strategies’ to try in order of their relative value to the current player. A strategy changes the board if it is applicable. I map them over the board in order pulling out the first that succeeds. Which may mean doing a little extra bit of work, in this case I am not concerned with that because it is so little.
The strategies I try are:
- If this player has two pieces in a row, column, or diagonal put a piece in the third position in that row (or column or diagonal).
- If the other player has two pieces in a row, column, or diagonal, block them by putting our piece in the third position.
- Put this player’s piece in the centre square.
- Try the first empty square — which I term the crappy move.
I struggled a bit to recall (or work out) the syntax for higher order functions. Once I had that and pattern matching the problem became easier. I am fairly confident that I could do a better job on the two_lines function, but life is short and so is my spare time. And it is springtime now too.
defmodule NoughtsAndCrosses do
import Enum
import List
# check if there is a horizontal, vertical or diagonal line
# for player p
def winner?(p,[a,b,c,d,e,f,g,h,i]) do
any?([ {a,b,c}, {d,e,f}, {g,h,i}, {a,d,g} \
, {b,e,h}, {c,f,i}, {a,e,i}, {c,e,g} ] \
, fn(x)->x=={p,p,p} end)
end
# define fst and snd. There is probably a builtin for this.
def fst ({x,_}) do
x
end
def snd ({_,y}) do
y
end
def other_player(p) do
cond do
p=="O" -> "X"
true -> "O"
end
end
# given an opponent flag, a player p and a board, look for any nearly complete
# lines of that player.
# If opponent is true, then complete the line with the symbol of the
# opposite player of p (for O,X and vice versa)
# Otherwise complete the line with p's symbol
# Return a modified board
# This could undoubtedly be simplified with a lookup for the numbers and 3 tuples
# for lines but it works fine for my purposes
def two_lines(opponent,p,[a,b,c,d,e,f,g,h,i]) do
danger = [ {{a,b},2}, {{b,c},0}, {{d,e},5}, {{e,f},3}, {{g,h},9}, {{h,i},7} \
, {{a,c},1}, {{d,f},4}, {{g,i},8} \
, {{a,d},7}, {{d,g},0}, {{b,e},8}, {{e,h},1}, {{c,f},9}, {{f,i},2} \
, {{a,g},3}, {{b,h},4}, {{c,i},5} \
, {{a,e},9}, {{e,i},0}, {{c,e},7}, {{e,g},2}, {{g,c},4}, {{a,i},4} ]
match = find(danger, false, &(fst(&1) == {p,p}))
cond do
match == nil
-> [a,b,c,d,e,f,g,h,i]
opponent -> replace_at([a,b,c,d,e,f,g,h,i], snd(match), other_player(p))
true -> replace_at([a,b,c,d,e,f,g,h,i], snd(match), p)
end
end
# is the centre available
def centre_free([_,_,_,_,c,_,_,_,_]) do
c==' '
end
def try_own_twos do
fn(player,board) -> two_lines(false,player,board) end
end
def try_opponent_twos do
fn(player,board) -> two_lines(true,player,board) end
end
# Go in the centre square
def try_centre do
fn(player,board) ->
if(centre_free(board)) do
replace_at(board,4,player)
else
board
end
end
end
# take the first empty square
def try_crappy_move do
fn(player,board) -> replace_at(board,find_index(board, &(&1==' ')),player) end
end
def show_result(player,board) do
oppo = other_player(player)
cond do
winner?(player, board) -> IO.puts(player <> " Wins!")
winner?(oppo, board) -> IO.puts(oppo <> " Wins!")
true -> IO.puts("A strange game. The only winning move is not to play. How about a nice game of chess?")
end
end
def board_complete?(board) do
not any?(board, &(&1==' '))
end
def next_move(player, board) do
if board_complete?(board) do
show_result(player, board)
board
else
strategies =
[try_own_twos, try_opponent_twos, try_centre, try_crappy_move]
find(map(strategies, &(&1.(player,board))), nil, &(&1!=board))
end
end
end
testBoard = [ 'X', ' ', 'X' \
, 'O', ' ', 'X' \
, 'X', 'O', 'O' ]
IO.inspect NoughtsAndCrosses.winner?('X',testBoard)
IO.inspect NoughtsAndCrosses.next_move('X',testBoard)
My experiences
There is a lot to like in Elixir.
The syntax is just sugary enough not to get in your way, the libraries feel sensible and well throught out, the docs are well written and have examples. Dynamic typing won’t suit some programmers, but I found it pleasant enough not to worry about type errors for a bit.
The areas where Elixir really shines, in particular its support for metaprogramming through hygenic macros and its concurrency model and fault tolerance have yet to be touched on. Maybe in day two.