Ленивые вычисления в 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 уже никогда не будут выполнены!