Pattern Matching in Elixir
Aug 01, 2016
Not long after you start learning Elixir, you'll run into odd statements claiming that Elixir doesn't have an assignment operator. Odd, I say, because such statements follow and are followed by code examples that look and behave like assignment operators (or impractical examples trying to "match" 1 to 2: 1 = 2
).
The way that I'd encourage you to approach Elixir's match operator is to realize that it behaves differently based on the types involved. And, in the case of a variable on the left side, that behaviour is an assignment. Beyond that, understanding the match operator's full potential requires the right examples.
First, consider a login page which, through some web framework, calls a method with a map hopefully containing an "email"
and "password"
field:
def create(%{"email" => e, "password" => p}) do
...
end
The above will only match when three conditions are met. First, the argument has to be a map. Second, it has to have an "email"
key. Third, it has to have a "password"
key. Put differently, when the left side of the match operator is a map, then it will only match another map that has at least the same keys. The behaviour on such a match is to deconstruct the map (into the e
and p
variables, for the above case).
If we try to call create
with something other than a map, or with a map that doesn't have both a "email"
and "password"
key, Elixir will raise an exception. Since we probably want something a little less dramatic for a web application, we'll add another method:
def create(_) do
end
It's important that we place this method last, else it'll match everything. If you're thinking that it would be nice if Elixir could properly prioritize these, consider that it's pretty trivial to get into ambiguous situations.
As another example, we have users which need to be handled differently based on their version
:
def load(id) do
load(Repo.get(User, id))
end
defp load(%User{version: 1} = user) do
end
defp load(%User{version: 2} = user) do
end
defp load(nil) do
end
The %{...} = user
syntax matches a map or structure and assigns the parameter to user
.
Matching also works with arrays, tuples and literals and it isn't restricted to method arguments. This final example demonstrates these capabilities. First, we have an array that can be empty, contain a user id, or contain a user id and a user name. We want to either do nothing, assign the user id to a map, or assign both the user id and user name to a map:
def load_context(header) do
load_context(%{}, header)
end
defp load_context(context, []) do
context
end
defp load_context(context, [id, name]) do
context = Map.put(context, :name, name)
load_cotext(context, id)
end
defp load_context(context, [id]), do
load_context(context id)
end
defp load_context(context, id) do
Map.put(context, :user_id, id)
end
It turns out that the "id" comes in as a string, but we want it as an integer. We'll rewrite that last method:
defp load_context(context, id) do
case Integer.parse(id) do
{id, _remainder} -> Map.put(context, :user_id, id)
:error -> context
end
end
Here we see matching with a tuple and an atom (aka, symbol / intern). What if we expected all our user ids to be suffixed with the letter "u"? We could rewrite that one case condition to be:
{id, "u"} -> Map.put(context, :user_id, id)
Matching is a powerful and it changes how you write and organize your code. The result tends to be very small and focused functions. This certainly isn't without its downsides, but overall I think it's a clear win.