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
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).
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: Add Maybe by danielberkompas · Pull Request #14 · infinitered/phoenix_base · GitHub
josevalim
Remember you can use pattern matching:
case person do
%{city: %{name: name}} -> name
%{} -> default
end
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









