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

Где функциональный подход окупается

Haskell выбирают для компиляторов, парсеров, финансовых расчётов, обработки потоков данных и backend-сервисов с жёсткими требованиями к корректности. Он не даёт «серебряную пулю»: старт медленнее, экосистема меньше, а найм дороже. Зато рефакторинг большого кода проходит спокойнее, потому что компилятор проверяет больше, чем тесты в динамическом языке.

  • Явные типы для домена: деньги, идентификаторы, единицы измерения.
  • Ошибки как значения Either вместо исключений.
  • Чистое ядро и тонкая IO-обвязка.

Инструменты: ghcup, cabal и редактор

Не смешивайте пакеты из дистрибутива с инструментами ghcup: версии GHC и библиотек начнут конфликтовать. ghcup ставит компилятор и cabal в домашний каталог, после чего проект собирается одинаково на любой машине.

curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
ghcup install ghc
ghcup install cabal
cabal update

В редакторе подключите Haskell Language Server, hlint и форматтер fourmolu. Это не роскошь: HLS показывает типы прямо в коде и ловит ошибки до сборки, а форматтер убирает споры о пробелах на ревью.

Типы как каркас программы

Начните с доменных типов, а не с Generic и сложных абстракций. newtype отличает email от обычной строки, а тип-сумма делает невозможные состояния непредставимыми.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DerivingStrategies #-}

newtype Email = Email Text
  deriving stock (Eq, Show, Ord)

data Role = Admin | Editor | Viewer
  deriving stock (Eq, Show)

data User = User
  { userName  :: Text
  , userEmail :: Email
  , userRole  :: Role
  } deriving stock (Eq, Show)

Смарт-конструктор переносит проверку в одно место и возвращает Either. Такой подход удобнее разрозненных if: вызывающий код обязан обработать ошибку, и компилятор не даст её проигнорировать.

Ошибки без исключений

mkEmail :: Text -> Either Text Email
mkEmail raw
  | "@" `Text.isInfixOf` raw && Text.length raw <= 254 =
      Right (Email (Text.toLower raw))
  | otherwise = Left "Некорректный адрес"

parseRole :: Text -> Either Text Role
parseRole "admin"  = Right Admin
parseRole "editor" = Right Editor
parseRole "viewer" = Right Viewer
parseRole other    = Left ("Неизвестная роль: " <> other)

Either удобен для последовательных проверок, но не для сбора всех ошибок сразу. Если нужно показать пользователю полный список, используйте Validation из библиотеки или аккуратно комбинируйте результаты через traverse.

Чистое ядро и IO на границе

Держите бизнес-логику в чистых функциях, а чтение файлов, сеть и время выносите в main. Тогда ядро тестируется без моков и запускается в ghci.

parseConfig :: Text -> Either Text [User]
parseConfig input =
  traverse parseLine (filter (not . Text.null) (Text.lines input))

parseLine :: Text -> Either Text User
parseLine line = case Text.splitOn ";" line of
  [name, mail, role] -> User name <$> mkEmail mail <*> parseRole role
  _ -> Left ("Ожидалось три поля: " <> line)

Applicative-стиль в parseLine читается как «собери пользователя из трёх проверок». Это стандартный способ комбинировать вычисления с ошибкой.

Ленивость, строгость и утечки памяти

Haskell ленив по умолчанию: выражение вычисляется только при необходимости. Это позволяет работать с бесконечными списками, но иногда приводит к накоплению отложенных вычислений и росту памяти. Для агрегатов используйте foldl’ из Data.List, строгие поля и bang patterns.

{-# LANGUAGE BangPatterns #-}

import Data.List (foldl')
import qualified Data.Map.Strict as Map

countWords :: [Text] -> Map.Map Text Int
countWords = foldl' (\acc w -> Map.insertWith (+) w 1 acc) Map.empty

summarize :: [Int] -> (Int, Int)
summarize = go 0 0
  where
    go !n !s []     = (n, s)
    go !n !s (x:xs) = go (n + 1) (s + x) xs

Data.Map.Strict вычисляет значения до WHNF при вставке, но не спасает от накопления thunk-ов внутри сложных значений. Для действительно горячих структур добавляйте строгие поля или включайте StrictData осознанно, проверив, что это не меняет семантику.

Сборка, тесты и профилирование

cabal init --interactive
cabal build all
cabal test --test-show-details=direct
cabal repl

В cabal-файле включите -Wall -Wcompat и превращайте предупреждения в ошибки только после того, как разберёте существующие. Прогоняйте сборку в CI на той же версии GHC, что и локально, иначе различия проявятся в самый неподходящий момент. Для поиска утечек соберите с профилированием и запустите с +RTS -s: статистика покажет время сборки мусора и пиковую память. Детальный профиль по функциям даёт +RTS -p.

Для property-тестов подойдёт QuickCheck: сформулируйте закон, например «декодирование после кодирования возвращает исходное значение», и пусть генератор ищет контрпримеры. Юнит-тесты на tasty или HUnit дополняют картину, но не заменяют свойства.

Конкурентность без гонок

Легковесные потоки GHC дешёвые, а Control.Concurrent.Async упрощает ожидание группы задач. Для общего состояния используйте STM: транзакции композируются и не требуют ручных блокировок.

import Control.Concurrent.Async
import Control.Concurrent.STM

main :: IO ()
main = do
  a <- async (fetch "https://example.com/a")
  b <- async (fetch "https://example.com/b")
  (ra, rb) <- waitBoth a b
  print (ra, rb)

enqueue :: TQueue Text -> Text -> IO ()
enqueue q item = atomically (writeTQueue q item)

STM не делает IO атомарным: сетевой вызов внутри atomically недопустим. Сначала получите данные, затем обновите состояние одной транзакцией. Такой же принцип помогает в тестах: чистая логика проверяется быстро, а взаимодействие с внешним миром остаётся тонким.

Чек-лист внедрения

  • Версии GHC и cabal зафиксированы, сборка воспроизводима.
  • Доменные типы важнее примитивов, проверки живут в смарт-конструкторах.
  • Чистое ядро отделено от IO.
  • Включены предупреждения и форматтер в CI.
  • Есть тесты свойств и профиль памяти на нагрузке.

Итог

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

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте