TIL
- Access supports keyword lists (Keyword) and maps (Map) out of the box, but not structs.
- There is a reason why Elixir does not implement
Access
behaviour for structs. - Perfer
data.key
for accessing predefined atom keys of map or struct.
Access behaviour
The Access module defines a behaviour for dynamically accessing keys of any type in a data structure via the data[key] syntax.
Keyword lists and maps are similar but different. It is convenient that we can use the data[key] syntax for both data types.
Good and Bad Elixir by Chris Keathley explains why you should prefer using Access
over Map.get/2
and Keyword.get/2
.
Access keys using Access behaviour
k = [abc: [xyz: 123]]
m = %{abc: %{xyz: 123}}
Keyword.get(k, :abc)
Map.get(m, :abc)
# Fills the gap between keyword lists and maps for accessing keys
k[:abc][:xyz]
m[:abc][:xyz]
# Returns nil when the path is nonexistent
k[:abc][:bad][:xyz] # nil
m[:abc][:bad][:xyz] # nil
# For nested data structures, we can also use `Kernel.get_in/2` for both keyword lists and maps
get_in(k, [:abc, :xyz])
get_in(m, [:abc, :xyz])
But structs are different! Structs do not implement the Access behaviour.
defmodule Opts do
defstruct [:abc]
end
s = %Opts{abc: %{xyz: 123}}
s[:abc]
** (UndefinedFunctionError) function Opts.fetch/2 is undefined (Opts does not implement the Access behaviour)
Opts.fetch(%Opts{abc: %{xyz: 123}}, :abc)
(elixir 1.13.0-rc.0) lib/access.ex:285: Access.get/3
get_in(s, [:abc])
** (UndefinedFunctionError) function Opts.fetch/2 is undefined (Opts does not implement the Access behaviour)
Opts.fetch(%Opts{abc: %{xyz: 123}}, :abc)
(elixir 1.13.0-rc.0) lib/access.ex:285: Access.get/3
# There is a workaround for Kernel.get_in/2
get_in(s, [Access.key(:abc), Access.key(:xyz)])
Why structs do not implement the Access behaviour
According to Elixir Access behaviour documentation, it seems intentional.
... 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.
That totally makes sense because most likeky we define structs for explicitness in the first place. Sometimes it is hard to make a decision between convenience vs explicitness, but I would stick with Elixir documentation's recomendation by default.
Implement Access protocol for structs
If we really want to implement Access protocol for structs, we could do so easily using one of a few convenient Elixir packages.
iex
Mix.install([{:accessible, []}, {:struct_access, []}])
# Using accessible
defmodule AccesibleOpts1 do
defstruct [:abc]
use Accessible
end
# Using struct_access
defmodule AccesibleOpts2 do
defstruct [:abc]
use StructAccess
end
# Problem solved!!!
accesible = %AccesibleOpts1{abc: %{xyz: 123}}
accesible[:abc][:xyz]
get_in(accesible, [:abc, :xyz])