Intro to Elixir for non Ruby programmers
Goals
I come from the land of PHP and JavaScript, with very little Ruby experience. I have seen quite a few posts around the interwebs introducing Elixir from a Rubyist's perspective but I haven't really seen any from a perspective I can relate to. The syntax of Elixir is kind of foreign to most people outside of Ruby-land, so I hope to help push people a little bit over the initial hump with some examples using PHP and JavaScript (ES2015) along with the Elixir examples.
My goal with this post is to help introduce the syntax as well as a shallow introduction to immutability and how we use recursion instead of loops.
If you have Elixir installed, you can paste or type examples into IEx (interactive elixir). Start IEx shell by just typing iex
into your terminal.
Syntax and Semantics
So, we have to admit that the Elixir syntax is very Ruby-like although the concepts and semantics are largely inherited from Erlang. However, Elixir introduces a lot of great new things of it's own, as well as cherry-picking some nice concepts from other functional languages.
Types
Basic types
Please read the elixir guide on basic types for more detailed info, this is a good place to start.
These common types you should already be used to:
- Boolean:
true
,false
- Integer:
1
- Float:
3.14
- String:
"Hello World"
- always using double quotes. - List:
[1, 2, 3]
- This is a singly linked list. Like an array in PHP or JavaScript, but not.
And maybe some you aren't used to:
- Atom:
:foo
- A constant whose value is its name. In some other languages it might be called a symbol. Later, you will see it's quite useful for pattern-matching. - Tuple:
{1, "two", :three}
- An ordered group of related values (any values).
Keyword Lists, Maps, and Structs
Please read the elixir guides on keyword lists & maps, and structs for more detailed info.
- Keyword List:
[one: 1, two: 2, three: 3]
- This is similar to an associative array in PHP, or a plain object in JavaScript (except it is ordered). It is basically just syntax sugar on top of lists of two-tuples of{:key, value}
. - Map:
%{name: "Joe", email: "joe@example.com"}
- This is also similar to an associative array in PHP (except it is unordered) or a plain JavaScript object. - Struct:
%Person{name: "Joe", email: "joe@example.com"}
- It's a map but with some compile-time guarantees and predefined properties.
Functions
In Elixir, functions (named functions) are always defined in modules. Modules are basically just however you decide that you want to group your functions. You should do this in a way that has meaning to you and makes sense in the context of your project. Defining a function is similar to any other language.
You define and use a function as follows: (I'm encapsulating the PHP and JavaScript versions in a class and object, to better compare with the Elixir module syntax)
PHP
class StrHelper
{
public static function uppity($str = "default")
{
return strtoupper($str);
}
}
StrHelper::uppity("foo"); // "FOO"
JavaScript
let strHelper = {
uppity(str = "default") {
return str.toUpperCase()
}
}
strHelper.uppity("foo") // "FOO"
Elixir
defmodule StrHelper do
def uppity(str \\ "default") do
String.upcase(str)
end
end
StrHelper.uppity("foo") # "FOO"
Anonymous functions
Anonymous functions are first class values and can be passed as arguments or returned from other functions.
Here is how you define and call an anonymous function:
PHP
$uppity = function($bar) {
return strtoupper($bar);
}
$uppity("bar"); // "BAR"
JavaScript
let uppity = function(bar) {
return bar.toUpperCase()
}
uppity("bar") // "BAR"
Elixir
uppity = fn bar ->
String.upcase(bar)
end
uppity.("bar") # "BAR"
# Notice the dot is required here.
Why do we need a dot for anonymous functions?
The main reason is that Elixir is a lisp-2 language - this means that variables and functions “occupy” different name spaces, so calling a function in the functions namespace and calling a function in the variables namespace has to work differently. Most languages are lisp-1 (which means that variables and functions are in the same namespace). Some other lisp-2 languages are Ruby, Common Lisp and Perl.
From a practical view variables and function names have basically the same syntax so you need to differentiate between when you want the function name and when you want the value of the variable.
– rvirding
Anonymous function short-hand
There is a "capture operator" in Elixir which can be used to either capture an existing named function to turn it into an anonymous function that can be passed as an argument, or to define a new anonymous function.
PHP
In PHP they are called "arrow functions."
$uppity = fn($bar) => strtoupper($bar);
$uppity("bar"); // "BAR"
JavaScript
They are also called arrow functions in JavaScript.
let uppity = (bar) => bar.toUpperCase()
uppity("bar") // "BAR"
Elixir
uppity = &(String.upcase(&1))
uppity.("bar") # "BAR"
Since the number of arguments of the anonymous function match the number of arguments of the named function we are using, we can also write it as:
uppity = &String.upcase/1
uppity.("bar") # "BAR"
Parentheses are optional in Elixir for function (and macro) calls. These are all the same thing:
fn s -> String.upcase(s) end
&(String.upcase(&1))
&String.upcase(&1)
&String.upcase/1
Capturing named functions
In Elixir, functions are not referred to only by their name, but also their arity. This just means they are defined by their name and the number of arguments they take.
For example String.upcase
takes one argument, so it is referenced as String.upcase/1
. Also, Enum.map
takes two arguments, so it is referenced as Enum.map/2
.
Putting that together, here is an example of capturing the exisiting function String.upcase/1
, for use in Enum.map/2
, which takes an anonymous function as it's second argument.
str = "Bananas"
str_list = String.graphemes(str)
#=> ["B", "a", "n", "a", "n", "a", "s"]
Enum.map(str_list, &String.upcase/1)
#=> ["B", "A", "N", "A", "N", "A","S"]
# Which is really the eqiuvalent of
Enum.map(str_list, fn c -> String.upcase(c) end)
#=> ["B", "A", "N", "A", "N", "A","S"]
Implicit returns
You may have noticed that there is no return
statement, because Elixir has implicit returns.
This means that the result of the last statement is what is returned.
so:
defmodule Foo do
def baz?(str) do
if String.equivalent?(str, "baz") do
"It's baz!"
else
"It's NOT baz!"
end
end
end
Foo.baz?("baz") # "It's baz!"
Foo.baz?("bar") # "It's NOT baz!"
Passing "baz"
above means the last statement executed is "It's baz!"
.
Also, notice the function name baz?
includes a ?
character. This is a common convention in Elixir where a function that returns a boolean will include a ?
in the name, as opposed to other languages where it would be common to name it something like is_baz
.
Pattern matching
There is a lot to say about pattern matching, because it's kind of a big deal in Elixir. However, I'm going to keep it shallow.
Pattern matching in function arguments
The last example could be re-written as:
defmodule Foo do
def baz?("baz") do
"It's baz!"
end
def baz?(str) do
"It's NOT baz!"
end
end
Foo.baz?("baz") # "It's baz!"
Foo.baz?("bar") # "It's NOT baz!"
You can define the same function multiple times, and Elixir will execute the first one whose argument pattern matches. So, in the example above, the first function will only ever match when "baz"
is passed, and the second one will match any other argument.
Pattern matching variable assignment
a = 1
This looks like typical assignment as done in other languages, but in Elixir it is actually matching the pattern of the left-hand side and right-hand side and doing the assignments based on that. Here is an example that might help clarify what this means. Pay attention to how the left-hand and right-hand side match, and how assignment is done based off of that:
{a, b, c} = {1, 2, 3}
#=> a = 1, b = 2, c = 3
{i, j, 3} = {1, 2, 3}
#=> i = 1, j = 2
{x, y, 9} = {1, 2, 3}
#=> ** (MatchError) no match of right hand side value: {1, 2, 3}
Some more examples, note that assignment can only be done if the variable you want to assign is on the left:
x = 1
#=> 1
1 = x
#=> 1
2 = x
#=> ** (MatchError) no match of right hand side value: 1
2 = z
#=> ** (CompileError) undefined function z/0
# (there is no such import)
z = 2
#=> 2
List building and destructuring
If you are not familiar with linked lists this section might be slightly confusing, but let's give it a chance. Don't worry if it doesn't sink in right away.
A list is a singly linked-list with a tail and a head, [head | tail]
, where head is a single item, and tail is the rest of the list. e.g. Writing [1, 2, 3]
is the same as [1 | [2, 3]]
is the same as
[1 | [2 | [3 | []]]]
. A list is basically a recursive structure and the final tail is an empty list []
.
So, you can both build lists this way or pattern-match to destructure.
x = [1, 2, 3]
# Lets assign the head to `a` and the tail to `b`.
[a | b] = x
#=> a = 1
#=> b = [2, 3]
# Next, walking through the list.
[c | d] = b
#=> c = 2
#=> d = [3]
# Again, and the final tail is an empty list.
[e | f] = d
#=> e = 3
#=> f = []
# Now that `f` is an empty list, what do you think
# will happen if we try to get a head and tail?
[g | h] = f
#=> ** (MatchError) no match of right hand side value: []
# Elixir doesn't care if you do the following. It just returns the
# result `[]`, since it matches.
[] = f
#=> []
# There is no head or tail in an empty list, which is why
# matching on an empty list would be your recursive "base case"
# when using a list.
Collections
Elixir doesn't have loops, so iterating collections is done with recursion.
Recursion
In a recursive function, we will call the same function recursively until a condition is met.
defmodule Math do
@doc """
If you call the function with no arguments, we'll start
automatically from `0`.
This function would be referred to as `Math.count_to_ten/0` and
executed as `Math.count_to_ten()`.
"""
def count_to_ten do
count_to_ten(0)
end
@doc """
You can specify which number to start from.
We can add a `guard` on the function to make sure that the
starting number is less than or equal to `10`. This is the
`when` part. It is called a guard.
This function would be referred to as `Math.count_to_ten/1`.
"""
def count_to_ten(10), do: IO.puts(10)
def count_to_ten(from) when from < 10 do
IO.puts(from)
count_to_ten(from + 1)
end
end
Math.count_to_ten(8)
#=> 8
#=> 9
#=> 10
Note the multiple definitions of the same function, again. The first one that matches will be called.
You will see another (probably) new-to-you concept here. We can define a function without the do/end
if we use , do:
. This is good for short, single line functions.
Okay, but what happens if you want to count from a number greater than ten?
Math.count_to_ten(13)
** (FunctionClauseError) no function clause matching
in Math.count_to_ten/1
The following arguments were given to Math.count_to_ten/1:
# 1
13
Let's try and add the possibility of counting down to ten.
defmodule Math do
@doc """
If you call the function with no arguments, we'll start
automatically from `0`. This function would be referred
to as `Math.count_to_ten/0`.
"""
def count_to_ten do
count_to_ten(0)
end
@doc """
You can specify which number to start from.
We can add a `guard` on the function to make sure that the
starting number is less than or equal to `10`. This is the
`when` part. It is called a guard.
This function would be referred to as `Math.count_to_ten/1`.
"""
def count_to_ten(10), do: IO.puts(10)
def count_to_ten(from) when from < 10 do
IO.puts(from)
count_to_ten(from + 1)
end
# We're adding this function that matches the case when `from`
# is greater than `10`.
def count_to_ten(from) when from > 10 do
IO.puts(from)
count_to_ten(from - 1)
end
end
Math.count_to_ten(8)
#=> 8
#=> 9
#=> 10
Math.count_to_ten(12)
#=> 12
#=> 11
#=> 10
Since we were using guards we just needed to add another function that matched that requirement.
Since you've seen how we can match on head and tail in a list, let's try recursion with a list. Below, we will call the list recursively until the tail is empty, and then when sum/2
is called again,
the base case sum([], total)
will match first, and we will return the total.
defmodule Math do
@doc """
We will accept a list of numbers and start the recursion by
calling `sum/2` with the initial total `0`.
"""
def sum(numbers) when is_list(numbers) do
sum(numbers, 0)
end
# These are private functions, used to walk through the list
# until we match the "base case" with the empty list.
defp sum([], total), do: total
defp sum([n | rest], total) do
sum(rest, total + n)
end
end
Math.sum([1, 2, 3, 4, 5])
#=> 15
Built-in collection functions
Luckily, we have functions that should cover at least all of your basic needs in modules like Enum
, and List
.
You should be familiar with a lot of the functions, especially the most common ones like map
, filter
, and reduce
from your other languages. They are common in languages like PHP and JavaScript.
The most notable thing about these functions is that none of them will mutate the original collection. (Actually, no functions in elixir will, ever.)
Using a built-in map function to apply an anonymous function to each item in a collection:
JavaScript
let items = ["foo", "bar", "baz"]
items.map((item) => {
return item.toUpperCase()
})
// ["FOO", "BAR", "BAZ"]
PHP
$items = ["foo", "bar", "baz"];
array_map(function($item) {
return strtoupper($item);
}, $items);
// ["FOO", "BAR", "BAZ"]
Elixir
items = ["foo", "bar", "baz"]
Enum.map(items, fn item ->
String.upcase(item)
end)
# ["FOO", "BAR", "BAZ"]
FizzBuzz time
Okay, now let's combine some of what you've seen and add some more, and let's do a FizzBuzz in Elixir.
defmodule FizzBuzz do
@moduledoc """
The classic. FizzBuzz!
"""
@doc """
Performs the classic FizzBuzz problem.
## Example
iex> FizzBuzz.fizz_buzz(15)
["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz",
"Buzz", "11", "Fizz", "13", "14", "FizzBuzz"]
"""
@spec fizz_buzz(pos_integer()) :: [String.t()]
def fizz_buzz(up_to) do
Enum.map(1..up_to, &do_fizz_buzz/1)
end
defp do_fizz_buzz(n) do
case {rem(n, 3), rem(n, 5)} do
{0, 0} -> "FizzBuzz"
{0, _} -> "Fizz"
{_, 0} -> "Buzz"
{_, _} -> to_string(n)
end
end
end
FizzBuzz.fizz_buzz(15)
# Returns
["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz",
"11", "Fizz", "13", "14", "FizzBuzz"]
- You've seen modules before.
@moduledoc
is documentation for the module. You use markdown syntax and these will format nicely in docs and in IEx help functions.@doc
is for documenting functions. Like@moduledoc
you use markdown syntax. You can also use doctests when testing your modules and the example code will automatically be tested and asserted against the example results."""
is for multiline strings.- Introducing
@spec
. These are optional and define the types the function expects and returns and is used solely for documentation and static analysis tools (usually including LSP). - There is also a range from
1
toup_to
. A range is defined like1..3
and this covers the integers1
to3
, inclusive. There is additional notation forsteps
, so ranges can be pretty cool, and they are also more memory efficient than a list of integers. - The
&do_fizz_buzz/1
is using the capture operator for anonymous function short-hand. It is equivalent to
fn n ->
do_fizz_buzz(n)
end
- Next, you see a
case
statement which is similar to aswitch
statement in other languages, but on steroids because of pattern-matching. - Finally, the
_
are how you tell Elixir that the variable is unused. So, because it's a variable it will match any value and assign the value to that variable for the scope that it's in, but the_
also tells Elixir that the variable is unused and will be discarded.
Comprehensions
Comprehensions might have a similar feel to for loops (or for..in, or foreach) but they are not quite the same thing. Due to recursion and scope, for example, you can not iterate on a counter variable like you might be used to in non-functional languages.
items = ["foo", "bar", "baz"]
i = 0
for item <- items do
i = i + 1
{i, item}
end
# Returns
[
{1, "foo"},
{1, "bar"},
{1, "baz"}
]
# Notice `i` is not increasing?
A comprehension takes one (or more) generators (e.g. item <- items
), conditionals (e.g. item != "foo"
), and will return a list of the results of each execution.
Here are a couple of examples to get an idea of how they can work:
for x <- 0..10 do
x
end
# Returns
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for x <- 0..10, rem(x, 2) == 0 do
x
end
# Returns
[0, 2, 4, 6, 8, 10]
for x <- 0..10, y <- 1..3, rem(x, 2) == 1 do
{x, y}
end
# Returns
[
{1, 1},
{1, 2},
{1, 3},
{3, 1},
{3, 2},
{3, 3},
{5, 1},
{5, 2},
{5, 3},
{7, 1},
{7, 2},
{7, 3},
{9, 1},
{9, 2},
{9, 3}
]
There are other options you can use with comprehensions, and interesting ways to use them in other ways (like with binaries).
I won't really go further into this subject, since this is just an intro post. You will usually use functions, but there are some cases where comprehensions are the most clear and concise way, so don't underestimate them either.
Pipes
Pipes |>
might look odd at first, but they are actually very simple and very awesome.
All that you really need to know is that they take the result of one function and pass it as the first argument into the next function. If you are familiar with unix, then you should understand the idea of a pipe operator, it's the same idea.
For example, you could refactor this:
String.upcase("foo")
into:
"foo" |> String.upcase()
(don't actually do this for a single function)
The real beauty comes from the fact that you can chain pipes as much as you want. This gets rid of temporary variables and also improves readability significantly.
Here is another example refactor where I will add some chained pipes. Let's take this:
def add_space_and_upcase(str) do
split = String.codepoints(str)
spaced = Enum.join(split, " ")
String.upcase(spaced)
end
add_space_and_upcase("foobar") # "F O O B A R"
and make it much better:
def add_space_and_upcase(str) do
str
|> String.codepoints()
|> Enum.join(" ")
|> String.upcase()
end
add_space_and_upcase("foobar") # "F O O B A R"
I think it's clear to see how great this is.
Conclusion
Elixir clearly has some really great things going on, and we're not even scratching the surface here, as I've mentioned nothing about processes or OTP.
I hope however, that this has helped you to at least not have a mini-wtf-heartattack when you look at some elixir code and gets you interested to learn more and try it out.
If you think I should include anything else, or have any critiques, please feel free to comment below.