Skip to content

Instantly share code, notes, and snippets.

@mgwidmann
Created March 4, 2016 02:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgwidmann/3931314cf9f1d7df5706 to your computer and use it in GitHub Desktop.
Save mgwidmann/3931314cf9f1d7df5706 to your computer and use it in GitHub Desktop.
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