Прежде чем принять решение о переходе на Haskell, я выполнил один пробный проект. Я портировал Ариадну с JavaScript на Haskell (GHCJS).

Ariadne — это библиотека для поиска минимального CSS-запроса, который однозначно идентифицирует заданный элемент DOM. На нем работает регистратор действий пользователя в инструменте тестирования Siesta JS и сервисе RootCause, разработанном нами в Бринтум.

Минимальный запрос означает, что он должен содержать как можно меньше сегментов CSS, поэтому запрос:

.cssClass

меньше, чем

.cssClass1 .cssClass2

Если вы подумаете некоторое время, как бы вы это сделали, это не так просто. Вам нужно будет сгенерировать список сегментов CSS для данного элемента и его родителей, а затем выполнить поиск в огромном комбинаторном пространстве возможных кандидатов на запрос. Я опущу подробности, но мы нашли довольно производительный алгоритм, который обычно находит результат за ‹100 мс даже для больших деревьев DOM.

Я бы назвал это алгоритмом средней сложности. Около 30 классов, многие из которых очень маленькие. Я реализовал примерно в функциональном стиле, сводя к минимуму изменение состояния. Тем не менее, потребовался месяц, чтобы сделать надежную реализацию на JavaScript с надлежащим набором тестов, обычным этапом отладки в несколько дней и т. д.

Поэтому я портировал его на GHCJS, как на свой первый проект на «детском хаскеле». В нем не было ничего необычного, никаких контравариантов, зависимых типов, просто довольно близкое преобразование из императивной версии.

Ключевым наблюдением было следующее: после проверки типов кода (без ошибок компиляции) он в основном работал!

После этого этапа нужно было исправить 2 ошибки:

  • В одном месте функция возвращала список списков: [ [a] ]. Базовым случаем рекурсии для такого типа должен быть список с пустым списком: [ [] ] , а не просто пустой список [] Из-за этой ошибки результатом функции всегда был пустой список. Немного нетривиально для меня как для новичка, но теперь я знаю закономерность.
  • В другом месте сортировка выполнялась в неправильном направлении (по возрастанию или по убыванию), поэтому результаты были в обратном порядке. Тривиальный.

Не было необходимости в длительном периоде отладки, как это обычно бывает с алгоритмами такого размера в императивном мире. Это просто работало, даже без набора тестов. Я не то чтобы ратую за то, что в Haskell нет необходимости в тестах, я просто уточняю, что без тестов императивная версия писалась бы гораздо дольше, но в функциональном мире она работала и без нее.

Так что слухи о том, что программы на Haskell улавливают гораздо больше внутренней семантики и что «если компилируется, значит работает», верны. Не буквально, поскольку всегда есть место для реальных ошибок, таких как неправильный порядок сортировки, но все измерение обычных императивных ошибок (воздействие на состояние) действительно отсутствует в функциональном мире.

Опять же, императивный алгоритм, на создание которого ушло 1 человеко-месяц, сработал в тот же день после того, как дошел до стадии проверки типов.

Очень сложно или даже невозможно программировать на Haskell, когда вы не знаете, зачем пишете эту строчку кода. Вместо этого типы мягко проводят вас через поток управления, предоставляя много статических (время компиляции) гарантий относительно кода.

Это светлая сторона Haskell!

Как вы, наверное, догадались, следующий пост будет о темной стороне :)

Быть в курсе!