Elixir version of a safe navigation operator? (navigating nil in maps/structs)

Ah true, it’s not hard but does suck to have to convert Core to Erlang just to have debug info…

That does explain why LFE did not have debug_info support and why secondary hooks were often added. ^.^;
Fascinating design, I definitely need to catch up on modern developments on the engine. :slight_smile:

/me is reading through the code in the PR now…

Pattern matching is the answer. :slight_smile: Instead of:

if user && user.address && user.address.city do
  ...
else
  ...
end

do:

case user do
  %{address: %{city: city}} when is_binary(city) -> ...
  _ -> ...
end
17 Likes

Thanks @josevalim for sharing the idea for the below as well. This works better in our Slime partials:

  @doc """
  Save navigation for structs:

  struct_get_in user, [:address, :city]
  """
  def struct_get_in(data, key) when is_atom(key) do
    struct_get_in(data, [key])
  end
  def struct_get_in(data, keys) when is_list(keys) do
    {first, last} = Enum.split(keys, -1)
    first = Enum.map(first, &Access.key(&1, %{}))
    last  = Enum.map(last, &Access.key(&1, nil))

    get_in data, first ++ last
  end
1 Like

For cases where we may get either a struct or nil, this is another way of handling it, without case or extra function heads:

struct_or_nil
|> Kernel.||(%{})
|> Map.get(:a_key) # works for both struct and bare map

Use Access.get. It can handle nil inputs.

1 Like

Well, Access.get requires implementing Access behaviour for struct.

Which is not a bad idea given how you seem to be accessing data of of it.

One year later but the answer to your question is get_in+Access.key. :slight_smile:

7 Likes

@josevalim I don’t believe that is true.

If you have something like this:

o = %OuterStruct{
  inner: %InnerStruct{
    field: 1
  }
}

get_in o, [Access.key(:inner), Access.key(:field)] #returns 1

o = %OuterStruct{
  inner: nil
}

get_in o, [Access.key(:inner), Access.key(:field)]
# returns
** (BadMapError) expected a map, got: nil
    (elixir 1.12.3) lib/map.ex:464: Map.get(nil, :pairing, :a)
    (elixir 1.12.3) lib/access.ex:459: anonymous fn/5 in Access.key/2

This happens because Access.key returns default only for a missing key. The :inner key is not missing, it is just set to nil so it is passed to next accessor.

1 Like

I get a different behaviour:

iex(2)> defmodule S do
...(2)> defstruct [:inner]
...(2)> end
iex(3)> get_in %S{inner: nil}, [Access.key(:inner), Access.key(:field)]
nil
iex(4)> get_in %S{inner: nil}, [Access.key(:unknown), Access.key(:field)]
nil

But i am on v1.13-rc.0. Maybe we fixed something?

1 Like

I think you fixed it here

4 Likes

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.

1 Like