- PVSM.RU - https://www.pvsm.ru -

Эффективная сортировка данных типа Struct

Все, пришедшие в Elixir / Erlang из других языков, скорее всего, имеют некоторые ожидания относительно того, как должны работать операторы сравнения >, <, == и т. п. Можно было бы ожидать, что 1 < 2, (и это действительно так). В принципе, можно сказать, что сравнение работает как надо. Но не всегда.

В Elixir / Erlang можно сравнивать все что угодно. Вообще. В то время как для двух операндов одного типа результат не обескураживает, как в приведенном выше примере, сравнение двух операндов разных типов приводит к довольно неожиданным последствиям. Потому что сами по себе типы «упорядочены для сравнения». Вот таким образом:

number < atom < reference < function < port < pid < tuple < map < list < bitstring

Что внезапно приводит к тому, что полностью легитимное сравнение 42 < nil возвращает true.

Кроме того, maps (и, как следствие, structs, которые на самом деле те же maps под капотом) сравниваются в соответствии с их полями в алфавитном порядке. Иными словами, даты (которые реализованы в Elixir структурой Date [1]) будут сравниваться последовательно по значениям полей day, затем month, а затем year, а это, скорее всего, не совсем то, чего бы мы хотели.

Начиная с v1.10.0, Elixir обеспечивает удобную сортировку стандартным Enum.sort/2 [2], если структура экспортирует функцию compare/2:

defmodule User do
  defstruct [:name]
  def compare(%User{name: n1}, %User{name: n2}) when n1 < n2,
    do: :lt
  def compare(%User{name: n1}, %User{name: n2}) when n1 > n2,
    do: :gt
  def compare(%User{}, %User{}), do: :eq
end

users = [
  %User{name: "john"},
  %User{name: "joe"},
  %User{name: "jane"}
]

Enum.sort(users, {:asc, User})
#⇒ [%User{name: "jane"},
#   %User{name: "joe"},
#   %User{name: "john"}]

Любой модуль, который определяет struct, и экспортирует функцию compare/2, может быть передан как второй параметр в вызов Enum.sort/2 либо как есть, либо как кортеж {:asc | :desc, StructModule}. Enum.sort/2 теперь достаточно умен, чтобы вызвать функцию compare/2 [3] модуля, переданного Enum.sort/2. Ниже приведен отрывок из модуля Enum, реализующий именно этот функционал:

...
defp to_sort_fun(module) when is_atom(module),
  do: &(module.compare(&1, &2) != :gt)
defp to_sort_fun({:asc, module}) when is_atom(module),
  do: &(module.compare(&1, &2) != :gt)
defp to_sort_fun({:desc, module}) when is_atom(module),
  do: &(module.compare(&1, &2) != :lt)

Это позволяет правильно сортировать даты в приведенном ниже примере (спасибо существующей уже давно Date.compare/2), как и любую определенную пользователем структуру (если она экспортирует compare/2, конечно), как в примере со структурой User выше.

dates = [~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]

Enum.sort(dates) # wrong
#⇒ [~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]

Enum.sort(dates, {:asc, Date}) # correct
#⇒ [~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]

Удачных сортировок!

Автор: Aleksei Matiushkin

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/comparison/341832

Ссылки в тексте:

[1] Date: https://hexdocs.pm/elixir/master/Date.html

[2] Enum.sort/2: https://hexdocs.pm/elixir/master/Enum.html#sort/2-sorting-structs

[3] вызвать функцию compare/2: https://github.com/elixir-lang/elixir/blob/ee758f987b7e240754bf386b903b92d38fb02233/lib/elixir/lib/enum.ex#L2515-L2517

[4] Источник: https://habr.com/ru/post/482676/?utm_source=habrahabr&utm_medium=rss&utm_campaign=482676