Rich_Morin

Rich_Morin

How to get a list of access lists for use with get_in/2?

I’d like to process a tree of maps to get a list of access lists for the leaf nodes (eg, for use with get_in/2). So, for example, if the tree is %{k1: %{k11: 11}, k2: %{k21: 21} }, the list should be [[:k1, :k11],[:k2,:k21]].

Before I hurt my brain hacking recursive functions, can anyone supply a clean and simple way to do this?

-r

Most Liked

peerreynders

peerreynders

defmodule Demo do
  def paths(tree) do
    paths(tree, [], [])
  end

  defp paths(tree, parent_path, paths) do
    {_,paths} = Enum.reduce(tree, {parent_path, paths}, &descend/2)
    paths
  end

  defp descend({key, value}, {parent_path, paths}) do
    paths =
    if is_map(value) do
      paths(value, [key|parent_path], paths)
    else
      [:lists.reverse([key|parent_path])|paths]
    end
    {parent_path, paths}
  end

  def run(tree) do
    tree
    |> Demo.paths()
    |> IO.inspect()
  end

end

Demo.run(%{})
Demo.run(%{k11: 11})
Demo.run(%{k1: %{k11: 11}})
Demo.run(%{k1: %{k11: 11}, k2: 2})
Demo.run(%{k1: %{k11: 11}, k2: %{k21: 21} })
Demo.run(%{k1: %{k11: 11}, k2: %{k21: 21, k22: 22}})
$ elixir demo.exs
[]
[[:k11]]
[[:k1, :k11]]
[[:k2], [:k1, :k11]]
[[:k2, :k21], [:k1, :k11]]
[[:k2, :k22], [:k2, :k21], [:k1, :k11]]
defmodule Demo do
  def paths(tree),
    do: paths(Map.to_list(tree), [], [], [])

  defp paths([], _, [], paths),
    do: paths

  defp paths([], _, [{parent, others} | rest], paths),
    do: paths(others, parent, rest, paths)

  defp paths([{key, value} | others], parent, rest, paths) when is_map(value),
    do: paths(Map.to_list(value), [key | parent], [{parent, others} | rest], paths)

  defp paths([{key, _} | others], parent, rest, paths),
    do: paths(others, parent, rest, [:lists.reverse([key | parent]) | paths])

  def run(tree) do
    tree
    |> Demo.paths()
    |> IO.inspect()
  end
end

Demo.run(%{})
Demo.run(%{k1: 1})
Demo.run(%{k1: %{k11: 11}})
Demo.run(%{k1: %{k11: 11}, k2: 2})
Demo.run(%{k1: %{k11: 11}, k2: %{k21: 21}})
Demo.run(%{k1: %{k11: 11}, k2: %{k21: 21, k22: 22}})
Demo.run(%{k1: %{k11: %{k111: 111, k112: 112}, k12: 12}, k2: %{k21: 21, k22: 22}})
$ elixir demo.exs
[]
[[:k1]]
[[:k1, :k11]]
[[:k2], [:k1, :k11]]
[[:k2, :k21], [:k1, :k11]]
[[:k2, :k22], [:k2, :k21], [:k1, :k11]]
[[:k2, :k22], [:k2, :k21], [:k1, :k12], [:k1, :k11, :k112], [:k1, :k11, :k111]]
$
alco

alco

Here’s a compact, non-tail-recursive implementation:

defmodule Nmap do
  def trace_map(map) when is_map(map) do
    # In a simple case the result of this function will be a list of one-element lists. For example,
    #
    #     trace_map(%{k1: 1, k2: 2}) == [[:k1], [:k2]]
    #
    # However, if there are nested maps, the top-level key will be prepended to all subpaths returned from the recursive
    # calls of this function:
    #
    #     trace_map(%{k1: %{k11: 11, k12: 12}, k2: 2}) == [[:k1, :k11], [:k1, :k12], [:k2]]
    #
    # This is why we're using Enum.flat_map for the iteration here: to allow recursive function calls to return a list
    # that will then be spliced into the parent list instead of being added to the parent as a single list of lists. If
    # we were to use just Enum.map, we'd get the following result instead:
    #
    #     trace_map(%{k1: %{k11: 11, k12: 12}, k2: 2}) ==
    #       [
    #         [[:k1, :k11], [:k1, :k12]],  # return value of the first recursive call
    #         [[:k2]]
    #       ]
    #
    Enum.flat_map(map, fn
      {key, nested_map} when is_map(nested_map) ->
        # If the value is a nested map, get its list of paths recursively and prepend the current key to each of them.
        for key_list <- trace_map(nested_map) do
          [key | key_list]
        end

      {key, _val} ->
        # For plain values return the key to end the recursion. The key is wrapped in a list twice because flat_map
        # will unwrap the outermost list and the remainder will be used as a list tail by the calling function (the one
        # immediately up the stack).
        [[key]]
    end)
  end
end

Usage example:

iex(21)> m = %{k1: %{k11: 11, k12: %{k121: 121}, k13: 13}, k2: 2}
%{k1: %{k11: 11, k12: %{k121: 121}, k13: 13}, k2: 2}

iex(22)> paths = Nmap.trace_map(m)
[[:k1, :k11], [:k1, :k12, :k121], [:k1, :k13], [:k2]]

iex(23)> for path <- paths do
...(23)>   get_in(m, path)
...(23)> end
[11, 121, 13, 2]
Rich_Morin

Rich_Morin

Thanks, guys! FWIW, I opted to use a slight variation on Peer’s first version:

  def leaf_paths(tree), do: leaf_paths(tree, [], [])

  defp leaf_paths(tree, parent_path, paths) do
    {_, paths} = Enum.reduce(tree, {parent_path, paths}, &descend/2)
    paths
  end

  defp descend({key, value}, {parent_path, paths}) when is_map(value), do:
    {parent_path, leaf_paths(value, [ key | parent_path ], paths) }
    
  defp descend({key, _value}, {parent_path, paths}), do:
    {parent_path, [ :lists.reverse( [ key | parent_path ] ) | paths ] }

-r

Where Next?

Popular in Questions Top

LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement