grantwest
Concise nil safe way to access field on nested struct?
If I have nested structures, and I want to fetch a field from the a nested structure, but handle a nil case, is there a succinct way to do that? Lets say I have a Project struct which has a client field, which is itself a struct that has field name:
project = %Project{}
name = project.client.name
This works, but if client is nil we get an error. If these were maps I could do
name = get_in(project, [:client, :name]) || ""
Now we get the name if it exists, or we have an empty string. This does not work with structs because they don’t implement access. Is there a similarly concise solution for structs?
Most Liked
kevinschweikert
In Elixir 1.17 you can use get_in/1 like this get_in(struct.foo.bar)
Docs: Kernel — Elixir v1.17.0-rc.1
In case any of the keys returns
nil, thennilwill be returned andget_in/1won’t traverse any further.
benwilson512
It may be heretical but for all of my ecto structs at least we just went ahead and implemented the Access behavior functions so that we can just use access normally. The ergonomics are really nice.
benwilson512
Yeah I think that moduledoc is perhaps a bit too strongly worded, because it isn’t that structs aren’t allowed to use that syntax, it’s just that it won’t work by default.
Not really, it’s worked great. In fact in particular, it works rather well with how Ecto handles associations that aren’t loaded yet. Eg:
shipment.origin_location[:customer][:name]
If you forget to Repo.preload(shipment, [origin_location: :customer]) then you get:
** (UndefinedFunctionError) function Ecto.Association.NotLoaded.fetch/2 is undefined (Ecto.Association.NotLoaded does not implement the Access behaviour.
Which is great! You didn’t load the association and so instead of treating that as just nil it errors, indicating that you need to actually load it to determine if it’s there or not.
chocolatedonut
I think it suffices to add @behaviour Access to your struct and implement the 3 @callbacks. If you only need a read access, it’s enough to merely implement
def fetch(struct, key) do
{:ok, Map.get(struct, key)}
end
in your struct (without needing to also add @behaviour Access to it). Though I think you then can’t get any compiler support via @impl Access annotation on the fetch/2 function. (Somebody correct me please if otherwise.)
codeanpeace
Hmm, pattern matching comes to mind…
For example, pattern matching via a case statement:
project = %Project{...}
name = case project do
%{client: %{name: name}} -> name
_ -> ""
end
# as one liners
name = case project, do: (%{client: %{name: name}} -> name; _ -> "")
name = case project do %{client: %{name: name}} -> name; _ -> "" end
Or alternatively, pattern matching via function heads:
project = %Project{...}
name = get_name(project)
defp get_name(%{client: %{name: name}}, do: name
defp get_name(_project), do: ""







