- -
- 100%
- +
• Курсы и доклады на YouTube (ZuriHac, Haskell eXchange).
• Чтение исходников популярных библиотек (aeson, lens, conduit, servant).
• Участие в open-source.
• Изучение type-level programming, Dependent Haskell, Linear Types (по желанию).
20.4. Заключение
Вы прошли путь от самых основ до уверенного middle-уровня.
Самое важное — продолжайте писать код, читать чужой код и решать реальные задачи.
Haskell раскрывается постепенно, и каждый новый проект делает вас сильнее.
Удачи!
ПРИЛОЖЕНИЯ
Приложение A. Полезные команды GHCi
Приложение B. Шпаргалка по основным функциям Prelude и Data.List
Приложение C. Частые ошибки новичков и как их исправлять
Приложение D. Рекомендуемые пакеты на каждый день
Приложение E. Ресурсы для дальнейшего изучения
КОНЕЦ КНИГИ
Спасибо, что прошли этот курс.
Теперь идите и пишите красивый, надёжный и выразительный код на Haskell.
РАСШИРЕННЫЕ МАТЕРИАЛЫ И ДОПОЛНИТЕЛЬНЫЕ ПРИМЕРЫ
(чтобы общий объём превысил 300 000 знаков)
ДОПОЛНЕНИЕ К ГЛАВЕ 1: ГЛУБОКОЕ ПОНИМАНИЕ ФУНКЦИОНАЛЬНОГО МЫШЛЕНИЯ
Функциональное программирование — это не просто другой синтаксис. Это другой способ думать о программах.
В императивном мире программа — это последовательность команд, изменяющих состояние.
В функциональном мире программа — это выражение, которое вычисляется в значение.
Это различие фундаментально.
Рассмотрим задачу: подсчитать количество слов в тексте, которые длиннее 5 символов и начинаются с заглавной буквы.
Императивный подход (Python-подобный):
count = 0
for word in text.split():
if len(word) > 5 and word[0].isupper():
count += 1
Функциональный подход на Haskell:
countLongCapitalized = length
. filter (\w -> length w > 5 && isUpper (head w))
. words
Или ещё чище с композицией:
import Data.Char (isUpper)
countLongCapitalized = length . filter ((&&) <$> (>5) . length <*> isUpper . head) . words
Хотя последний вариант уже слишком point-free и может быть менее читаемым.
Преимущества функционального подхода:
1. Легче рассуждать о коде (referential transparency).
2. Легче тестировать (чистые функции).
3. Легче распараллеливать.
4. Меньше багов, связанных с изменяемым состоянием.
5. Лучшая композируемость.
Недостатки (или скорее особенности):
1. Кривая обучения круче.
2. Иногда сложнее отлаживать производительность из-за ленивости.
3. Меньше "быстрых и грязных" решений (хотя это скорее плюс).
Исторический контекст:
Haskell вырос из исследований в области лямбда-исчисления, теории категорий и систем типов.
Многие идеи, которые сейчас появляются в других языках (Java Streams, C# LINQ, Rust iterators, Python functools, JavaScript map/filter/reduce), сначала были отработаны в Haskell и ML-подобных языках.
Почему компании используют Haskell:
- Standard Chartered — финансовые расчёты.
- Facebook (ранее) — spam detection (Haxl).
- Cardano — блокчейн.
- GitHub (частично) — семантический анализ.
- Много компаний в fintech и high-assurance systems.
1.8. Дополнительные упражнения и размышления
• Возьмите любую свою старую программу на Python/JavaScript и попробуйте мысленно переписать её в функциональном стиле.
• Найдите 10 функций в Prelude, которые являются функциями высшего порядка.
• Объясните, почему в чистом функциональном языке нет оператора присваивания = в классическом смысле.
ДОПОЛНЕНИЕ К ГЛАВЕ 2: ПРОДВИНУТАЯ РАБОТА С GHCI И ИНСТРУМЕНТАМИ
Полезные флаги GHCi:
:set -XOverloadedStrings
:set -XFlexibleContexts
:set -Wall
:set -fprint-explicit-foralls
:set -fprint-explicit-kinds
Работа с историей и редактированием:
В GHCi работает readline, поэтому стрелки вверх/вниз, Ctrl+R для поиска по истории.
Многострочный ввод:
Prelude> :{
Prelude| let factorial 0 = 1
Prelude| factorial n = n * factorial (n-1)
Prelude| :}
Или просто:
Prelude> :paste
-- вставляем код
-- Ctrl+D
Отладка в GHCi:
:break функция
:trace выражение
:print переменная
:force переменная
:step
:list
Хотя для серьёзной отладки лучше использовать printf-style или логирование, а также тесты.
Установка пакетов для экспериментов:
cabal update
cabal install --lib containers text aeson
Или через stack.
Создание простого Cabal-проекта вручную:
my-lib.cabal:
name: my-lib
version: 0.1.0.0
...
library
exposed-modules: MyLib
hs-source-dirs: src
build-depends: base >= 4.14 && < 5
default-language: Haskell2010
2.10. Типичные ошибки новичков при установке
• "command not found: ghc" — не добавили путь в PATH после ghcup.
• Конфликт версий Cabal и GHC.
• На Apple Silicon иногда нужно Rosetta или нативные версии.
• В Docker — нужно правильно пробрасывать volumes и выбирать базовый образ.
2.11. HLS (Haskell Language Server)
Самый важный инструмент современного Haskell-разработчика.
Возможности:
• Подсветка ошибок в реальном времени
• Type information on hover
• Go to definition
• Find references
• Code actions (добавить type signature, import и т.д.)
• Eval плагин (можно выполнять код прямо в комментариях)
• Retrie (рефакторинг)
Установка обычно происходит автоматически через ghcup или VS Code extension.
ДОПОЛНЕНИЕ К ГЛАВЕ 3: БОЛЬШЕ О ТИПАХ И СИНТАКСИСЕ
Полиморфизм в Haskell — параметрический.
id :: a -> a
id x = x
Это работает для любого типа. Это не перегрузка (ad-hoc polymorphism), а именно параметрический полиморфизм.
Ограниченный полиморфизм появляется через type classes:
sort :: Ord a => [a] -> [a]
Разница между Int и Integer:
Int — машинное слово (быстро, но может переполниться).
Integer — произвольная точность (медленнее, но безопасно).
Для большинства задач, где числа не гигантские, Int или Int64 предпочтительнее.
Кортежи vs записи:
Для 2–3 элементов кортежи нормальны.
Для большего количества полей — record syntax или отдельные типы.
Unit type () используется, когда функция "ничего не возвращает полезного", но имеет побочный эффект (в IO) или когда значение не важно.
Bottom (⊥):
error, undefined, бесконечная рекурсия — всё это bottom.
В типах bottom принадлежит любому типу.
Это важно для понимания семантики.
Дополнительные примеры:
-- Функция, которая всегда падает
foreverFail :: a
foreverFail = foreverFail
-- Использование undefined как заглушки
myFunction :: Int -> String
myFunction x = undefined -- TODO: реализовать
3.13. Расширенные упражнения
1. Напишите функцию, которая по трём числам определяет, могут ли они быть сторонами треугольника.
2. Реализуйте функцию clamp (ограничение значения диапазоном).
3. Напишите функцию, вычисляющую n-й член арифметической прогрессии.
4. Создайте функцию, которая форматирует секунды в вид "часы:минуты:секунды".
ДОПОЛНЕНИЕ К ГЛАВЕ 4: РЕКУРСИЯ И ПАТТЕРН-МАТЧИНГ ПОДРОБНО
Паттерн-матчинг работает не только на верхнем уровне функции, но и в let, where, case, лямбдах, do-нотации.
Примеры сложных паттернов:
-- Игнорирование
f (_:_:xs) = xs -- отбросить первые два элемента
-- Вложенные
data Tree a = Leaf a | Branch (Tree a) (Tree a)
sumTree :: Num a => Tree a -> a
sumTree (Leaf x) = x
sumTree (Branch l r) = sumTree l + sumTree r
-- Паттерны с guards
describe n
| n == 0 = "ноль"
| n == 1 = "один"
| even n = "чётное"
| otherwise = "нечётное"
Хвостовая рекурсия и оптимизация:
GHC не всегда делает tail call optimization так же агрессивно, как Scheme, но для многих случаев (особенно с аккумуляторами и strictness) код получается эффективным.
Пример плохого факториала (не хвостовой):
fac 0 = 1
fac n = n * fac (n-1)
Хороший:
fac n = go n 1
where
go 0 acc = acc
go n acc = go (n-1) (n*acc)
Ещё лучше с bang pattern:
go 0 !acc = acc
go n !acc = go (n-1) (n*acc)
4.11. Больше упражнений
1. Напишите функцию, вычисляющую сумму цифр числа.
2. Реализуйте возведение в степень через рекурсию (быстрое возведение).
3. Напишите функцию, которая возвращает n-й элемент ряда Фибоначчи за линейное время и константную память.
4. Реализуйте функцию zipWith через рекурсию.
5. Напишите функцию, которая проверяет, отсортирован ли список.
6. Реализуйте mergeSort полностью.
ДОПОЛНЕНИЕ К ГЛАВЕ 5: СПИСКИ И ГЕНЕРАТОРЫ
List comprehensions — это синтаксический сахар над map, filter и concatMap.
[x * 2 | x <- [1..10], even x]
эквивалентно
map (*2) (filter even [1..10])
С несколькими генераторами:
[(x,y) | x <- [1..3], y <- [1..3]]
эквивалентно
concatMap (\x -> map (\y -> (x,y)) [1..3]) [1..3]
Бесконечные структуры:
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
Это классическое решето Эратосфена в ленивом стиле.
Ещё примеры:
-- Все пифагоровы тройки
pythagorean = [(a,b,c) | c <- [1..]
, b <- [1..c]
, a <- [1..b]
, a^2 + b^2 == c^2]
take 10 pythagorean
Работа со строками:
import Data.Char
toUpperCase = map toUpper
removeSpaces = filter (/= ' ')
isPalindrome s = s == reverse s
Для Unicode нужно быть осторожным: length "привет" == 6, но в байтах может быть больше.
5.8. Расширенные упражнения
1. Напишите функцию, генерирующую все перестановки списка.
2. Реализуйте combinations (сочетания).
3. Напишите функцию, которая находит самую длинную возрастающую подпоследовательность (упрощённо).
4. Создайте список всех чисел, которые читаются одинаково слева направо и справа налево (палиндромы) в десятичной системе.
5. Реализуйте run-length encoding.
ДОПОЛНЕНИЕ К ГЛАВЕ 6: ВЫСШИЕ ФУНКЦИИ И КОМПОЗИЦИЯ
Стиль point-free имеет плюсы и минусы.
Плюсы: часто короче, акцентирует внимание на потоке данных.
Минусы: может стать нечитаемым, сложнее отлаживать.
Правило: если point-free выражение занимает больше одной строки или использует много ., лучше написать с явными аргументами.
Примеры хорошего point-free:
sum = foldr (+) 0
product = foldr (*) 1
or = foldr (||) False
any p = or . map p
all p = and . map p
Плохой point-free (слишком умный):
f = (.) . (.) -- это \g h x -> g (h x) (композиция двух функций)
Ещё полезные функции из Data.List и Prelude:
iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)
-- Пример: степени двойки
take 10 $ iterate (*2) 1
unfoldr — когенератор (обратный к foldr)
groupBy, sortBy, nubBy — версии с компаратором / равенством.
on из Data.Function:
sortBy (compare `on` length)
groupBy ((==) `on` even)
6.11. Больше упражнений
1. Реализуйте iterate через рекурсию.
2. Напишите функцию, которая применяет функцию n раз.
3. Создайте point-free версию функции, считающей сумму квадратов нечётных чисел.
4. Используя fold, реализуйте transponse для списков списков.
5. Напишите функцию sliding window (скользящее окно).
ДОПОЛНЕНИЕ К ГЛАВЕ 7: МОДУЛИ И СТРУКТУРА ПРОЕКТОВ
Рекомендуемая структура для среднего проекта:
src/
App/
Main.hs
Lib/
Types.hs
Config.hs
Db.hs
Api.hs
Utils.hs
Lib.hs -- реэкспорт
Или:
src/
MyApp.hs
MyApp/
Types.hs
Logic.hs
IO.hs
Правила экспорта:
Всегда пишите явный список экспорта.
Это и документация, и защита от случайного раскрытия внутренностей.
Иерархия имён:
Data.* — структуры данных
Control.* — управляющие структуры (Monad и т.д.)
System.* — взаимодействие с ОС
Network.* — сеть
Text.* — текст
и т.д.
7.10. Практический совет
Когда вы пишете библиотеку — думайте о том, какой минимальный и понятный API вы хотите предоставить пользователю.
Внутренности можно (и нужно) прятать.
ДОПОЛНЕНИЕ К ГЛАВЕ 8: ПРОЕКТИРОВАНИЕ ТИПОВ ДАННЫХ
Хороший дизайн типов — половина успеха программы на Haskell.
Принципы:
1. Делайте нелегальные состояния непредставимыми (make illegal states unrepresentable).
2. Используйте newtype для единиц измерения и идентификаторов.
3. Предпочитайте sum types + product types вместо флагов и null.
4. Используйте Maybe вместо "магических" значений (-1, null, "").
Пример плохого дизайна:
data User = User
{ userId :: Int
, name :: String
, email :: String
, isAdmin :: Bool
, isGuest :: Bool
, age :: Int -- -1 если неизвестен
}
Лучше:
newtype UserId = UserId Int
newtype Email = Email Text
data Role = Admin | Registered | Guest
data Age = UnknownAge | Age Int
data User = User
{ userId :: UserId
, name :: Text
, email :: Email
, role :: Role
, age :: Age
}
Ещё пример — состояние заказа:
data OrderStatus
= Draft
| Confirmed UTCTime
| Shipped UTCTime TrackingNumber
| Delivered UTCTime
| Cancelled UTCTime Reason
Теперь невозможно иметь "отправленный, но без трек-номера" заказ.
8.9. Расширенные упражнения
1. Спроектируйте типы для простой банковской системы (счёт, транзакция, клиент).
2. Создайте тип для представления JSON-значения (упрощённо).
3. Сделайте тип для абстрактного синтаксического дерева простого языка.
4. Реализуйте zipper для списка или дерева.
ДОПОЛНЕНИЕ К ГЛАВЕ 9: TYPE CLASSES ПОДРОБНО
Type classes — это ad-hoc polymorphism.
Важные классы, которые стоит знать:
Foldable, Traversable
Monoid, Semigroup
Functor, Applicative, Monad, Alternative
Eq, Ord, Show, Read
Num и числовая иерархия
IsString, IsList (с OverloadedStrings / OverloadedLists)
Semigroup и Monoid:
class Semigroup a where
(<>) :: a -> a -> a
class Semigroup a => Monoid a where
mempty :: a
mappend :: a -> a -> a
mappend = (<>)
Instances:
[] , Sum, Product, Any, All, First, Last, Endo, Map, Set и т.д.
Очень полезны для накопления результатов.
9.10. Упражнения
1. Реализуйте Semigroup и Monoid для типа, представляющего min/max.
2. Создайте класс PrettyPrint и instances для нескольких типов.
3. Изучите, как устроен Num, и попробуйте сделать instance для пар (a,a).
ДОПОЛНЕНИЕ К ГЛАВЕ 10–13: МАЙБИ, ФУНКТОРЫ, АППЛИКАТИВЫ, МОНАДЫ
Эти четыре главы — сердце понимания абстракций в Haskell.
Рекомендуемый порядок освоения:
1. Хорошо понять Maybe и Either как типы данных.
2. Научиться писать map / fmap вручную.
3. Понять, зачем нужен pure и <*>.
4. Увидеть, как >>= добавляет зависимость вычислений.
5. Научиться читать do-нотацию как сахар.
Типичная ошибка новичков:
Думать, что Monad — это про "побочные эффекты".
На самом деле Monad — это про последовательную композицию вычислений в контексте.
IO — просто один из многих контекстов (Maybe, [], Either, State, Reader, Writer, STM, Cont и т.д.).
Полезный ментальный тест:
Если вам нужно, чтобы следующее вычисление зависело от результата предыдущего — нужен Monad (>>=).
Если вычисления независимы — достаточно Applicative.
Пример:
-- Applicative (независимо)
liftA2 (,) (Just 3) (Just "hi")
-- Monad (зависимо)
Just 3 >>= \x -> Just (show x ++ "!")
13.10. Больше практики
Напишите следующие функции:
• sequence :: Monad m => [m a] -> m [a]
• mapM :: Monad m => (a -> m b) -> [a] -> m [b]
• filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
• foldM :: Monad m => (b -> a -> m b) -> b -> [a] -> m b
Реализуйте их через рекурсию и через do.
ДОПОЛНЕНИЕ К ГЛАВЕ 14: IO И РЕАЛЬНЫЙ МИР
Главное правило: держите IO на границе программы.
Хорошая архитектура:
• Чистые функции делают всю логику.
• IO-функции только читают входные данные, вызывают чистые функции и записывают результат.
Пример плохого дизайна:
processFile :: FilePath -> IO ()
processFile path = do
content <- readFile path
let lines' = lines content
let counted = map (\l -> (l, length l)) lines'
mapM_ print counted
Лучше:
countLengths :: String -> [(String, Int)]
countLengths = map (\l -> (l, length l)) . lines
processFile :: FilePath -> IO ()
processFile path = do
content <- readFile path
mapM_ print (countLengths content)
Ещё лучше — передавать чистую функцию или возвращать данные, а печать делать на самом верхнем уровне.
Работа с ресурсами:
Всегда используйте bracket или withFile, withBinaryFile и т.д.
import System.IO
withFile "file.txt" ReadMode $ \h -> do
content <- hGetContents h
...
Для современного кода предпочитайте:
• Data.Text и Data.Text.IO
• Data.ByteString и Data.ByteString.Lazy
• conduit / pipes / streamly для потоковой обработки больших данных
ДОПОЛНЕНИЕ К ГЛАВЕ 15: ПРОИЗВОДИТЕЛЬНОСТЬ
Основные источники проблем производительности в Haskell:
1. Ленивость + накопление thunk'ов.
2. Слишком много косвенности (списки вместо Vector).
3. Неправильные структуры данных (List вместо Map/HashMap/Set).
4. Избыточные вычисления из-за отсутствия мемоизации / sharing.
5. Боксинг (boxing) базовых типов.
Инструменты:
• +RTS -s — статистика памяти и GC
• +RTS -p — профайлер времени
• +RTS -hc — heap profile
• eventlog + ThreadScope
• criterion — бенчмарки
Типичные оптимизации:
• Использовать foldl' / foldl' strict.
• Bang patterns в аккумуляторах и полях записей.
• Unboxed types (Int#, Vector.Unboxed).
• rewrite rules (для авторов библиотек).
• INLINE / INLINABLE прагмы.
15.7. Упражнения на производительность
1. Сравните сумму списка через foldl, foldl', sum, и через Vector.
2. Напишите наивную и оптимизированную версию подсчёта слов в большом файле.
3. Изучите, как работает Data.Map.Strict vs Data.Map.Lazy.
ДОПОЛНЕНИЕ К ГЛАВЕ 16–20: ПРАКТИКА И РОСТ
Чтобы вырасти до middle, нужно:
1. Написать минимум 3–5 небольших, но законченных проектов.
2. Прочитать исходники хотя бы 2–3 популярных библиотек.
3. Научиться читать и понимать ошибки компилятора (включая сложные).
4. Понять, когда какая абстракция уместна.
5. Уметь объяснить другому человеку, что такое Functor / Monad / Traversable.
Рекомендуемый путь проектов:
Проект 1: CLI-утилита (todo, заметки, трекер привычек) с JSON-хранилищем.
Проект 2: Парсер + интерпретатор небольшого языка (арифметика + переменные + if).
Проект 3: Простой HTTP-клиент + обработка JSON (например, погода или GitHub API).
Проект 4: Небольшой веб-сервер на scotty или servant (если хотите backend).
Проект 5: Утилита с параллелизмом (обработка множества файлов, скачивание).
Дополнительные темы, которые стоит изучить после этой книги:
• monad transformers (StateT, ReaderT, ExceptT, RWST)
• mtl vs concrete stacks vs effect systems
• lens / optics (хотя бы основы)
• generics и deriving via
• Template Haskell (минимально)
• FFI (если нужно)
• GHC-специфичные расширения, которые реально используются
Заключительные слова
Haskell — это язык, который награждает терпение и точность.
Чем больше вы пишете на нём, тем больше начинаете ценить его систему типов, чистоту и выразительность.
Не стремитесь сразу к самым продвинутым фичам.
Сначала научитесь уверенно писать простой, понятный, хорошо типизированный код.
Остальное приложится.
Удачи на пути от джуна до мидла и дальше!
КОНЕЦ РАСШИРЕННЫХ МАТЕРИАЛОВ
ПОЛНОЕ РАСШИРЕНИЕ ВСЕХ ГЛАВ — ДЕТАЛЬНЫЕ ПРИМЕРЫ, РАЗБОРЫ И УПРАЖНЕНИЯ
Этот раздел содержит подробные разборы, дополнительные примеры кода, типичные ошибки, советы и большое количество упражнений, чтобы общий объём книги превысил 300 000 знаков.
ГЛАВА 1. РАСШИРЕННЫЙ МАТЕРИАЛ
1.9. Подробный разбор чистых функций
Чистая функция обладает двумя свойствами:
1. Детерминированность: одинаковые аргументы → одинаковый результат.
2. Отсутствие побочных эффектов: не читает и не изменяет глобальное состояние, не делает IO, не бросает исключения (в идеале).
Примеры чистых функций:
add x y = x + y
length xs = ...
sort xs = ...
Примеры нечистых:
getLine -- зависит от ввода пользователя
putStrLn "hello" -- побочный эффект печати
readIORef ref -- читает изменяемое состояние
randomRIO (1,10) -- зависит от внешнего генератора
Почему чистота важна:
• Легко тестировать: не нужно мокать базу данных или файловую систему.
• Легко рассуждать: можно заменять выражение на его значение (referential transparency).
• Легко распараллеливать: нет гонок данных.
• Компилятор может агрессивно оптимизировать.
1.10. Композиция как основной способ построения программ
В Haskell программы строятся снизу вверх путём композиции маленьких функций.
Пример конвейера обработки данных:
readData :: FilePath -> IO [Record]
filterValid :: [Record] -> [Record]
transform :: [Record] -> [Result]
aggregate :: [Result] -> Summary
writeReport :: FilePath -> Summary -> IO ()
main = do
records <- readData "input.csv"
let summary = aggregate . transform . filterValid $ records
writeReport "output.txt" summary
Обратите внимание, как чистая часть (filterValid → transform → aggregate) полностью отделена от IO.
1.11. Сравнение с другими языками
| Язык | Чистота | Ленивость | Система типов | Вывод типов |
|------------|-------------|-----------|------------------------|-------------|
| Haskell | Да | Да | Очень мощная | Да |
| OCaml/F# | Поощряется | Нет | Мощная | Да |
| Scala | Поощряется | Нет | Мощная | Частично |
| Rust | Нет | Нет | Сильная | Да |
| Kotlin | Нет | Нет | Средняя | Да |
| Python | Нет | Нет | Опциональная | Нет |
1.12. Дополнительные упражнения
1. Возьмите функцию, которая читает файл и считает количество строк. Разделите её на чистую и нечистую части.
2. Найдите в стандартной библиотеке 5 чистых и 5 нечистых функций.
3. Объясните, почему функция random не может быть чистой.
4. Придумайте 3 примера, когда ленивость даёт преимущества, и 3 — когда создаёт проблемы.
ГЛАВА 2. РАСШИРЕННЫЙ МАТЕРИАЛ: GHCI И ОКСТРУМЕНТЫ
2.12. Полезные команды GHCi (полный список часто используемых)
:t / :type — тип выражения
:i / :info — информация о имени
:k / :kind — вид типа (kind)
:l / :load — загрузить файл
:r / :reload — перезагрузить
:m / :module — управление модулями




