Раздел 21 · Haskell

Зачем Haskell. GHCup, cabal и первая программа

junior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Зачем Haskell. GHCup, cabal и первая программа

Открываем раздел по Haskell. Чисто функциональный, ленивый, со строгой статической типизацией. Язык, который не даст писать как раньше, и в этом его ценность. В этом уроке ставим инструментарий 2026 года (GHCup, cabal, HLS), запускаем первую программу и на пальцах разбираем три столпа: чистоту, ленивость и типы. Бонусом лямбда-исчисление за 15 минут.

Идея

Честный вопрос перед стартом: зачем тратить недели на язык, на котором ты, скорее всего, не будешь писать на работе?

Затем же, зачем музыканты играют гаммы. Haskell это эталон функционального программирования со статической типизацией: всё, что ты трогал в ФП-серии раздела 02, в Effect и в Gleam, здесь не библиотека и не стиль, а сам язык. LINQ в C#, Streams в Java, ranges в C++, ramda и fp-ts в JS, алгебраические типы в Rust и Gleam: всё это идеи, которые сначала обкатали в Haskell, а потом растащили по экосистемам. Авторы этих инструментов источник вдохновения не скрывают. Выучив оригинал, ты начинаешь видеть, откуда растут “лучшие практики” твоего основного языка, и применять их не по карго-культу, а по смыслу.

Есть и обратная сторона: после пяти лет на императивных языках в голове накатаны ментальные рельсы. “Заведи переменную, обнови её в цикле, верни”. Haskell с этих рельсов сталкивает принудительно: переменные не меняются, циклов нет, эффекты помечены в типах. Первую неделю это бесит. Потом оказывается, что код в стиле “поток сознания” сюда просто не влезает, а влезает только структурированный: маленькие функции, явные данные, отделённые эффекты. Этот навык переносится в любой язык.

И сразу разберёмся с мифом: Haskell это не язык для математиков. Теория категорий для работы с ним не нужна, лямбда-исчисление мы пройдём за 15 минут в конце этого урока, и этого хватит. На Haskell написаны компиляторы (сам GHC, Elm, PureScript, Agda), спам-фильтр Facebook (правила на DSL, который компилируется в машинный код), системы высокочастотного трейдинга, банковские бэкенды, анализаторы кода. Это язык общего назначения, с необычным набором умолчаний.

Столп первый: чистота

При вызове чистой функции результат зависит только от входных параметров. Одни и те же аргументы, одно и то же значение на выходе, и никакого влияния на состояние системы. Функция, которая нарушает это, имеет побочные эффекты, и они размазывают её результат по системе: запись в БД, HTTP-запрос, вывод в консоль, чтение текущего времени.

Ты уже знаешь из урока про четыре столпа ФП, что суть не в тотальном уничтожении эффектов. Любая полезная программа читает и пишет во внешний мир. Суть в стратегии “разделяй и властвуй”: эффекты вытесняются на периферию, а посередине остаётся чистое ядро. Бизнес-логика, алгоритмы, преобразования данных, которые ничего не знают про базы и сеть. Вокруг ядра тонкая оболочка: получила сырые данные снаружи, прогнала через ядро, выполнила эффекты.

Что это даёт на практике:

  • Тесты без моков. Чистая функция тестируется как чёрный ящик: передал значения, проверил результат. Не нужно поднимать БД, симулировать сеть, выставлять состояние системы.
  • Бесплатная мемоизация. Результат зависит только от входа, значит его можно кешировать по входу.
  • Потокобезопасность. Нечему гоняться, если никто ничего не меняет.
  • Локальные рассуждения. Чтобы понять функцию, достаточно прочитать функцию. Не нужно держать в голове, кто и когда дёрнет её с другим глобальным состоянием.

В JS и TS всё это держится на дисциплине: язык не мешает воткнуть fetch в середину “чистого” маппера. В Haskell деление вшито в сам язык. Операция с внешним миром называется действием ввода-вывода и носит тип IO. Функция Int -> Int физически не может сходить в сеть: тип не позволяет. Компилятор здесь работает внимательным ментором, который не даст написать код, в котором легко ошибиться.

double :: Int -> Int               -- чистая: только вход и выход
readNotes :: FilePath -> IO String -- эффект честно записан в типе

Столп второй: ленивость

В привычных языках аргументы функции вычисляются до её вызова. Haskell откладывает любое вычисление до момента, когда результат реально понадобится. Если значение никто не спросил, оно не считается вообще.

Из этого следуют вещи, которые на энергичном языке выглядят фокусом. Например, бесконечные структуры данных:

odds :: [Integer]
odds = [1, 3 ..]          -- все нечётные числа, без конца

take 5 odds               -- [1, 3, 5, 7, 9]

Список бесконечный, но программа завершается мгновенно: вычислились ровно пять элементов, которые попросил take. Это не трюк для собеседований, а инструмент модульности: можно отдельно описать “все варианты” и отдельно “сколько и каких мне нужно”, и склеить. Подробно мы разбирали стратегии вычислений в уроке про ленивость, а устройство ленивости в Haskell (thunk, WHNF, sharing) ждёт в уроке 08.

Заметь, как ленивость цепляется за чистоту. Если выражения могут иметь побочные эффекты, откладывать их вычисление страшно: непонятно, когда и в каком порядке эффекты выстрелят. Ленивым языку можно быть только чистым.

Столп третий: типы

Третий столп ты уже видел в действии в Effect и Gleam: алгебраические типы данных, паттерн-матчинг, вывод типов. В Haskell всё это в самой богатой форме. Два акцента на сегодня.

Первый: типы выводятся. Можно написать функцию без единой аннотации, и компилятор сам вычислит самый общий тип. Сигнатуры ты всё равно пишешь, но как документацию, а не как дань компилятору.

Второй: типы здесь конструктор, а не клетка. Главный приём раздела звучит как “невозможные состояния невыразимы”: тип проектируется так, что невалидное значение нельзя даже собрать. К этому придём всерьёз в уроке 06, и этот же словарь ты уже встречал в Decider-серии Gleam. Следствие для ежедневной работы: рефакторинг перестаёт быть прыжком веры. Поменял тип, компилятор перечислил все места, которые перестали сходиться, починил их по списку, работает. Код на Haskell можно не трогать два года, вернуться, и он не сгнил.

Три столпа держат друг друга: ленивость требует чистоты, чистота держится на типах (эффект виден в сигнатуре), а типы становятся по-настоящему полезными, когда программа это выражение, а не последовательность инструкций. Поэтому вынуть один кусок и перенести в другой язык целиком не получается, и поэтому стоит пожить в системе, где собраны все три.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.