Ленивые вычисления в Elixir с помощью Stream

Небольшая заметка о чрезвычайно полезных ленивых Streams

Когда я только познакомился с этим модулем, то не сразу понял где это может пригодиться мне.

Нет, ну, конечно, в голове у меня возникли некоторые ассоциации с RxJs, который я когда-то немного полапал, но в целом, на тот момент я не смог придумать достойного применения этой штуки.

Однако!

Стримы пригодились мне при разработке первого же приложения не из гайда или книги. Это небольшой проект по определению CMS (движка сайта), и пусть я и следовал гайдам и читал ооочень много дополнительной инфы до этого (особенно о GenStage), моя теория об эффективном изучении языков снова нашла подтверждения.

Вся суть

Всё приложение построено на двух моментах — GenStage проверяет список урлов через эрланговский inet_res:gethostbyname, на валидность и доступность, фильтруя тем самым список того, что мы в итоге будем загонять в контроллер парсеров CMS, и, соответственно, прогоняющий все доступные парсеры.

Парсер выполняет функции двух типов: парсинг содержимого HTML главной страницы (большинство SAAS сервисов палится уже на этом этапе), и проверка доступности/контента по другим адресам (некоторые могут и то и другое).

Так вот

На начальном этапе, когда я проверял на одну/две CMS, проблем никаких не было, однако, в один прекрасный момент я психанул, и, просто экспромтом, навалял 12 модулей для разных CMS и ещё 9 для SAAS сервисов.

Вот тут-то и выяснились стрёмные баги интересные особенности моего приложения…

Самая глупая ошибка заключалась как раз в использовании Enum функций в критических местах — парсеры выполняются в рекурсии — следующий парсер запускается только если текущий вернул false, однако функции проверки (которых у каждого может быть от 2 до 6-8) были реализованы через Enum.map — что при наличии 21 парсера вылилось в огромные временные затраты.

Вот пример проверки по Url (да-да, это — 🤦‍♂️):

def batch_check_urls(base_url, urls, content_type \\ nil) do
  parsed_url = URI.parse(base_url)
  # проверяем урлы по статусу 200 и content-type
  urls |> Enum.map(fn(path) ->
    URI.merge(parsed_url, path)
    |> to_string
    |> Net.load_page?(content_type)
  end)
  |> Enum.drop_while(&(not &1)) # совершенно ненужная функция здесь
  |> Enum.any?(&(&1))
end

Основная проблема этой функции, помимо лишних функций (Enum.filter тут особо не даст прироста), заключается в том, что она прогоняет все проверочные урлы вне зависимости от результата.

Вот так выглядит оптимизированная версия этой функции:

def batch_check_urls(base_url, urls, content_type \\ nil) do
  parsed_url = URI.parse(base_url)

  urls |> Stream.filter(fn(path) ->
    URI.merge(parsed_url, path)
    |> to_string
    |> Net.load_page?(content_type)
  end)
    |> Enum.any?
end

Как видите, от map я отказался в пользу filter, а Enum заменил на Stream. Что это даёт нам? — всё просто, как только мы получаем положительный результат в Enum.any? — все остальные проверки отменяются, т.е. в моем случае, если есть 20 проверок и совпала, например, вторая — остальные 18 уже никогда не будут выполнены!

Комментарии

comments powered by Disqus
Яндекс.Метрика