Pattern Matching a Function Body: Maps

Maps are your go-to key-value collections. You are likely to use them a lot. When building a web application, the request data submitted by a client is represented as a map with string keys. Before we jump into practicing how we pattern match maps in functions, this is a good time to talk about a code pattern you will see in Elixir.

In Pieces and Whole

There is a common code pattern we use in Elixir that is worth exploring here. Let’s look at a code example and talk about it.

web_params = %{"name" => "John", "email" => "john@example.com"}

defmodule Testing do
  def do_work(%{"email" => email} = params) do
    IO.inspect email
    IO.inspect params
    "Sent an email to #{email} addressed to #{params["name"]}"
  end
end

Testing.do_work(web_params)
#=> "john@example.com"
#=> %{"email" => "john@example.com", "name" => "John"}
#=> "Sent an email to john@example.com addressed to John"

In this code, we have a string-key map called params similar to what you’d get when handling a web request. The do_work function takes the params in and does a pattern match on some of the data. It looks like this: do_work(%{"email" => email} = params). What’s special here is that we are pattern matching for some data we care about and still binding everything passed in as params.

This is a common code pattern. You can use this same approach everywhere you pattern match. It works in case statements and even in a straight forward match expression. Check this out:

%{"email" => email} = params = %{"name" => "John", "email" => "john@example.com"}

email
#=> "john@example.com"
params
#=> %{"email" => "john@example.com", "name" => "John"}

Yes, this may look strange, but when you break it down you can see what’s happening. The match operator performs a Pattern Match of the left and right sides. When we say params by itself, we haven’t defined a type or a data shape so it just gets bound.

This is effectively what we’re doing with the map match in the function. We still bind everything to the variable params but we also define the pattern (data type and shape) which also pulls out the data we care most about.

This can be very helpful like in my hypothetical function below. I want to Pattern Match and access a portion of the data when it is passed in, but I still want the full customer data because I pass it on to another function called notify_customer.

defmodule Billing do
  def apply_charge(%{id: customer_id} = customer, charge) do
    record_charge(customer_id, charge)
    notify_customer(customer, charge)
  end
end

Still Naming for Clarity

You’ve seen how we can name an argument for developer clarity and documentation reasons using the underscore prefix like _customer. We can do that when matching map arguments. When I don’t need the full data structure, it still adds value to name it for developer clarity and expressing the intent of the function.

defmodule Testing do
  def do_work(%{"email" => email} = _customer_params) do
    # use `email`
  end
end

Having the argument named _customer_params conveys meaning for what the developer was expecting to be passed in. That meaning may not be clear from just looking at a function declaration that says, “do_work(%{"email" => email}) do“. A map with an email string key could be for a user, customer, a login, or something else.

Be kind to your future self an any other developers that work on your code base. Name your variables even when the code doesn’t need it.

Binding to a Nested Map

Maps are the go-to data structure for key-value data. They also elegantly handle nested data. When pattern matching, it is important to understand that you can also easily bind to a nested map. This can be very handy when working with web requests, and actually gets used frequently.

An example will help demonstrate this. First, we need some nested data to pattern match against. Remember, in this example we are focusing on the function declaration and the pattern match it uses.

params = %{
  "customer" => %{
    "id" => 123,
    "name" => "Willy Wonka Chocolates",
    "bonuses" => %{
      "employees" => %{
        "Oompa 1" => 1_000,
        "Oompa 2" => 2_000,
        "Hillary" => 1_500,
        "Oompa 3" => 500
      },
      "total" => 5_000
    }
  }
}

defmodule NestedBinding do

  def award_bonuses(%{"customer" => %{"bonuses" => %{"total" => bonus_total} = bonuses}} = _params) do
    IO.inspect bonus_total, label: "TOTAL TO VALIDATE"
    IO.inspect bonuses, label: "BONUSES"
    # TODO: validate intended total and employee amounts
    :ok
  end
end

NestedBinding.award_bonuses(params)
#=> TOTAL TO VALIDATE: 5000
#=> BONUSES: %{
#=>   "employees" => %{
#=>     "Hillary" => 1500,
#=>     "Oompa 1" => 1000,
#=>     "Oompa 2" => 2000,
#=>     "Oompa 3" => 500
#=>   },
#=>   "total" => 5000
#=> }
#=> :ok

The pattern is where we are focusing right now:

%{"customer" => %{"bonuses" => %{"total" => bonus_total} = bonuses}} = _params

Notice that we can bind to a deeply nested map to get the bonus_total and bind a variable to the full bonuses map at the same time!

The ability to bind to a nested piece of data works on all data types. However, it is most helpful when working with maps, primarily because maps tend to have nested data more than other data types.

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/maps_test.exs. Running the following command will execute all the tests in this file. Running all the tests now will show they all fail.

$ mix test test/maps_test.exs

[...]

Finished in 0.05 seconds
9 tests, 9 failures

Randomized with seed 958439

You should be more comfortable with the TDD (Test Driven Development) approach here. Let’s get started with an easy warm-up.

Exercise #1 – Maps.return_name/1

The test to focus on is in test/maps_test.exs. Given a map with a :name key, return the value from the function.

mix test test/maps_test.exs:20

Exercise #2 – Maps.has_sides?/1

There are two tests to focus on. The first is the “success” path and the second is handing non-matching data. This is focusing on validating the shape of the data with less focus on the values and binding variables.

Refer to the tests for examples of the input data.

mix test test/maps_test.exs:30
mix test test/maps_test.exs:40

Exercise #3 – Maps.net_change/1

The test we want to focus on is in test/maps_test.exs. It is under “net_change/1” and the test is “subtracts beginning balance from ending balance”. If you haven’t modified the file causing the line numbers to change, then using the following mix command will work.

There are two tests for this function. This function takes a map, performs a calculation and returns the result in an {:ok, result} tuple. The result is the difference of :ending_balance and :initial_balance.

mix test test/maps_test.exs:50
mix test test/maps_test.exs:60

Iterate on the code changes to make the tests pass. Use pattern matching in the function declaration.

Exercise #4 – Maps.classify_response/1

This exercise is one of my favorites! This is based on several real-world experiences of mine. At some point, you will have a project that integrates with some other external service. That external service may return XML or JSON but not in any standardized, clean way. You are left dealing with whatever they give you. Even APIs returning JSON can be messy and not follow good REST practices. This exercise is inspired by those experiences. You don’t get to control the data you have to react to. Your goal is to get the meaning and result you need from the given data.

When I was learning Elixir and encountered a situation like this, I created my first-pass solution. It was a traditional imperative step-by-step solution. Looking at my working solution, I wasn’t satisfied. I thought, “That doesn’t feel very Elixir-like yet”. I went back, looked at more Elixir code, tried again. My second attempt still didn’t feel quite “right” yet. After my third attempt, I ended up with a solution that “felt right”. It was elegant and I was elated!

I stress this point because you need to know that it’s okay to take multiple passes at a solution.


The test defines 4 sample responses from an external service. We want to be able to classify the response and identify when a request was successful and extract the desired value. There are 3 examples of how the request might fail. We want to be able to tell them apart as they will influence how the application responds.

The response data we need to look at is spread out throughout a nested map. Pattern matching lets you create an elegant solution.

There are 4 tests for this function. One is the happy-path solution and the other 3 are for handling the different error conditions we expect.

mix test test/maps_test.exs:86
mix test test/maps_test.exs:92
mix test test/maps_test.exs:96
mix test test/maps_test.exs:102

Matching Success

First, let’s detect when it succeeds. Our function should return that it succeeded with an {:ok, result} tuple which include the extracted token. The test case include a comment that explains what about the data defines a success. The setup for the test cases includes sample response data.

mix test test/maps_test.exs:88

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

Matching When Invalid

Next, let’s add to our existing solution by moving on to the next test. Again, the test case includes a comment that defines when the response is “invalid”.

mix test test/maps_test.exs:92

Matching When Retry

Continue building on your work to add support for detecting when we should “retry” our request with the server. Again, the test case includes a comment that defines when the response tells us to “retry”.

mix test test/maps_test.exs:96

Matching When Account is Frozen

Conclude this exercise by building on your work to add support for detecting when a request failed because an account is “frozen”. Again, the test case includes a comment that defines when we can tell an account is “frozen”.

mix test test/maps_test.exs:102

Summary

All the tests for classify_response/1 should be passing!

$ mix test test/maps_test.exs
Compiling 1 file (.ex)
.........

Finished in 0.05 seconds
9 tests, 0 failures

Randomized with seed 955953

Take a look at the full solution (hidden below) and think how the same solution would look if done imperatively using nested if statements! I love pattern matching! It makes the code declarative and clear. I feel like I’m a better programmer because of it.

When code is written this way, it is “declarative”. The code just “declares” the shape of the data it cares about. It basically says, “When the data matches this specific shape, it means we retry.”

When the same problem is solved without pattern matching (like when using nested if statements), reading it becomes a exercise of mentally parsing code to figure out what it’s doing. Instead of applying a single pattern definition for a match statement, it is step-by-step poking the data to “feel” what the shape is. “Does it have this key? Yes? So then does it have this next key?”

As you become more comfortable writing code this way, you won’t want to go back to the old way. Code becomes elegant. Your solutions become a declarative expression of what the data means. It’s easier to understand, easier to refactor, and more fun to write!

Comments are closed

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

28 Comments

  1. sammygadd on November 8, 2020 at 2:03 am

    The first example looks really confusing to me as an Elixir noob. I think it would be a bit easier to understand if the params variables had different names. Overloading them makes things even more confusing. (Or do I completely misunderstand how this works??)

    • Mark Ericksen on November 8, 2020 at 6:08 am

      I’d love to answer your question but I’m not sure I understand it. Are you referring to the example at the “In Pieces and Whole”? Or are you referring to Exercise #1? Which params would you change to improve the clarity?

      • sammygadd on November 9, 2020 at 3:39 am

        I’m referring to the example in “In Pieces and Whole”. The first line declares a params variable. Then the function binds the arguments to another variable also named params.
        At least this is how I understand it. Or are those two variables actually the same?? If not then, to me it would be easier to understand if they had different names to signify that they are different. (A guess simply moving the first line below the function would also help)

        • Mark Ericksen on November 9, 2020 at 4:45 am

          Ah! I see. Thank you for explaining that. The idea with the outer params variable is that it’s like the params you get from Phoenix in a controller from a user POST. They come in with string keys.

          Within the scope of the function do_work, the params argument to the function is different. In this particular case, they would have the same values, but they are different. So yes, I can see that this is confusing. Would it make more sense if the outer params were named phoenix_params or web_params?

          • sammygadd on November 10, 2020 at 12:26 pm

            Yes, for me that makes total sense. I think `web_params` would makes this more clear for noobs like me 🙂
            Thanks!



          • Mark Ericksen on November 10, 2020 at 1:02 pm

            Awesome! Thanks for the feedback! I’ve updated the lesson.



  2. Francisco Quintero on November 25, 2020 at 8:30 am

    Awesome exercises. The last one took me a bit because I wasn’t pattern matching in the function definition but when I realized that, it all went downhill to have passing tests.

  3. romenigld on December 12, 2020 at 5:06 pm

    I loved the explanation and again I was having doubts and needed to understand better was this large of pattern matching and your explanation was very helpful for me.
    Just the second exercise I made a mistake on pattern matching and after review the sample of the NestedBinding.award_bonuses function I realized the test with success and the rest was builded more quickly.
    And it wasn’t needed to put the ‘ = _response’ for match the map.
    And your notes are amazing thank you!

  4. Mark Johnson on February 21, 2021 at 8:29 am

    Awesome lesson!

    • Mark Ericksen on February 21, 2021 at 8:31 am

      I’m glad you liked it!

  5. Chad Henson on February 24, 2021 at 2:42 pm

    I love the format. I notice that I instinctively went to a case statement instead of defining the function multiple times. Are there pros/cons to that approach that I should take into consideration? Specifically, here I am referring to the classify_response(...) function. I did the following

    def classify_response(response) do
      case response do
         %{.... relevant pattern ...} ->
            {ok:, token}
         %{.... another pattern ...} ->
            {error:, invalid:}
      end
    end
    
    • Mark Ericksen on February 25, 2021 at 5:33 pm

      That’s a good question. In a simple example like this, where the code being performed on a match is a simple mapping to a tuple, there isn’t much difference between a case and using separate function bodies.

      In general, I encourage you to reach more often for separate function bodies. Consider what the case statement would look like when more involved logic is performed on each match. It could easily become many lines possibly including nested case statements. A wonderful side-effect of having multiple function bodies is that the logic for what to do in that case is fully contained. This makes the code easier to understand. The functions become very straight forward and single purposed.

      • Chad Henson on February 26, 2021 at 3:26 pm

        Upon further reflection, the suggestion for multiple function bodies seems to be the most “pure” of the functional approach. Each function “does one thing” and your job as a developer is to make it “do that one thing well”. I think you could argue that having multiple pattern matches in one function via a case statement could violate that principle. Especially when you aren’t limiting your pattern matching to only a single “domain” (e.g. customer, order, etc..).

        • Mark Ericksen on February 27, 2021 at 6:44 am

          Well said! I agree that the pattern matches in the function declaration helps keep individual function bodies focused on a single purpose. Functions that are focused on a single thing are much easier to understand, test, and maintain. However, it is also common to use case statements inside functions for simple things like this…

          def place_order(%Customer{} = customer, order_params) do
            case Orders.create(customer, order_params) do
              {:ok, new_order} ->
                # notify customer that order was placed
          
              {:error, reason} ->
                # notify customer of order problem
            end
          end
          

          In this example we need to execute the function Orders.create/2 before we can have data to pattern match on. Also, the code we perform in the case matches will likely be simple. So there are plenty of times we want to use a case too. It becomes a question of complexity and when is the right time to refactor it out into functions.

  6. Uzo Enudi on March 11, 2021 at 6:33 am

    Given that:

    response = %{payload: 123}

    Please, what’s the implied difference between:

    def fun( resp = %{payload: data} ), do: {data, resp}
    and:
    def fun( %{payload: data } = resp ), do: {data, resp}
    

    When fun(response) is called they, I get the same result.

    • Mark Ericksen on March 11, 2021 at 8:52 am

      You are correct. They are functionally equivalent. It is really a personal preference. I think the Credo library even accepts both versions but all of the code must do it consistently the same way.

      The community convention is to define the pattern first and then name it (if desired). The critical part is the pattern, so that’s why I like it first.

      But you are right, they both give the same results. 🙂

      • Uzo Enudi on March 12, 2021 at 11:03 am

        Thank you for such a great response. I’ll stick with the community convention.

  7. Maksym Kosenko on November 4, 2021 at 4:11 pm

    Wow!!! This lesson made me thinking ‘pattern matching’. 💯

    • Mark Ericksen on November 4, 2021 at 8:39 pm

      Awesome! It’ll become your new super power!

  8. Bill Bozarth on November 30, 2021 at 7:20 am

    Learning to program and I am loving your courses Mark!! Thank you for providing them and please make more! Maybe a short one on either using Exercism.io’s problems and/or common problems/challenges concerning app construction and build out? As if you are not doing a lot already. I love what you are doing at Fly.io!!

    Question:
    From my beginner perspective I am confused why there is such a debate about dynamic versus static typing when types appear to be taken care of with patterns like the one you used above for Billing. I have to be missing something, could you explain this to me or provide a link possibly?

    defmodule Billing do
      def apply_charge(%{id: customer_id} = customer, charge) do
        record_charge(customer_id, charge)
        notify_customer(customer, charge)
      end
    end
    
    • Mark Ericksen on November 30, 2021 at 1:48 pm

      Hi Bill! Thanks for the kind words!

      As for static vs dynamic types, it’s a good question and I’m not sure I can do it justice here. I’ll give it a try. You may have already seen this, but in case you haven’t: https://thinkingelixir.com/elixir-in-the-type-system-quadrant/

      Static types are checked at compile time. The code won’t compile if the compiler isn’t satisfied that all the types, variables of those types and the operations on those types are all valid. This gives some greater compile-time guarantees and even enables more powerful refactoring IDE tools.

      Dynamic types are checked at runtime. Like in the pattern matches, the pattern is checked against the data at runtime… during program execution. So it becomes possible to write the following.

      def do_work(customer) do
        IO.puts customer.invalid_field
      end
      

      This will compile… because there is no check or type declaring what customer is here. But it will fail during executing because the field is invalid for the actual data passed in.

      A con for static types is that you spend a lot of time doing work just to satisfy the compiler. A lot of language features (like interfaces) get created just to help provide types while trying to separate them from the data. You end up doing things like “dependency injection” just to work around the constraints of the types. I find it adds a lot of complexity that gets in the way of the expression and intent of the code.

      There are pros and cons to each approach (static vs dynamic). Having worked in both, as you can guess, I prefer dynamic. However, others will prefer static. I hope that at least helps a little in understanding the difference.

      I hope you have fun as you continue on your programming journey!

      • Bill Bozarth on November 30, 2021 at 5:29 pm

        That was more than I hoped for, thanks!
        Oh, this weeks Thinking Elixir talk with Dave Lucia was super interesting for me since I imagine making an app that this stack and architecture might well serve. Thanks, again!

        • Mark Ericksen on December 1, 2021 at 6:21 am

          Awesome! Glad it was helpful!

  9. Kevin Kelleher on January 14, 2022 at 8:11 am

    I’ve got to say, this chapter really excited me. I’ve done a lot of fetching/parsing JSON, and it’s a pain, even with nice libraries or tools like jq.

    The example you gave is a killer. I love it.

  10. Juan Borrás on August 25, 2023 at 3:16 am

    The paragraph that starts with “In this code, we have a string-key map called params similar…” shoud be In this code, we have a string-key map called web_params similar..”

    Notice also that unless web_params contains a map with a key called “email” the do_work/1 function will not be executed and the runtime will throw an exception. So yes, the idiom is powerful but you may want to add some robustness to it all.

  11. Juan Borrás on August 25, 2023 at 3:29 am

    Also notice that:

    %{:name => “foo”} == %{name: “foo”}

    holds, but:

    %{“name” => “foo”} == %{name: “foo”}

    does not.

    • Mark Ericksen on September 12, 2023 at 6:10 am

      That’s correct! In this example the key is an atom for the first map and a string in the second map. They are both valid maps, but different data types make them not equal.

  12. Caleb Josue Ruiz Torres on September 11, 2023 at 5:27 pm

    “… reading it becomes a exercise of mentally parsing code to figure out what it’s doing. ”

    Agree, and I would say this is one of the benefits of functional programming, by declaratively expressing what you want to compute you liberate cognitive load and “mental brute force”, leveraging digital computer’s power.

Comments are closed on this static version of the site.