Elixir Access behaviour

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])