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?