sudostack
Elixir version of a safe navigation operator? (navigating nil in maps/structs)
Having written a lot more Phoenix templates as of late, I’m doing a lot more attribute checks than I would like. In Ruby 2.3 we could use the #try method or the safe-navigation operator &. to navigate possible nested nils. Here, if address was nil, then it shouldn’t try to access city:
Ruby:
user.try(:address).try(:city) OR user&.address&.city as opposed to user && user.address && user.address.city / writing nested if expressions/statements.
Is there a nicer way to navigate nested maps or structs in Elixir, so that code that I write in EEX could be more terse? Or is there a better pattern to not have an exception raised if I’m trying to access a nested field found in a parent field that is nil?
Most Liked
josevalim
Pattern matching is the answer.
Instead of:
if user && user.address && user.address.city do
...
else
...
end
do:
case user do
%{address: %{city: city}} when is_binary(city) -> ...
_ -> ...
end
josevalim
michalmuskala
def, defmacro, and defmodule are already plain macros.
Most people working on the compiler would love to get rid of this too, but it’s a backwards-incompatible change. So we have to live with it until 2.0.
You can easily introduce a new lexical scope by wrapping in try do <code> end.
Furthermore, Elixir has hygienic macros, so it won’t leak between contexts. You’re building the variable AST by hand working really hard to work around the regular (hygienic) macro mechanisms and then complaining it’s not hygienic ![]()
What’s more, you can rebind variables, so it’s perfectly fine to have something like this in reduce:
case unquote(maybe_map) do
nil -> nil
map -> Map.get(map, unquote(k), nil)
end
This leads us to yet another realisation that we never bind any variable using = so nothing will ever leak, even with current implementation:
iex(1)> case %{} do
...(1)> map -> map
...(1)> end
%{}
iex(2)> map
** (CompileError) iex:2: undefined function map/0
This means the whole thing can be simplified to:
defmodule SafeNilGet do
defmacro sng({var, _meta, ctx} = ast) when is_atom(var) and is_atom(ctx) do
ast
end
defmacro sng({{:., _, [v, k]}, _, []}) when is_atom(k) do
quote do
case sng(unquote(v)) do
nil -> nil
map -> Map.get(map, unquote(k), nil)
end
end
end
end
iex> import SafeNilGet
SafeNilGet
iex> map = %{blah: %{blorp: %{bleep: 42}}}
%{blah: %{blorp: %{bleep: 42}}}
iex> sng map.blah.blorp.bleep
42
iex> sng map.blah.wrong.bleep
nil
iex> binding()
[map: %{blah: %{blorp: %{bleep: 42}}}]
Last Post!
tomekowal
That makes sense. I was testing on 1.12.3
I like that change a lot because it is more intuitive to me and makes the common problem of safely navigating easier to solve.
I hope it lands in 1.13 even though it is a breaking change for people depending on the BadMapError.
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
- #hex
- #security









