Elixir Access behaviour
Tags: elixir
Published: 2021-11-14 17:44:36

## 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.
- Prefer `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](https://keathley.io/blog/good-and-bad-elixir.html) explains why we should prefer using [`Access`] over [`Map.get/2`] and [`Keyword.get/2`].

## Access keys using Access behaviour

```elixir
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.

```elixir
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][Access] 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 probably I would stick with Elixir documentation's recommendation whenever possible.

## 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.

- https://github.com/codedge-llc/accessible
- https://github.com/mbramson/struct_access

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

[Access]: https://hexdocs.pm/elixir/Access.html
[`Access`]: https://hexdocs.pm/elixir/Access.html
[`Keyword`]: https://hexdocs.pm/elixir/Keyword.html
[`Map`]: https://hexdocs.pm/elixir/Map.html
[`Keyword.get/2`]: https://hexdocs.pm/elixir/Keyword.html#get/3
[`Map.get/2`]: https://hexdocs.pm/elixir/Map.html#get/3