Pattern Matching a Function Body: Struct

Matching with a struct is just like matching with a map. The main differences with structs are:

  • the keys are always atoms
  • the compiler tells us if we get a key wrong
  • the struct type gives us another thing to match against

In fact, you can write the pattern matching solutions to the tests using only maps! With the test code available, this is a good chance to explore and understand how this works and why there is value in using structs when you can.

The project defines two structs for us to play with here. Feel free to check them out.

  • lib/pattern_matching/customer.ex
  • lib/pattern_matching/user.ex

You can also see a description of the structs in IEx. Start the IEx using iex -S mix to load the project into the IEx session. Using the IEx helper t(), it can describe a type for us.

$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> t PatternMatching.Customer
@type t() :: %PatternMatching.Customer{
        active: boolean(),
        contact_name: nil | String.t(),
        contact_number: nil | String.t(),
        location: nil | String.t(),
        name: String.t(),
        orders: list()
      }

iex(2)> t PatternMatching.User    
@type t() :: %PatternMatching.User{
        active: boolean(),
        admin: boolean(),
        age: nil | integer(),
        gender: nil | :male | :female,
        hair: nil | String.t(),
        name: String.t(),
        points: integer()
      }

There are some similarities between the Customer and User structs. They both have :name and :active keys. All the other fields are different.

Compile Time Checks

A benefit of using struct types in Elixir is that you get compile-time checks that the keys are correct.

When we use IEx, the Elixir commands we enter are interpreted at runtime, they aren’t compiled at build time. We still get errors for incorrect structs, but they look different. Let’s see an example of both:

Example: IEx – Interpreted. Started using iex -S mix.

alias PatternMatching.User
user = %User{car: "Toyota"}     
#=> ** (KeyError) key :car not found
#=>     (pattern_matching) expanding struct: PatternMatching.User.__struct__/1
#=>     iex:2: (file)

This is a runtime KeyError trying to use a key that doesn’t exist on the struct.

Example: Compiled. Assumes you modify the code to be invalid and then try to start the project, which compiles it in the process.

$ iex -S mix
Erlang/OTP 21 [erts-10.0.6] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Compiling 1 file (.ex)

== Compilation error in file lib/pattern_matching/structs.ex ==
** (CompileError) lib/pattern_matching/structs.ex:9: unknown key :car for struct PatternMatching.User
    lib/pattern_matching/structs.ex:9: (module)

This is helpful in catching problems ranging from a simple typo to changing a struct definition where a key was renamed or removed.

However, if I perform the match as a map instead of the struct, I have no protections or guarantees. This alone is a significant benefit. It’s the difference of including the struct name or not.

# This version is compile-time checked for Customer keys.
def do_work(%Customer{name: name}) do
  # work
end

# This version cannot be checked.
def do_work(%{name: name}) do
  # work
end

Guarantee it is the Struct

When a struct is part of the Pattern Match, it is a guarantee that the data coming in is that struct. Not just a map with similar keys, but it is that struct. All the code inside that function clause can be written confidently knowing it can’t be something else with a similar structure.

It is totally valid to define a function clause that matches the struct just to have that assurance and protection. Here’s an example:

def do_work(%Customer{} = customer) do
  # work
end

Inside this function clause I am guaranteed to have a Customer struct. This is opposed to a similar function declaration without it.

def do_work(customer) do
  # work
end

This function calls the argument customer but there is no guarantee it is a customer. The code would likely be written with the assumption it is a Customer struct but it implicitly depends on all callers to always only pass in a customer struct. When this function is passed something other than a customer, it will likely result in runtime errors.

Protects from Misspellings

Explicitly using a struct in a Pattern Match helps protect against misspelled keys. In the following example, I don’t use the struct type but instead use a map. I’ve accidentally misspelled name as nam.

def do_work(%{nam: name} = customer) do
  # Will never match a %Customer{}!
end

Calling this function and passing in a Customer struct won’t error, it will just never match! I’ve described a pattern that cannot match a Customer. When the Customer struct type is included, I get a compilation error for the invalid key.

Keep in mind that it is completely acceptable to intentionally use a map to match a struct. In this way my code can be somewhat polymorphic. I match on attributes of the data that multiple types can share. However, if I’m expecting the data to be Customer type, then it is preferable to explicitly declare that.

Practice Exercises

The following exercises continue using the Pattern Matching project. We will continue focusing on making a single test pass at a time.

The tests we are focusing on are in test/structs_test.exs. Running the following command will execute all the tests in this file. Running all the tests now will show they all fail.

Please review the solutions below even if you feel confident with your answer. You may find additional insight in the explanations.

$ mix test test/structs_test.exs

[...]

Finished in 0.05 seconds
9 tests, 9 failures

Randomized with seed 958439

Exercise #1 – Structs.get_name/1

In this exercise you write the function get_name/1 that takes both a User and Customer struct.

There are 2 tests for this function. One is the happy-path solution and the other handles when some other data type is given.

mix test test/structs_test.exs:25
mix test test/structs_test.exs:30

Make the tests pass by using pattern matching in the function declaration.

Exercise #2 – Structs.create_greeting/1

In this exercise you write the function create_greeting/1 that handles receiving a User and Customer struct differently.

There are 2 tests for this function. One is the happy-path solution that handles creating a customized greeting for a User or Customer struct. The other handles when the User or Customer being greeted is inactive.

mix test test/structs_test.exs:38
mix test test/structs_test.exs:45

Make the tests pass by using pattern matching in the function declaration.

Exercise #3 – Structs.deactivate_user/1

In this exercise you write the function deactivate_user/1 that handles receiving a User struct, modifying it and returning the modified struct.

There are 2 tests for this function. One is the happy-path solution that handles creating an updated User struct. The other handles when something other than a User is passed in.

mix test test/structs_test.exs:55
mix test test/structs_test.exs:60

Make the tests pass by using pattern matching in the function declaration.

Comments are closed

This is a static version of the site. Comments are not available.

15 Comments

  1. McKinnin Lloyd on January 27, 2020 at 10:04 pm

    I have a question. Why in the function get_name it will throw an error if you do %User{name: name} and not %{name: name}, but in the create_greeting function it is opposite. In that function it wants you to do %User{name: name}?

    • brainlid on January 28, 2020 at 6:20 am

      That’s a good question! In the get_name example, we want the pattern to be less specific. By not specifying the struct type, we match on any map data type (including structs) but only if they have a key called :name. For get_name, it would also be valid to write this:

      def get_name(%Customer{name: name}), do: {:ok, name}
      def get_name(%User{name: name}), do: {:ok, name}
      def get_name(_other), do: {:error, "Doesn't have a name"}

      This is more specific. We give an explicit pattern match on a customer and user struct. However, the results of the functions are identical. It doesn’t actually matter that it’s a Customer or a User struct type. It is better to match more generally to only the data that we actually care about for getting the :name from the data.

      Then in the create_greeting function we want to be more specific with our match because we want a different result based on the data type.

      Another way to describe the behavior in get_name is like this: “if the data has a key called :name then return the value”. It doesn’t care about what kind of struct. Then create_greeting is saying “if it is a customer then greet this way. If it is a user then greet this other way.” The best way to tell them apart is by their struct types.

      Did I answer your question?

      • Marcus West on June 28, 2024 at 5:57 am

        I am really struggling with Q1. Your solution renders an error, and requires me to offer the more comprehensive solution employing User & Customer. When I do this, (as above), I get another error:

        “error: PatternMatching.User.__struct__/0 is undefined, cannot expand struct PatternMatching.User. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code

        18 │ def get_name(%User{name: name}), do: {:ok, name}
        │ ^

        └─ structs.ex:18:16: PatternMatching.Structs.get_name/1

        ** (CompileError) structs.ex: cannot compile module PatternMatching.Structs (errors have been logged)
        structs.ex:18: (module)”

        • Mark Ericksen on June 28, 2024 at 6:09 am

          The error `PatternMatching.User.__struct__/0 is undefined` usually means you are missing an `alias` statement. The compiler is saying, “I don’t know what struct you’re talking about.” The statement is `alias PatternMatching.User`.

          • Marcus West on June 29, 2024 at 7:44 am

            Hi Mark. I am not seeing that.

            defmodule PatternMatching.Structs do
            @moduledoc “””
            Fix or complete the code to make the tests pass.
            “””
            alias PatternMatching.Customer
            alias PatternMatching.User

            def get_name(%{name: name}), do: {:ok, name}
            def get_name(_other), do: {:error, “Doesn’t have a name”}

            def create_greeting(_value) do
            end

            def deactivate_user(_user) do
            end
            end

            warning: unused alias Customer

            13 │ alias PatternMatching.Customer
            │ ~

            └─ structs.ex:13:3

            warning: unused alias User

            14 │ alias PatternMatching.User
            │ ~

            └─ structs.ex:14:3



          • Mark Ericksen on June 29, 2024 at 10:51 am

            Hi Marcus! I’m not sure what the problem is. Your solution here is correct and the tests in Exercise 1 pass correctly. I’m not seeing a problem. The compiler warnings are just that the aliases weren’t used, which is fine at this point in the exercise.

            Is there something else that you’re seeing?



  2. romenigld on December 14, 2020 at 7:42 am

    I loved this example.
    It’s like when you will refactor the code.
    You match on any map data type (including structs) and has less lines of code to write and understand.
    Thank’s!

  3. Mark Johnson on February 22, 2021 at 1:22 am

    Another really great exercise. Thank you for taking the effort to do this for all of us trying to learn

  4. Alister Sibbald on April 14, 2021 at 6:50 am

    Great lessons – Thanks! I’m really internalising the Elixir way thanks to these!

    Could I ask you how you got the great colorization of IEX as seen in the first code example on this page?

    • Mark Ericksen on April 14, 2021 at 7:15 am

      For the colors, on this page it’s just text using a web syntax highlighter for the Elixir language. So it didn’t come out of my IEx terminal looking like that. Sorry!

  5. Pavel Delgado on August 29, 2021 at 1:26 pm

    Hi ! What is the main difference between using %{key: value} and %{key => value} ? . Thanks for the great lessons.

    • Mark Ericksen on August 30, 2021 at 5:33 am

      When the key is an atom, it can use the %{key: value} form. When the key is a string or other data structure, it uses the “arrow” form. It is also valid to write %{:key => 1}. When you write that in IEx, you will see it returned as %{key: 1}. So it’s really “syntactic sugar” that makes maps more readable when the key is an atom. It was initially covered here: https://thinkingelixir.com/course/pattern-matching/module-2/map/

  6. Takudzwa Gwindingwi on November 2, 2021 at 11:03 am

    Loving this course. Also enjoying reading the comments. Could you kindly explain how we were using the Customer and User structs in the structs.ex when they are defined in their respective files.

    • Mark Ericksen on November 4, 2021 at 6:55 am

      I think you are asking how the structs can be referenced without something like an “include” or “require”? That’s at least the question I’ll try to answer. In Elixir, all modules and types are available globally. There is no need to “require” or “import” or anything like that. What keeps them separate and organized is their namespace. So you could reference a struct using it’s full name like this: %PatternMatching.Customer{} at anytime anywhere in your code. However, that can be awkward and verbose. If you have a file that repeatedly deals with that struct, it’s common to “alias” it at the top of the file. This only creates a shortcut to the name you want to reference inside the module you are writing.

      In the code and test files, it looks like this: alias PatternMatching.{Customer, User}. Aliasing multiple things at a time like this (using the {}) is discouraged. It’s considered a bad practice because it’s confusing and doing a Find/Replace doesn’t work well.

      I had forgotten that the examples used this approach and I should update them to be a better example. It should instead be like this:

      alias PatternMatching.Customer
      alias PatternMatching.User
      

      I hope that answered the question you were asking!

  7. Maksym Kosenko on December 5, 2021 at 5:59 am

    Thanks Mark! These’re great explanations of Struct. I like the way of studying through examples ⛰️

Comments are closed on this static version of the site.