🤘🏻
PostsMeContact

The Power of Map.take/2 in Elixir

April 06, 2021 3 min readTweet this post
Disassembled Typewriter

After working exclusively in elixir for 3 years now, I've been floored by the breadth of its standard library. Simply put, it's delightful.

I've developed a loving connection to certain utilities in the standard library; Enum.any?/2, Map.pop/3, Enum.zip/1 to name a few. But there's one you may not have used before that has made me very happy over the years. Map.take/2.

It's a simple little function that doesn't do much on the surface. The documentation is fairly unassuming. But, I'll bet you can find a way to make your code more readable after seeing these examples.

No more "maybe add" functions

Starting out in elixir I would commonly take some source map and put its values on another map conditionally. Usually, that condition would just be whether or not the source map had the key. I would find myself writing these kinds of functions:

elixir
1def call do
2 source = get_some_map()
3
4 %{}
5 |> maybe_add_foo()
6 |> maybe_add_bar()
7end
8
9defp maybe_add_foo(map, %{foo: foo}) do
10 Map.put(map, :foo, foo)
11end
12
13defp maybe_add_foo(map, _), do: map
14
15defp maybe_add_bar(map, %{bar: bar}) do
16 Map.put(map, :bar, bar)
17end
18
19defp maybe_add_bar(map, _), do: map

Now, there's nothing wrong with the code above. But you could imagine that if you needed 2-3 more maybe_add_* functions, it could get pretty unwieldy.

Using Map.take/2 is so much simpler when you just need to check if a key exists.

elixir
1def call do
2 source = get_some_map()
3
4 Map.take(source, [:foo, :bar])
5end

Split a map

When doing data transformations I'll sometimes need to split a source map into 2 or more sub-maps. Without Map.take/2 your code might look like:

elixir
1def call(args) do
2 contact = split_contact_info(args)
3 address = split_address_info(args)
4
5 {contact, address}
6end
7
8defp split_contact_info(map) do
9 Enum.reduce(map, %{}, fn
10 {:phone, phone}, acc -> Map.put(acc, :phone, phone)
11 {:email, email}, acc -> Map.put(acc, :email, email)
12 _, acc -> acc
13 end)
14end
15
16defp split_address_info(map) do
17 Enum.reduce(map, %{}, fn
18 {:line1, line1}, acc -> Map.put(acc, :line1, line1)
19 {:line2, line2}, acc -> Map.put(acc, :line2, line2)
20 {:city, city}, acc -> Map.put(acc, :city, city)
21 {:state, state}, acc -> Map.put(acc, :state, state)
22 {:postal_code, postal_code}, acc -> Map.put(acc, :postal_code, postal_code)
23 {:country, country}, acc -> Map.put(acc, :country, country)
24 _, acc -> acc
25 end)
26end

Again, using Map.take/2 is so much simpler.

elixir
1def call(args) do
2 contact = Map.take(args, [:phone, :email])
3 address = Map.take(args, [:line1, :line2, :city, :state, :postal_code, :country])
4
5 {contact, address}
6end

Argument/Options validation

Often I've found myself writing a function where I want to be ultra-defensive about what I allow to be passed in. When there's only a few arguments, passing them positionally works well:

elixir
1def call(name, email) do
2 # Process something with name and email
3end

With a few more arguments, I'll pass a map to the function and use Map.take/2 to only allow the values I want. I've found this especially helpful when talking to external APIs.

Without Map.take/2 the reader has to keep the context of a reduce loop in their head:

elixir
1def call(params) do
2 params = allowed_params(params)
3
4 # Process something with params now sanitized
5end
6
7def allowed_params(params) do
8 Enum.reduce(map, %{}, fn
9 {:name, name}, acc -> Map.put(acc, :name, name)
10 {:email, email}, acc -> Map.put(acc, :email, email)
11 {:phone, phone}, acc -> Map.put(acc, :phone, phone)
12 {:country, country}, acc -> Map.put(acc, :country, country)
13 {:message, message}, acc -> Map.put(acc, :message, message)
14 _, acc -> acc
15 end)
16end

Map.take/2 shines again with its readability. Making the code declarative like this removes overhead from the reader, freeing their mind from the reduce loop logic.

elixir
1def call(params) do
2 params = Map.take(params, [:name, :email, :phone, :country, :message])
3
4 # Process something with params now sanitized
5end

Hopefully, you can start to appreciate the readability that Map.take/2 can provide. For me, it sparks a little joy each time I use it because I know the code it's saving me from writing, documenting, and testing ❤️.

About the Author

Hi 👋 I'm Tyler. I'm a software engineer with a passion for learning new things. I love solving hard problems and simplifying them down to their pieces. Presently residing in Utah with my two girls and beautiful wife.

Buy me a coffee