We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
get_in/2 needs Access.key!/1 for structs
almirsarajcic
get_in/2, put_in/3, and update_in/3 walk a path of atom keys through maps and keyword lists just fine — but the same atom path raises on a struct.
defmodule Profile do
@type t :: %__MODULE__{}
defstruct [:city, :country]
end
defmodule User do
@type t :: %__MODULE__{}
defstruct [:name, :profile]
end
user = %User{name: "Ada", profile: %Profile{city: "London", country: "UK"}}
get_in(user, [:profile, :city])
# ** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour
#
# You can use the "struct.field" syntax to access struct fields. You can also use Access.key!/1 to access struct fields dynamically inside get_in/put_in/update_in). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
get_in(user, [Access.key!(:profile), Access.key!(:city)])
# => "London"
get_in/2 treats a plain atom in the path as a call to Access.fetch/2 on whatever module owns the current value. Map implements the Access behaviour, so atom keys work against plain maps and keyword lists. Structs deliberately do not implement Access — Elixir’s authors want a typo’d struct field to fail loudly (a KeyError or, here, an UndefinedFunctionError) instead of silently returning nil the way a missing map key would. Access.key!/1 and Access.key/2 are the accessor functions that opt a struct field back into the get_in/put_in/update_in machinery, explicitly, one key at a time.
The two accessor functions differ on a missing key:
Access.key!(:profile)
# raises KeyError if :profile is not a key on the struct
Access.key(:profile, %Profile{})
# returns the default (here, a fresh %Profile{}) instead of raising
Access.key!/1 on a key the struct genuinely doesn’t have:
get_in(user, [Access.key!(:profile), Access.key!(:missing_field)])
# ** (KeyError) key :missing_field not found in:
#
# %Profile{city: "London", country: "UK"}
The same Access.key!/1 path works for writes too:
put_in(user, [Access.key!(:profile), Access.key!(:city)], "Paris")
# => %User{name: "Ada", profile: %Profile{city: "Paris", country: "UK"}}
update_in(user, [Access.key!(:profile), Access.key!(:city)], &String.upcase/1)
# => %User{name: "Ada", profile: %Profile{city: "LONDON", country: "UK"}}
Two more gotchas. First, a nil intermediate value blows up the same way a missing struct key does — get_in doesn’t short-circuit through nil for you, so a chain like Access.key!(:profile) on a User whose profile is nil fails at the next step, not silently. Second, since Elixir 1.17, if you already know every step of the path exists, plain dot access works directly on structs without any of this: user.profile.city returns "London" with no Access involved at all — reach for get_in/Access.key!/1 specifically when the path is built dynamically or needs to survive an absent key gracefully.
copied to clipboard