danielberkompas

danielberkompas

Maybe: nil protection for nested structs

I have a problem. It’s very common to have nested structs when you deal with Ecto and associations. For example, a Person can have a City, which has a name.

The problem is, city can sometimes be nil. In that case, this code will raise an error. (“nil doesn’t respond to :name”) I want something like Ruby try here.

person.try(:city).try(:name)

Elixir has a built-in thing like this, called get_in:

get_in map, [:nonexistent, :keys]
# => nil

However, it only works with maps, not structs. The documentation says it’s specifically ​_not_​ supposed to work with structs.

So, I have a proposal. We make a module with a functional API like this:

maybe(person, [:city, :name])

And a macro to pretty things up a bit:

maybe(person.city.name)

If any element in the chain returns nil, the expression returns nil.

Here’s my sample implementation:

defmodule Maybe do
  defmacro maybe(ast) do
    [variable|keys] = extract_keys(ast)
​
    quote do
      maybe(var!(unquote(variable)), unquote(keys))
    end
  end
​
  def maybe(nil, _keys), do: nil
  def maybe(val, []), do: val
  def maybe(map, [h|t]) do
    maybe(Map.get(map, h), t)
  end
​
  defp extract_keys(ast, keys \\ [])
  defp extract_keys([], keys), do: keys
  defp extract_keys({{:., _, args}, _, _}, keys) do
    extract_keys(args, keys)
  end
  defp extract_keys([{{:., _, args}, _, _}|t], keys) do
    keys = keys ++ extract_keys(args)
    extract_keys(t, keys)
  end
  defp extract_keys([{:., _, args}|t], keys) do
    keys = keys ++ extract_keys(args)
    extract_keys(t, keys)
  end
  defp extract_keys([key|t], keys) do
    keys = keys ++ [key]
    extract_keys(t, keys)
  end
end

And example usage:

import Maybe

defmodule Person do
  defstruct city: nil
end
	
defmodule City do
  defstruct name: nil
end
	
person = %Person{city: %City{name: "Portland"}}
maybe(person.city.name) # => "Portland"
maybe(person.nonexistent.name) # => nil

Is this a good idea? Should it be named something else? Is there something in the standard library that I missed?

Most Liked

josevalim

josevalim

Creator of Elixir

Is there a certain reason why pattern matching on %{} and is treatened differently?

They are different structures. Maps were designed to always map on a subset, therefore the empty map ends up matching all maps. Otherwise, imagine how useless pattern matching on maps would be if every time I wanted to take a key from the map, I had to match on all keys. The cases where I would want to do a full matching are rather rare (and can always be achieved with map_size).

12
Post #4
danielberkompas

danielberkompas

@Oliver, yes, Map.get does do what I want. I actually use it here to provide the functional API for Maybe:

def maybe(nil, _keys), do: nil
def maybe(val, []), do: val
def maybe(map, [h|t]) do
  maybe(Map.get(map, h), t)
end

You can call it like so: maybe(map, [:first, :second, :third]). I think it’s a little more convenient than doing a pipeline.

The maybe macro just converts calls like this: maybe(map.first.second.third) into functional calls for maybe(may, [:first, :second, :third]).

Edit: Since the Maybe module is so small, I decided not to release a hex package for it. If you’re interested, you can find it here: https://github.com/infinitered/phoenix_base/pull/14

josevalim

josevalim

Creator of Elixir

Remember you can use pattern matching:

case person do
  %{city: %{name: name}} -> name
  %{} -> default
end
NobbZ

NobbZ

This makes absolutely sense, thank you for your answer.

NobbZ

NobbZ

I’m always confused when matching on %{} (or #{} in erlang)… My intuition does tell me we are matching on an empty map (as we were when matching on []), but in reality it is like anything when is_map(anything).

Is there a certain reason why pattern matching on %{} and [] is treatened differently?

Where Next?

Popular in Questions Top

LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement