Alex Antonov
Alex Antonov

Reputation: 15156

get_in for nested list & struct in elixir

I have a structure

s = [
  a: %Bla{
   b: "c"
  }
]

I want to take c value from it. I'm trying to do

get_in(s, [:a, :b])

But it's not designed to take value from the struct. Is there any analogue which allows me to fetch c from the list with nested struct?

Upvotes: 10

Views: 9989

Answers (5)

Livak
Livak

Reputation: 11

As of Elixir 1.17, Kernel.get_in/1 supports traversing structs. So this question could now be answered simply with get_in(s[:a].b). Alternatively, in the case where the outer keyword list is actually a struct, you can do get_in(s.a.b).

Upvotes: 1

Brett Beatty
Brett Beatty

Reputation: 5983

If you're worried about Access.key/2 not working with non-maps, it's fairly simple to write your own implementation that doesn't throw a fit if the data isn't a map.

def key(key) do
  # example only includes :get, not :get_and_update
  fn
    :get, %{^key => value}, next -> next.(value)
    :get, _data, next -> next.(nil)
  end
end

Upvotes: 0

Dave Buchanan
Dave Buchanan

Reputation: 61

This is my response which is more forgiving of non-map values encountered, such as a mid-traversal value being nil a string or something else Map.get() will complain about

This is essentially a more-forgiving version of Kernel.get_in/2. If any key is missing, or if mid-traversal it gets a non-mappy thing, it will return nil.

    @spec key_getter(map(), list(atom() | String.t())) :: any() | nil
    def key_getter(nil, _), do: nil
    def key_getter(map_or_struct, []), do: map_or_struct
    def key_getter(map_or_struct, _) when not is_map(map_or_struct), do: nil

    def key_getter(map_or_struct, [next_key | keys]),
      do: key_getter(Map.get(map_or_struct, next_key, nil), keys)

Upvotes: 1

Dogbert
Dogbert

Reputation: 222198

As documented, get_in does not work with structs by default:

The Access syntax (foo[bar]) cannot be used to access fields in structs, since structs do not implement the Access behaviour by default. It is also design decision: the dynamic access lookup is meant to be used for dynamic key-value structures, like maps and keywords, and not by static ones like structs.

There are two ways to achieve what you want:

  1. Implement Access behaviour for your struct.

  2. Use Access.key(:foo) instead of :foo.

I would use (2):

iex(1)> defmodule Bla do
...(1)>   defstruct [:b]
...(1)> end
iex(2)> s = [a: %Bla{b: "c"}]
[a: %Bla{b: "c"}]
iex(3)> get_in(s, [:a, Access.key(:b)])
"c"

Upvotes: 17

denis.peplin
denis.peplin

Reputation: 9851

Here is my version of try function to return values from both maps and structs:

def try(map, keys) do
  Enum.reduce(keys, map, fn key, acc -> 
    if acc, do: Map.get(acc, key) 
  end)
end

Upvotes: 4

Related Questions