Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple example showcasing the power of pattern matching.
# Lets create a bartender in the same way you would in a imperative language.
defmodule Bartender do
def serve(user, drink) do
if user.age >= 21 do
IO.puts "The bartender slides #{user.name} a #{drink}."
else
IO.puts "Sorry #{user.name}, you'll have to wait #{21 - user.age} more year(s)."
end
end
end
# And we'd call it like this
Bartender.serve(%{name: "Jimmy", age: 25}, :martini)
# Output:
# The bartender slides Jimmy a martini.
# But what happens if the data is not what we expected. For example:
Bartender.serve([%{name: "Jimmy", age: 25}], :martini)
# Output:
# ** (ArgumentError) argument error
# :erlang.apply([%{age: 25, name: "Jimmy"}], :age, [])
# Bartender.serve/2
# In statically typed systems this obviously wouldn't compile. But additionally,
# in most statically typed systems we can overload the same method to accept a
# different type, in this case a list. We can do the same, but better, with pattern matching.
defmodule Bartender do
def serve([user | users], drink) do # Also asserts that there is at least one element in the list!
serve(user, drink) # This calls the method below, unless this is also a list!
serve(users, drink)
end
def serve(user, drink) do
# Unchanged...
end
end
# Instead of using a plain map. We can use Elixir's structs to make users have their own type.
defmodule User do
defstruct name: "Unknown", age: 0 # We give each field a default value
end
# Then we can change the code to pattern match on that type
defmodule Bartender do
def serve([user = %User{} | users ], drink) do
serve(user, drink)
serve(users, drink)
end
def serve(user = %User{}, drink) do
# Unchanged..
end
end
# This means if someone calls without passing a User object,
# it will throw an exception (at runtime unfortunately :( )
Bartender.serve(%{name: "Jimmy", age: 25}, :martini)
# Output:
# ** (FunctionClauseError) no function clause matching in Bartender.serve/2
# iex:6: Bartender.serve(%{age: 25, name: "Jimmy"}, :martini)
# But what if our data is totally bogus
Bartender.serve(%{name: "Jimmy", age: fn -> :haha end}, :martini)
# Output:
# The bartender slides Jimmy a martini.
# http://elixir-lang.org/getting-started/basic-operators.html
# We can improve the main function definition that does all the work.
def serve(%User{age: age, name: name}, drink) when is_number(age) and age >= 21 do
IO.puts "The bartender slides #{name} a #{drink}."
end
def serve(%User{age: age, name: name}, drink) when is_number(age) do
IO.puts "Sorry #{name}, you'll have to wait #{21 - age} more year(s)."
end
# Now if the age is bogus
Bartender.serve(%User{name: "Jimmy", age: fn-> :hi end}, :martini)
# Output:
# ** (FunctionClauseError) no function clause matching in Bartender.serve/2
# iex:11: Bartender.serve(%User{age: #Function<20.54118792/0 in :erl_eval.expr/5>, name: "Jimmy"}, :martini)
# Lastly if we want to handle this case or the empty list case, we can.
defmodule Bartender do
def serve([], _drink), do: IO.puts "Sure thing."
def serve([user = %User{} | users ], drink) do
serve(user, drink)
serve(users, drink)
end
def serve(%User{age: age, name: name}, drink) when is_number(age) and age >= 21 do
IO.puts "The bartender slides #{name} a #{drink}."
end
def serve(%User{age: age, name: name}, drink) when is_number(age) do
IO.puts "Sorry #{name}, you'll have to wait #{21 - age} more year(s)."
end
def serve(_user, _drink), do: IO.puts "I'm sorry, but we just don't serve your type here."
end
# Now for all inappropriate data we are handling it in a separate part of our code. We can choose
# to raise an exception, handle it ourselves or even let it crash at runtime (see below for build
# time exceptions, however, crashing at runtime is acceptable in Elixir because of Supervisors).
Bartender.serve(%{name: "Jimmy", age: 25}, :martini)
# I'm sorry, but we just don't serve your type here.
Bartender.serve(:no_one, :nothing)
# I'm sorry, but we just don't serve your type here.
Bartender.serve([], :anything)
# Sure thing.
# Mixed data in a list will produce mixed results
Bartender.serve([%User{name: "Jimmy", age: 25}, :no_one], :martini)
# Output:
# The bartender slides Jimmy a martini.
# I'm sorry, but we just don't serve your type here.
# This is really powerful, but the one thing a statically typed system has over this
# is spotting type mismatches at build time. The Erlang VM has that covered though,
# through a tool called diaylzer which comes with Erlang. Elixir has a third party
# package that makes the output more friendly to Elixir developers here:
# https://github.com/fishcakez/dialyze
# It can do some pretty amazing things in addition to just checking types. For example,
# checking for race conditions, functions that never return, improperly constructed lists,
# unused functions, patterns that will never match and several more.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.