Поскольку нам нужно буквально fork , проходя уровень, когда мы встречаем список как значение, нашим лучшим другом будет Task.async_stream/3
. Если нам нужно выполнить это лениво, все внутренние операции также будут ленивыми, пока нам не потребуется завершить результат, чтобы вернуть его из flatten/1
(с Enum.to_list/1
.)
defmodule Flat do
@spec flatten(map()) :: [map()]
def flatten(input),
do: input |> do_flatten([%{}]) |> Enum.to_list()
@doc "here we fork it in parallel and collect results"
defp do_flatten([%{}|_] = input, acc) do
input
|> Task.async_stream(&do_flatten(&1, acc))
|> Stream.flat_map(&elem(&1, 1))
end
@doc """
add `{key, value}` pairs to each list
in currently accumulated result
"""
defp do_flatten(%{} = input, acc) do
Stream.flat_map(acc, fn list ->
Enum.reduce(input, [list], &do_flatten(&1, &2))
end)
end
@doc "enumerable as value → go for it"
defp do_flatten({_k, v}, acc) when is_list(v) or is_map(v),
do: do_flatten(v, acc)
@doc "the leaf, add to all lists in the accumulator"
defp do_flatten({k, v}, acc),
do: Stream.map(acc, &Map.put(&1, k, v))
end
input = %{
a: "a", b: "b",
c: [
%{d: "d1", e: [%{f: "f1", g: "g1"}], h: "h1", i: "i1"},
%{d: "d2", e: [%{f: "f2", g: "g2"}], h: "h2", i: "i2"}]
}
Flat.flatten()
#⇒ [
# %{a: "a", b: "b", d: "d1", f: "f1", g: "g1", h: "h1", i: "i1"},
# %{a: "a", b: "b", d: "d2", f: "f2", g: "g2", h: "h2", i: "i2"}
# ]
Здесь это сообщение в блоге, в котором подробно объясняется этот прием на примере загадки «Волк, Коза, Капуста» .