Как стать автором
Обновить

Iteraptor: библиотека для глубокого прозрачного мап-редьюса

Время на прочтение5 мин
Количество просмотров1K

Структуры данных Elixir — иммутабельны. Это здорово с точки зрения уверенности в том, что наши данные не будут искорежены до неузнаваемости в каком-то другом нерелевантном куске кода, но это немного раздражает, когда нам нужно изменить глубоко вложенную структуру.


У нас есть блестящая абстракция Access, которая донельзя упрощает четыре основные операции на глубоко вложенных объектах, при помощи экспортируемых по умолчанию из Kernel функций:



Эти четыре мушкетера (и д’Артаньян Kernel.get_and_update_in/{2,3} обычно используются как-то так:


iex> users = %{"john" => %{age: 27, mood: ""}, "meg" => %{age: 23}}

# получить значение
iex> get_in(users, ["john", :age])
#⇒ 27

# записать значение
iex> put_in(users, ["john", :age], 28)
#⇒ %{"john" => %{age: 28, mood: ""}, "meg" => %{age: 23}}

# обновить значение
iex> update_in(users, ["john", :mood], & &1 + 1)
#⇒ %{"john" => %{age: 28, mood: ""}, "meg" => %{age: 23}}

# удалить значение
iex> pop_in(users, ["john", :mood])
#⇒ {"", %{"john" => %{age: 27}, "meg" => %{age: 23}}}

Это удобно, и работает во многих случаях… кроме тех, когда оно не работает. Чтобы использовать Access, необходимо знать путь к целевому элементу, и это все требует значительного количества шаблонного кода, чтобы разом обновить несколько вложенных значений (например, удалить все листья со значением nil, или зазвездить содержимое всех полей, которые не нужно показывать в логах).


Чтобы обеспечить оптовые скидки на работу со вложенными структурами, и была создана библиотека Iteraptor.


TL;DR:





Итерирование всего, что можно итерировать в Elixir волшебно. Чтобы сделать что-нибудь итерируемым, достаточно имплементировать протокол Enumerable для этого конкретного типа. Можно группировать проходы в пайплайны, маппить, редьюсить, фильтровать, прореживать… Простите мой французский, да. Каждый, кто провел с Elixir хотя бы восемь часов, определенно видел (и даже, возможно, написал) что-то вроде этого:


~w|саня ваня оля вера|
|> Enum.map(&String.capitalize/1)
|> Enum.each(fn capitalized_name ->
     IO.puts "Hello, #{capitalized_name}!"
   end)

# Hello, Саня!
# Hello, Ваня!
# Hello, Оля!
# Hello, Вера!

Это действительно очень удобно. Однако, код достаточно быстро становится громоздким, когда речь заходит о глубоко вложенных структурах, таких как map с вложенными keywords, списками и т. д. Хорошим примером этого может быть любой конфигурационный файл, содержащий вложенные подразделы.


Количество вопросов на Stack Overflow с вопросом «как я могу поменять вложенную структуру?» заставило меня, наконец, создать эту библиотеку. Реализация в Elixir выглядит немного запутанной, поскольку все вокруг — иммутабельно, и нельзя просто спуститься вниз по веткам структуры вплоть до листьев, изменяя все необходимое на месте. Потребуется аккумулятор, как, впрочем, под любым капотом функционального кода. Изменение вложенных структур — вероятно, единственный пример, который я встречал в своей жизни, когда мутабельность делает вещи проще.


В качестве бонуса к обычным мапредьюсам, я добавил реализацию для сохранения значения глубоко внутри структуры, которая создает промежуточные ключи по мере необходимости. Она ведет себя как предложенная, но отвергнутая в ruby core Hash#bury. Еще эта библиотека умеет «джейсонифицировать» вложенные структуры, содержащие keywords, которые не могут быть просто так сериализованы в json, поскольку внутри представлены как списки двухэлементных кортежей ([foo: :bar] == [{:foo, :bar}]), а кортежи не сериализуемы просто так из коробки.




Итак, поприветствуем библиотеку, которая в хвост и в гриву итерирует любой map / keyword / list почти так же просто, как и стандартные Enum.each/2 и Enum.map/2.


Возможности


  • Iteraptor.each/3 простая итерация, возвращает саму структуру;
  • Iteraptor.map/3 маппинг, возвращает замапленную структуру;
  • Iteraptor.reduce/4 редьюс, возвращает аккумулятор;
  • Iteraptor.map_reduce/4 мап и редьюс, возвращает кортеж с результатом маппинга и аккумулятором;
  • Iteraptor.filter/3 фильтрует структуру при помощи функции, полученной последним параметром;
  • Iteraptor.jsonify/2 подготавливает структуру для сериализации в json: все keywords заменяются на maps, ключи конвертируются в строки;
  • Iteraptor.Extras.bury/4 записывает значение вглубь структуры, создавая промежуточные ключи по мере необходимости;
  • Iteraptor.to_flatmap/2 превращает структуру в плоскую, заботливо создавая индексы для списков; оригинальную структуру можно получить обратно с использованием
  • Iteraptor.from_flatmap/3 превращает плоскуб структуру с конкатенированными ключами в глубоко вложенную;
  • use Iteraptor.Iteraptable автоматическая имплементация протоколов Enumerable и Collectable, а также Access behaviour для структуры. Есть более подробное описание того, что там под капотом (англ.).

Слова ничего не стоят, покажи код!


Итерирование, маппинг, редьюсинг


# each
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.each(&IO.inspect(&1, label: "each"), yield: :all)
# each: {[:a], %{b: %{c: 42}}}
# each: {[:a, :b], %{c: 42}}
# each: {[:a, :b, :c], 42}
%{a: %{b: %{c: 42}}}

# map
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.map(fn {k, _} -> Enum.join(k) end)
%{a: %{b: %{c: "abc"}}}

iex> %{a: %{b: %{c: 42}, d: "some"}}
...> |> Iteraptor.map(fn
...>      {[_], _} = self -> self
...>      {[_, _], _} -> "********"
...>    end, yield: :all)
%{a: %{b: "********", d: "some"}}

# reduce
iex> %{a: %{b: %{c: 42}}}
...> |> Iteraptor.reduce([], fn {k, _}, acc ->
...>      [Enum.join(k, "_") | acc]
...>    end, yield: :all)
...> |> :lists.reverse()
["a", "a_b", "a_b_c"]

# map-reduce
iex> %{a: %{b: %{c: 42}}}
...> |> Iteraptor.map_reduce([], fn
...>      {k, %{} = v}, acc -> {​{k, v}, [Enum.join(k, ".") | acc]}
...>      {k, v}, acc -> {​{k, v * 2}, [Enum.join(k, ".") <> "=" | acc]}
...>    end, yield: :all)
{​%{a: %{b: %{c: 42}}}, ["a.b.c=", "a.b", "a"]}

# filter
iex> %{a: %{b: 42, e: %{f: 3.14, c: 42}, d: %{c: 42}}, c: 42, d: 3.14}
...> |> Iteraptor.filter(fn {key, _} -> :c in key end, yield: :none)
%{a: %{e: %{c: 42}, d: %{c: 42}}, c: 42}

Глубокая вложенность → плоская структура и обратно


iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
...> |> Iteraptor.to_flatmap(delimiter: "_")
#⇒ %{"a_b_c" => 42, "a_b_d_0" => nil, "a_b_d_1" => 42, "a_e_0" => :f, "a_e_1" => 42}

iex> %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
...> |> Iteraptor.from_flatmap
#⇒ %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}

Плюшки


iex> Iteraptor.jsonify([foo: [bar: [baz: :zoo], boo: 42]], values: true)
%{"foo" => %{"bar" => %{"baz" => "zoo"}, "boo" => 42}}

iex> Iteraptor.Extras.bury([foo: :bar], ~w|a b c d|a, 42)
[a: [b: [c: [d: 42]]], foo: :bar]



Исходники открыты, документация довольно подробная, у нас в продакшене уже почти два года.


Удачных итераций!

Теги:
Хабы:
+4
Комментарии0

Публикации