The Pattern Matching Approach
Elixir Ruby Vinicius NegrisoloMy goal here:
How pattern matching affects coding in Elixir? Let’s talk about Elixir pattern matching and how this would change the way we solve problems.
What is Pattern Matching?
- destructured assignment
- validation by raises
MatchError
- right to left <=
Example
- destructured assignment
- validation by raises
MatchError
- right to left <=
user = %User{id: 1}
result = {:ok, user}
{:ok, %{id: id}} = result
id
#=> 1
user = %User{id: 1}
result = {:ok, user}
{:error, message} = result
#=> ERROR** (MatchError)
#=> no match of right hand side value:
#=> {:ok, %{id: 1}}
More Examples
# Map
%{} = %{key: :value}
# Struct
%{id: 1} = %User{id: 1}
# Tuple
{:ok, _status} = {:ok, "Processed"}
# List
[:apple | [:pear]] = [:apple, :pear]
# Order matters
%{k1: 1, k2: 2} = %{k2: 2, k1: 1}
# Map
%{key: :value} = %{}
# Struct
%User{id: 1} = %{id: 1}
# Tuple
{:ok} = {:ok, "Processed"}
# List
[:pear | _rest] = [:apple, :pear]
# Order matters
[:pear, :apple] = [:apple, :pear]
Check against variables
- default to assign
%{id: user_id} = %User{id: 1}
user_id #=> 1
^
the pin operator
user_id = 1
%{id: ^user_id} = %User{id: 1}
Pin operator take aways
- gives more power
- complicates syntax
^
- NO pin for nested
^user.id
=>CompileError
user = %{id: 6}
%{id: ^user.id} = %User{id: 6}
#=> ERROR** (CompileError)
#=> invalid argument for unary operator ^,
#=> expected an existing variable, got: ^user.id()
- allow error swallowing
user_id = 5
%{id: user_id} = %User{id: 1}
user_id
#=> 1
Check against module attributes
- @var is not assignable, it’s “definable”
defmodule User do
@type "User"
end
- module attrs are “pinned” already
defmodule User do
@admin_id 1
def admin?(%{id: @admin_id}), do: true
def admin?(_), do: false
end
User.admin?(%{id: 1})
#=> true
Where to use Pattern Matching?
Equals operator
%{id: _} = %User{id: 1}
- could raise
MatchError
Where to use Pattern Matching?
Function definition
def validate(%{id: nil}), do: {:error, "id is nil"}
def validate(%{id: _id_}), do: :ok
def validate(_), do: {:error, "missing id"}
- could raise
FunctionClauseError
Where to use Pattern Matching?
case
statement
case Repo.insert(%User{name: "John"}) do
{:ok, _struct} -> :ok
{:error, %{errors: errors}} -> {:error, errors}
{:error, %{}} -> {:error, "something is wrong"}
end
- could raise
CaseClauseError
- new syntax element:
->
Where to use Pattern Matching?
with
statement
with changeset = User.insert_changeset(params),
{:ok, user} <- Repo.insert(changeset),
{:ok, token} <- Security.generate_token(user),
:ok <- send_user_email(user, token) do
put_flash(conn, :info, "Success!")
else
{:error, %Changeset{data: %User{}}} -> put_flash(conn, :error, "Failed to save user")
{:error, :user_email} -> put_flash(conn, :error, "Failed to email to the user")
_ -> put_flash(conn, :error, "Something failed")
end
- could raise
MatchError
- could raise
WithClauseError
- new syntax element:
<-
Exercise
- classifying a workout
- list of intervals
- find the workout mode/type
Exercise v1
Input => list of:
%{type: "dist", value: 500}
=> 500 meters%{type: "time", value: 600}
=> 10 min
workout = [
%{type: "dist", value: 500},
%{type: "dist", value: 1000},
%{type: "time", value: 60},
]
Output:
rule | mode |
---|---|
if all same type dist |
FixedDistSplits |
if all same type time |
FixedTimeSplits |
if any different | VariableInterval |
Exercise v1
Elixir
Ruby
- recursion
- pattern matching
- guards
- enumerable methods
- temporary variables / private functions
- conditions
Exercise v1
defmodule ErgZone.WorkoutMode do
def run([%{type: "dist"}]) do
"FixedDistSplits"
end
def run([%{type: "time"}]) do
"FixedTimeSplits"
end
def run([
%{type: t}
| [%{type: t} | _] = tail
]) do
run(tail)
end
def run(_intervals) do
"VariableInterval"
end
end
class ErgZone::WorkoutMode do
def self.run(intervals)
new(intervals).run
end
def initialize(intervals)
@intervals = intervals
@first_type = intervals[0].type
end
attr_reader :intervals, :first_type
def run
return "VariableInterval" unless same_type?
case first_type
when "dist" then "FixedDistSplits"
when "time" then "FixedTimeSplits"
end
end
def same_type?
intervals.all? do |interval|
interval.type == first_type
end
end
end
Exercise v2
New Input => list of:
%{type: "dist", value: 500, rest: 30}
=> 500 meters and rest for 30s%{type: "time", value: 600, rest: 0}
=> 10 min
New Output:
rule | mode |
---|---|
if all same type dist and no rest |
FixedDistSplits |
if all same type dist and rest |
FixedDistInterval |
if all same type time and no rest |
FixedTimeSplits |
if all same type time and rest |
FixedTimeInterval |
if any different | VariableInterval |
Exercise v2 diff
defmodule ErgZone.WorkoutMode do
- def run([%{type: "dist"}]) do
+ def run([%{type: "dist", rest: 0}]) do
"FixedDistSplits"
end
- def run([%{type: "time"}]) do
+ def run([%{type: "dist", rest: _}]) do
+ "FixedDistInterval"
+ end
+
+ def run([%{type: "time", rest: 0}]) do
"FixedTimeSplits"
end
+ def run([%{type: "time", rest: _}]) do
+ "FixedTimeInterval"
+ end
+
def run([
- %{type: t}
- | [%{type: t} | _] = tail
+ %{type: t, rest: r}
+ | [%{type: t, rest: r} | _] = tail
]) do
run(tail)
end
def run(_intervals) do
"VariableInterval"
end
end
class ErgZone::WorkoutMode do
def self.run(intervals)
new(intervals).run
end
def initialize(intervals)
@intervals = intervals
@first_type = intervals[0].type
+ @first_rest = intervals[0].rest
end
- attr_reader :intervals, :first_type
+ attr_reader :intervals, :first_type, :first_rest
def run
- return "VariableInterval" unless same_type?
-
- case first_type
- when "dist" then "FixedDistSplits"
- when "time" then "FixedTimeSplits"
+ if same_type? && same_rest?
+ if first_type == "dist"
+ if with_rest?
+ "FixedDistInterval"
+ else
+ "FixedDistSplits"
+ end
+ else
+ if with_rest?
+ "FixedTimeInterval"
+ else
+ "FixedTimeSplits"
+ end
+ end
+ else
+ "VariableInterval"
end
end
def same_type?
intervals.all? do |interval|
interval.type == first_type
end
end
+
+ def same_rest?
+ intervals.all? do |interval|
+ interval.rest == first_rest
+ end
+ end
+
+ def with_rest?
+ first_rest > 0
+ end
end
Exercise v2
defmodule ErgZone.WorkoutMode do
def run([%{type: "dist", rest: 0}]) do
"FixedDistSplits"
end
def run([%{type: "dist", rest: _}]) do
"FixedDistInterval"
end
def run([%{type: "time", rest: 0}]) do
"FixedTimeSplits"
end
def run([%{type: "time", rest: _}]) do
"FixedTimeInterval"
end
def run([
%{type: t, rest: r}
| [%{type: t, rest: r} | _] = tail
]) do
run(tail)
end
def run(_intervals) do
"VariableInterval"
end
end
class ErgZone::WorkoutMode do
def self.run(intervals)
new(intervals).run
end
def initialize(intervals)
@intervals = intervals
@first_type = intervals[0].type
@first_rest = intervals[0].rest
end
attr_reader :intervals, :first_type, :first_rest
def run
if same_type? && same_rest?
if first_type == "dist"
if with_rest?
"FixedDistInterval"
else
"FixedDistSplits"
end
else
if with_rest?
"FixedTimeInterval"
else
"FixedTimeSplits"
end
end
else
"VariableInterval"
end
end
def same_type?
intervals.all? do |interval|
interval.type == first_type
end
end
def same_rest?
intervals.all? do |interval|
interval.rest == first_rest
end
end
def with_rest?
first_rest > 0
end
end
Exercise v3
New Input => list of:
%{type: "dist", value: 500, rest: 30}
=> 500 meters and rest for 30s%{type: "time", value: 600, rest: 0}
=> 10 min
New Output:
rule | mode |
---|---|
if all same type dist , value [1] and no rest |
FixedDistSplits |
if all same type dist , value [1] and rest [2] |
FixedDistInterval |
if all same type time , value [1] and no rest |
FixedTimeSplits |
if all same type time , value [1] and rest [2] |
FixedTimeInterval |
if any different | VariableInterval |
- last
value
could be smaller (chill out) - last
rest
could be smaller, possibly 0
Exercise v3 diff
defmodule ErgZone.WorkoutMode do
- def run([%{type: "dist", rest: 0}]) do
+ def run([
+ %{type: "dist", value: v1, rest: 0},
+ %{type: "dist", value: v2, rest: 0}
+ ])
+ when v1 >= v2 do
"FixedDistSplits"
end
- def run([%{type: "dist", rest: _}]) do
+ def run([
+ %{type: "dist", value: v1, rest: r1},
+ %{type: "dist", value: v2, rest: r2}
+ ])
+ when v1 >= v2 and r1 >= r2 do
"FixedDistInterval"
end
- def run([%{type: "time", rest: 0}]) do
+ def run([
+ %{type: "time", value: v1, rest: 0},
+ %{type: "time", value: v2, rest: 0}
+ ])
+ when v1 >= v2 do
"FixedTimeSplits"
end
- def run([%{type: "time", rest: _}]) do
+ def run([
+ %{type: "time", value: v1, rest: r1},
+ %{type: "time", value: v2, rest: r1}
+ ])
+ when v1 >= v2 and r1 >= r2 do
"FixedTimeInterval"
end
def run([
- %{type: t, rest: r}
- | [%{type: t, rest: r} | _] = tail
+ %{type: t, value: v, rest: r}
+ | [%{type: t, value: v, rest: r} | _] = tail
]) do
run(tail)
end
def run(_intervals) do
"VariableInterval"
end
end
class ErgZone::WorkoutMode do
def self.run(intervals)
new(intervals).run
end
def initialize(intervals)
- @intervals = intervals
- @first_type = intervals[0].type
- @first_rest = intervals[0].rest
+ @first_interval = intervals[0]
+ @intervals = intervals[0..-2]
+ @last_interval = intervals[-1]
end
- attr_reader :intervals, :first_type, :first_rest
+ attr_reader :first_interval, :intervals, :last_interval
def run
- if same_type? && same_rest?
- if first_type == "dist"
+ if same_type? && same_value? && same_rest? && first_bigger_than_last?
+ if first_interval.first_type == "dist"
if with_rest?
"FixedDistInterval"
else
"FixedDistSplits"
end
else
if with_rest?
"FixedTimeInterval"
else
"FixedTimeSplits"
end
end
else
"VariableInterval"
end
end
def same_type?
intervals.all? do |interval|
interval.type == first_type
end
end
+ def same_value?
+ intervals.all? do |interval|
+ interval.value == first_value
+ end
+ end
+
def same_rest?
intervals.all? do |interval|
interval.rest == first_rest
end
end
+ def first_bigger_than_last?
+ first_interval.value >= last_interval.value &&
+ first_interval.rest >= last_interval.rest
+ end
+
def with_rest?
first_rest > 0
end
end
Exercise v3
defmodule ErgZone.WorkoutMode do
def run([
%{type: "dist", value: v1, rest: 0},
%{type: "dist", value: v2, rest: 0}
])
when v1 >= v2 do
"FixedDistSplits"
end
def run([
%{type: "dist", value: v1, rest: r1},
%{type: "dist", value: v2, rest: r2}
])
when v1 >= v2 and r1 >= r2 do
"FixedDistInterval"
end
def run([
%{type: "time", value: v1, rest: 0},
%{type: "time", value: v2, rest: 0}
])
when v1 >= v2 do
"FixedTimeSplits"
end
def run([
%{type: "time", value: v1, rest: r1},
%{type: "time", value: v2, rest: r1}
])
when v1 >= v2 and r1 >= r2 do
"FixedTimeInterval"
end
def run([
%{type: t, value: v, rest: r}
| [%{type: t, value: v, rest: r} | _] = tail
]) do
run(tail)
end
def run(_intervals) do
"VariableInterval"
end
end
class ErgZone::WorkoutMode do
def self.run(intervals)
new(intervals).run
end
def initialize(intervals)
@first_interval = intervals[0]
@intervals = intervals[0..-2]
@last_interval = intervals[-1]
end
attr_reader :first_interval, :intervals, :last_interval
def run
if same_type? && same_value? && same_rest? && first_bigger_than_last?
if first_interval.first_type == "dist"
if with_rest?
"FixedDistInterval"
else
"FixedDistSplits"
end
else
if with_rest?
"FixedTimeInterval"
else
"FixedTimeSplits"
end
end
else
"VariableInterval"
end
end
def same_type?
intervals.all? do |interval|
interval.type == first_type
end
end
def same_value?
intervals.all? do |interval|
interval.value == first_value
end
end
def same_rest?
intervals.all? do |interval|
interval.rest == first_rest
end
end
def first_bigger_than_last?
first_interval.value >= last_interval.value &&
first_interval.rest >= last_interval.rest
end
def with_rest?
first_rest > 0
end
end
Summary
- Elixir syntax might be a challenge
- recursion is not scary with PM
- pattern matching rules!