Skip to content

Latest commit

 

History

History
375 lines (274 loc) · 8.09 KB

advanced_pattern_matching.livemd

File metadata and controls

375 lines (274 loc) · 8.09 KB

Pattern Matching

Mix.install([
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:tested_cell, github: "brooklinjazz/tested_cell"},
  {:utils, path: "#{__DIR__}/../utils"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.

Pattern Matching

We've already used pattern matching for matching values in a data structure.

[_, two, _] = [1, 2, 3]

two

We can use pattern matching in a wide variety of other cases.

Pattern Matching With Case

Previously, we talked about how the case statement checks for a match.

case "same" do
  "same" -> "this will return"
end

We're also able to use pattern matching for case statements. The first valid match will execute, and you can use the variables you assign in the instruction.

case [1, 2] do
  [] -> nil
  [one] -> one
  [one, two] -> one + two
end

Your Turn

Trigger the "A" case. Replace nil with your answer.

case nil do
  [_, _, 3] -> "A"
  _ -> "default"
end

Pattern Matching Multi-Clause Functions

You create multi-clause functions with pattern matching.

defmodule Greeter do
  def greet([name1, name2]) do
    "Hello, #{name1} and #{name2}"
  end

  def greet(%{name: name, identity: identity}) do
    "Hi #{identity} err..I mean #{name}"
  end

  def greet(name) do
    "Hello, #{name}"
  end
end

The function that first pattern matches with the provided input will be executed.

Greeter.greet("Peter")
Greeter.greet(["Peter", "Bruce"])
Greeter.greet(%{name: "Batman", identity: "Bruce Wayne"})

Your Turn

Use pattern matching to create a Hello.everyone/1 function which only accepts a list with three or more elements.

Example solution defmodule Hello do def everyone([_, _, _ | _tail]) do "Hello, everyone!" end end

Enter your solution below.

defmodule Hello do
  def everyone(list) do
  end
end

Pattern Matching in Anonymous Functions

We can use pattern matching in anonymous functions.

hello = fn
  "Peter" -> "Hey Spidey!"
  name -> "Hello, #{name}."
end

hello.("Bruce")
hello.("Peter")

Pattern matching becomes seriously powerful when leveraged with the Enum module.

list = [1, 2, 3]

Enum.map(list, fn
  1 -> "one"
  int -> int * 2
end)

Now we can enumerate and handle different patterns of data separately.

list = [{:add, 4}, {:subtract, 2}, {:multiply, 2}, 10, %{add: 2}]

Enum.reduce(list, 0, fn
  {:add, int}, acc -> acc + int
  {:subtract, int}, acc -> acc - int
  {:multiply, int}, acc -> acc * int
  %{add: int}, acc -> acc + int
  int, acc -> acc + int
end)

Your Turn

Use Enum.map/2 with pattern matching and multi-clause functions to double {:double, integer} tuples and half {:halve, integer} tuples in the following list.

[{:double, 2}, {:halve, 10}, {:double, 4}] -> [4, 5, 8]
Example Solution
Enum.map([{:double, 2}, {:halve, 10}, {:double, 4}], fn
  {:double, integer} -> integer * 2
  {:halve, integer} -> div(integer, 2)
end)
[{:double, 2}, {:halve, 10}, {:double, 4}]

Pattern Matching Vs. If

Often we have many tools to accomplish the same action. For example, let's say we're building an application where users send each other messages. However, only admin users are allowed to send messages.

Using if, we could write the following.

defmodule Message do
  def send(user, message) do
    if user.is_admin do
      message
    else
      {:error, :not_authorized}
    end
  end
end

Let's say we also need to handle empty messages.

defmodule Message do
  def send(user, message) do
    if user.is_admin do
      if message == "" do
        {:error, :empty_message}
      else
        message
      end
    else
      {:error, :not_authorized}
    end
  end
end

Message.send(%{is_admin: true}, "")

Nested if statements are generally a cue that we should consider an alternative implementation.

Let's see how we could solve this problem with pattern matching.

defmodule Message do
  def send(%{is_admin: true}, "") do
    {:error, :empty_message}
  end

  def send(%{is_admin: true}, message) do
    {:ok, message}
  end

  def send(%{is_admin: false}, _) do
    {:error, :not_authorized}
  end
end
Message.send(%{is_admin: true}, "")
Message.send(%{is_admin: false}, "Error!")
Message.send(%{is_admin: true}, "Successful!")

Pattern matching can help reduce the complexity of our control flow.

Anytime that we can use multiple function clauses, we can also use a case statement.

defmodule Message do
  def send(user, message) do
    case {user, message} do
      {%{is_admin: true}, ""} -> {:error, :empty_message}
      {%{is_admin: true}, message} -> message
      {%{is_admin: false}, _} -> {:error, :not_authorized}
    end
  end
end

Pin Operator

The pin operator allows us to use variables as hard-coded values, rather than rebinding a variable.

Often we use the pin operator when testing our code to assert that the value is correct.

For example, the following will rebind the received variable to [1, 2, 3].

received = [1, 2]
expected = [1, 2, 3]

received = expected

But instead, we might use the match operator to check that the received value matches the expected value.

received = [1, 2]
expected = [1, 2, 3]

^received = expected

By using the pin operator above, we accomplish the same as if we had written:

[1, 2] = [1, 2, 3]

We can also use this for internal values in a collection. The following is the same as [1, 2, 3] = [2, 2, 3]

first = 1
actual = [2, 2, 3]
[^first, 2, 3] = actual

And the following is the same as [1, 2, 3] = [1, 2, 3]

first = 1
actual = [1, 2, 3]
[^first, 2, 3] = actual

Your Turn

Use the pin operator to make the following code crash with a MatchError because expected does not match actual, rather than rebinding expected as it is currently doing.

Example Solution
expected = {"hello"}
actual = {"hello", "hi"}

^expected = actual
expected = {"hello"}
actual = {"hello", "hi"}

expected = actual

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Commit Your Progress

Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.

$ git add .
$ git commit -m "finish advanced pattern matching section"

Up Next

Previous Next
Polymorphism Treasure Matching