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:
def call do
source = get_some_map()
%{}
|> maybe_add_foo()
|> maybe_add_bar()
end
defp maybe_add_foo(map, %{foo: foo}) do
Map.put(map, :foo, foo)
end
defp maybe_add_foo(map, _), do: map
defp maybe_add_bar(map, %{bar: bar}) do
Map.put(map, :bar, bar)
end
defp 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.
def call do
source = get_some_map()
Map.take(source, [:foo, :bar])
end
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:
def call(args) do
contact = split_contact_info(args)
address = split_address_info(args)
{contact, address}
end
defp split_contact_info(map) do
Enum.reduce(map, %{}, fn
{:phone, phone}, acc -> Map.put(acc, :phone, phone)
{:email, email}, acc -> Map.put(acc, :email, email)
_, acc -> acc
end)
end
defp split_address_info(map) do
Enum.reduce(map, %{}, fn
{:line1, line1}, acc -> Map.put(acc, :line1, line1)
{:line2, line2}, acc -> Map.put(acc, :line2, line2)
{:city, city}, acc -> Map.put(acc, :city, city)
{:state, state}, acc -> Map.put(acc, :state, state)
{:postal_code, postal_code}, acc -> Map.put(acc, :postal_code, postal_code)
{:country, country}, acc -> Map.put(acc, :country, country)
_, acc -> acc
end)
end
Again, using Map.take/2
is so much simpler.
def call(args) do
contact = Map.take(args, [:phone, :email])
address = Map.take(args, [:line1, :line2, :city, :state, :postal_code, :country])
{contact, address}
end
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:
def call(name, email) do
# Process something with name and email
end
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:
def call(params) do
params = allowed_params(params)
# Process something with params now sanitized
end
def allowed_params(params) do
Enum.reduce(map, %{}, fn
{:name, name}, acc -> Map.put(acc, :name, name)
{:email, email}, acc -> Map.put(acc, :email, email)
{:phone, phone}, acc -> Map.put(acc, :phone, phone)
{:country, country}, acc -> Map.put(acc, :country, country)
{:message, message}, acc -> Map.put(acc, :message, message)
_, acc -> acc
end)
end
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.
def call(params) do
params = Map.take(params, [:name, :email, :phone, :country, :message])
# Process something with params now sanitized
end
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 ❤️.