Created
March 4, 2016 02:55
-
-
Save mgwidmann/3931314cf9f1d7df5706 to your computer and use it in GitHub Desktop.
Simple example showcasing the power of pattern matching.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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