The Pattern Matching Approach

Elixir Ruby Vinicius Negrisolo Vinicius Negrisolo

My 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
  1. last value could be smaller (chill out)
  2. 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!

Questions?