I recently started learning Elixir for an upcoming project. As part of my usual prep work, I have studied the language documentation, walked through the Koans, and practiced some katas on Exercism.io.
The end result is a few changes to how I approach problem solving, that I want to share with you.
Before we jump in, some things you might find confusing are:
1) Function signatures include the number of parameters that they take, referred to as arity. A function signature any?(collection, fn) would be referred to as any?/2.
2) Unused parameters are named but are prefixed with an underscore.
3) iex is the name of the REPL in Elixir.
Pattern matching and Decomposition
Pattern matching as a first class idea is new to me. Usually I would avoid case statements and destructuring, and favor polymorphism, but I have begun to play with some of the interesting cases that are possible.
Let's look at an example. Here I match the variable a to a pattern of a string literal.
iex> a = "Some String" # Binds string to a
"Some String"
iex> "Some String" = a # Matches a to the pattern
"Some String"
Pattern matching throws an error when there is not a match.
iex> "Not a Match" = a # Throws an error
** (MatchError) no match of right hand side value: "Some String"
It is possible to destructure lists using pattern matching. Here you can see how to separate the first element of a list from the rest of it.
iex> [head | tail] = [1, 2, 3, 4, 5]
iex> head
1
iex> tail
[2, 3, 4, 5]
You can also destructure more complex data types. For instance, grabbing a key out of a map:
iex> user = %{id: 1, login: %{user_name: 'bob114', password: 'Apple12Sauce!'}}
iex> %{id: id} = user
iex> id
1
iex> %{login: %{user_name: user_name}} = user
iex> user_name
bob114
Recursion First
I had to adapt my style to embrace recursion. That last time that I wrote so much recursive code was in a computer science course. This is a drastic style change but would be best demonstrated through an example of navigating collections.
Let's create a function to determine if any element is contained in a collection, and name the function any?/2.
I start by asking, "What is the base case for any?/2?".
It would be when the list parameter is empty. Using pattern matching we can define a function header that will match the scenario where the first element passed is an empty list, and return false.
defmodule Example do
def any?([], _a), do: false # Matches when the first param is an empty list
end
Next I ask myself, "What could be the recursive case?"
When the list is not empty, I try to divide the problem into smaller parts, allowing me to hit the base cases. There are two possibilities, 1) the first element is equal to the variable, 2) it isn't and we need to keep looking. If we implement scenario one, then we have the head equal to a, so we return true.
defmodule Example do
def any?([], _a), do: false
def any?([head | _tail], a) do
if head == a do
true
end
end
end
In the last scenario we deal with recursing the rest of the list.
defmodule Example do
def any?([], _a), do: false
def any?([head | tail], a) do
if head == a do
true
else
any?(tail, a)
end
end
end
Maintaining state
In Elixir state is managed inside of processes. Each process emulates state transitions by using recursion. Processes allow their state to be changed or queried by other processes via messages. Rather than show you the low level tooling (spawn, Agent, or Task), the most high-level construct I am aware of is the GenServer.
GenServer is a core language construct that wraps all the complexity around a process sending and receiving messages. It is a module that has two types of functions, asynchronous and synchronous.
Synchronous functions are calls, they will reply to the calling process with some data. They do this by returning a tuple like {:reply, data, state}. The :reply tells the server to respond to the caller, the data is what to send in the reply, and the state is the new state for the process.
defmodule OurServer do
use GenServer
...
def call({:message, caller_data}, _from, state) do
{:reply, caller_data, [caller_data]}
end
end
Asynchronous functions are casts, they do not reply to callers. They return a tuple like {:noreply, state}. :noreply tells the server not to reply, and state is the new state for the process.
defmodule OurServer do
use GenServer
def cast({:message, caller_data}, state) do
{:noreply, caller_data}
end
end
In future articles, I plan to discuss GenServers more deeply, but that's all for now.
Conclusion
I am venturing out into the wilderness of Functional Languages by trying Elixir for the first time.
The things I found the most interesting on week one are:
- Pattern matching
- Recursion first
- Emulating state with processes
If you enjoyed this article and want to read more about Elixir, please leave a comment or a like below. Thanks!