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?
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: ""
You can also use a combination of the Kernel.get_in/2 function and the Access.key/2:
name = get_in(project, [Access.key(:client), Access.key(:name)]) || “”
But I think pattern matching is always best first option
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.
Attention! While the access syntax is allowed in maps via map[key] , if your map is made of predefined atom keys, you should prefer to access those atom keys with map.key instead of map[key] , as map.key will raise if the key is missing (which is not supposed to happen if the keys are predefined). Similarly, since structs are maps and structs have predefined keys, they only allow the struct.key syntax and they do not allow the struct[key] access syntax. See the Map module for more information.
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.
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.)