
Полная версия
Курс Хаскель от джуна до мидла

Учитель Начальной
Курс Хаскель от джуна до мидла
ГЛАВА 1. ВВЕДЕНИЕ В HASKELL И ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ
1.1. Что такое Haskell и зачем он нужен
Haskell — это чисто функциональный язык программирования с сильной статической типизацией, ленивыми вычислениями и мощной системой типов. Он был создан в конце 1980-х — начале 1990-х годов группой исследователей как стандартный язык для исследований в области функционального программирования. Назван в честь математика Хаскелла Брукса Карри.
Основные особенности Haskell:
• Чистота (purity). Функции не имеют побочных эффектов. Одна и та же функция с одними и теми же аргументами всегда возвращает один и тот же результат.
• Ленивые вычисления (lazy evaluation). Выражения вычисляются только тогда, когда их значение действительно нужно.
• Сильная статическая типизация с выводом типов (type inference). Компилятор сам выводит большинство типов.
• Алгебраические типы данных и паттерн-матчинг.
• Мощная система типов, включая type classes, higher-kinded types, GADTs и т.д.
• Отсутствие изменяемого состояния по умолчанию (immutable by default).
Зачем изучать Haskell в 2020–2020-х годах?
1. Понимание функционального программирования на глубоком уровне.
2. Улучшение качества кода даже в других языках (Java, C#, Python, JavaScript, Rust, Scala).
3. Работа с современными системами, где Haskell используется: финансы (Standard Chartered, Barclays), блокчейн (Cardano), компиляторы, анализ данных, DevOps-инструменты.
4. Развитие мышления: Haskell заставляет думать о данных и трансформациях, а не о последовательности команд.
5. Высокая надёжность программ благодаря системе типов.
1.2. Императивное vs Функциональное мышление
Императивный подход (C, Java, Python в классическом стиле):
"Сделай шаг 1, потом шаг 2, измени переменную x, потом проверь условие..."
Функциональный подход:
"Опиши, что есть результат, через композицию чистых функций".
Пример. Подсчёт суммы квадратов чётных чисел в списке.
Императивно (псевдокод):
sum = 0
for x in list:
if x % 2 == 0:
sum = sum + x * x
Функционально на Haskell:
sumOfSquaresOfEvens = sum . map (^2) . filter even
Или более явно:
sumOfSquaresOfEvens xs = sum (map (^2) (filter even xs))
Здесь каждая функция делает одну вещь, и они комбинируются.
1.3. Основные концепции, которые нужно усвоить с самого начала
• Значение (value) и выражение (expression).
• Функция как значение первого класса.
• Рекурсия вместо циклов.
• Неизменяемость данных.
• Типы как контракты.
• Композиция функций.
1.4. Краткая история и экосистема
• 1987–1990: разработка языка.
• 1990: первый отчёт (Haskell 1.0).
• 1998: Haskell 98 (стандарт на долгие годы).
• 2010: Haskell 2010.
• Сегодня: GHC (Glasgow Haskell Compiler) — основной компилятор, постоянно развивается (GHC 9.x).
• Cabal и Stack — системы сборки.
• Hackage — центральный репозиторий пакетов.
• Stackage — стабильные наборы пакетов.
1.5. Кому подходит этот курс
Курс рассчитан на тех, кто:
• уже умеет программировать хотя бы на одном языке (Python, JavaScript, Java, C++ и т.д.);
• хочет перейти от junior-уровня понимания FP к уверенному middle-уровню;
• готов писать много кода и разбирать ошибки компилятора.
Если вы совсем новичок в программировании — сначала освойте основы любого императивного языка, потом возвращайтесь.
1.6. Как работать с этой книгой
1. Читайте главу.
2. Набирайте все примеры в GHCi или в файле.
3. Модифицируйте примеры.
4. Выполняйте упражнения.
5. Не бойтесь ошибок компилятора — это ваш лучший учитель.
1.7. Первые полезные ресурсы
• Официальный сайт: haskell.org
• Книга "Learn You a Haskell for Great Good!" (есть русский перевод).
• "Haskell Programming from First Principles" (платная, очень глубокая).
• Документация GHC и Hoogle (поиск по типам и функциям).
• Reddit r/haskell, Haskell Discourse, Telegram-чаты.
В следующей главе мы настроим окружение и напишем первую программу.
Упражнения к главе 1
1. Объясните своими словами, чем чистая функция отличается от процедуры с побочными эффектами.
2. Приведите 3 примера задач, которые удобнее решать функционально, чем императивно.
3. Найдите в интернете 5 компаний, которые используют Haskell в продакшене, и кратко опишите, для чего.
ГЛАВА 2. УСТАНОВКА ОКРУЖЕНИЯ, GHCI И ПЕРВЫЙ КОД
2.1. Установка GHC и инструментов
Рекомендуемый способ в 2024–2026 годах — использовать GHCup.
На Linux / macOS:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
После установки доступны команды:
• ghcup — менеджер версий
• ghc — компилятор
• ghci — интерактивная оболочка (REPL)
• cabal — система сборки
• stack — альтернативная система сборки (многие до сих пор предпочитают)
Проверка:
ghc --version
ghci
На Windows лучше использовать официальный установщик или WSL2 + GHCup.
2.2. Первый запуск GHCi
Открываем терминал и пишем:
$ ghci
GHCi, version 9.6.x: https://www.haskell.org/ghc/ :? for help
Prelude>
Prelude — это стандартный модуль, который загружается по умолчанию.
Простые вычисления:
Prelude> 2 + 2
4
Prelude> 7 * 8
56
Prelude> 2 ^ 10
1024
Prelude> sqrt 16
4.0
Prelude> pi
3.141592653589793
Строки:
Prelude> "Hello, " ++ "Haskell!"
"Hello, Haskell!"
Логика:
Prelude> True && False
False
Prelude> True || False
True
Prelude> not True
False
2.3. Основные команды GHCi
:t выражение — показать тип
:i имя — информация о типе/классе/функции
:l файл.hs — загрузить файл
:r — перезагрузить последний файл
:q — выход
:? — справка
:set +t — показывать типы результатов
:set prompt "λ> " — красивый промпт
Пример:
Prelude> :t 5
5 :: Num a => a
Prelude> :t "hello"
"hello" :: String
Prelude> :t True
True :: Bool
Prelude> :t not
not :: Bool -> Bool
2.4. Первый файл .hs
Создаём файл Hello.hs:
-- Hello.hs
module Main where
main :: IO ()
main = putStrLn "Привет, Haskell!"
Компиляция и запуск:
$ ghc Hello.hs
$ ./Hello
Привет, Haskell!
Или через runhaskell (без компиляции в бинарник):
$ runhaskell Hello.hs
2.5. Простейшие определения в файле
-- Simple.hs
module Simple where
double :: Int -> Int
double x = x * 2
square :: Int -> Int
square x = x * x
sumOfSquares :: Int -> Int -> Int
sumOfSquares x y = square x + square y
Загружаем в GHCi:
$ ghci Simple.hs
*Simple> double 21
42
*Simple> sumOfSquares 3 4
25
2.6. Полезные настройки
В файле ~/.ghci можно добавить:
:set prompt "λ> "
:set +t
:set -Wall
Флаг -Wall включает почти все предупреждения — очень полезно с самого начала.
2.7. Cabal и Stack — краткий обзор
Для небольших экспериментов достаточно GHCi и ghc.
Для реальных проектов:
Создание проекта Cabal:
$ cabal init
$ cabal build
$ cabal run
Stack:
$ stack new my-project
$ cd my-project
$ stack build
$ stack exec my-project-exe
В этом курсе до главы 14–15 мы будем в основном работать с отдельными .hs файлами и GHCi. Позже перейдём к проектам.
2.8. Типичные проблемы при установке
• На macOS иногда нужно установить Command Line Tools.
• На Linux — libgmp, zlib и другие зависимости.
• Конфликт версий — используйте ghcup set.
• В Windows — лучше WSL2.
2.9. Редакторы и IDE
• VS Code + Haskell extension (на базе HLS — Haskell Language Server)
• Emacs + haskell-mode
• Vim/Neovim + coc-haskell или haskell-tools.nvim
• IntelliJ + Haskell plugin (менее популярен)
HLS даёт подсветку, автодополнение, переход к определению, type holes и многое другое.
Упражнения к главе 2
1. Установите GHCup и проверьте версии ghc, cabal, stack.
2. Напишите программу, которая выводит ваше имя и текущий год.
3. Создайте файл с тремя функциями: сложение, умножение и возведение в степень. Загрузите в GHCi и протестируйте.
4. Настройте красивый промпт в GHCi.
ГЛАВА 3. ТИПЫ, ВЫРАЖЕНИЯ И БАЗОВЫЙ СИНТАКСИС
3.1. Всё есть выражение
В Haskell почти всё является выражением, которое имеет значение и тип.
5 -- Int (или Num a => a)
True -- Bool
'a' -- Char
"hello" -- String (это [Char])
[1,2,3] -- [Int]
(1, "hello", True) -- (Int, String, Bool)
3.2. Базовые типы
• Int — целые числа фиксированного размера (обычно 64 бита)
• Integer — целые числа произвольной точности
• Float — числа с плавающей точкой одинарной точности
• Double — двойной точности
• Bool — True | False
• Char — символ
• String — синоним для [Char]
Проверка:
Prelude> :t 42
42 :: Num a => a
Prelude> :t (42 :: Int)
42 :: Int
Prelude> :t (42 :: Integer)
42 :: Integer
3.3. Списки и кортежи
Списки — однородные:
[1,2,3,4] :: [Int]
["a","b"] :: [String]
[] :: [a] -- пустой список любого типа
Кортежи — разнородные, фиксированной длины:
(1, "hello") :: (Int, String)
(True, 3.14, 'x') :: (Bool, Double, Char)
() :: () -- unit type
3.4. Функции как значения
Функция тоже имеет тип:
not :: Bool -> Bool
length :: [a] -> Int
take :: Int -> [a] -> [a]
(++) :: [a] -> [a] -> [a]
Оператор тоже функция:
(+) :: Num a => a -> a -> a
3.5. Аннotation типов
Хотя Haskell умеет выводить типы, явные аннотации очень полезны:
add :: Int -> Int -> Int
add x y = x + y
Или в выражениях:
(5 :: Int) + (7 :: Int)
3.6. Условные выражения
if условие then выражение1 else выражение2
Важно: и then, и else обязательны, и оба выражения должны иметь один тип.
absolute :: Int -> Int
absolute n = if n >= 0 then n else -n
Можно писать в несколько строк:
absolute n =
if n >= 0
then n
else -n
3.7. Охраняющие условия (guards)
Часто удобнее guards:
absolute :: Int -> Int
absolute n
| n >= 0 = n
| otherwise = -n
otherwise — это просто True.
Можно несколько:
bmiTell :: Double -> Double -> String
bmiTell weight height
| bmi <= 18.5 = "Недостаточный вес"
| bmi <= 25.0 = "Норма"
| bmi <= 30.0 = "Избыточный вес"
| otherwise = "Ожирение"
where bmi = weight / height ^ 2
3.8. where и let
where — локальные определения после функции:
cylinder :: Double -> Double -> Double
cylinder r h =
sideArea + 2 * topArea
where
sideArea = 2 * pi * r * h
topArea = pi * r ^ 2
let — выражение:
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^ 2
in sideArea + 2 * topArea
В GHCi let используется для определений:
Prelude> let x = 5
Prelude> let y = 7
Prelude> x + y
12
3.9. Комментарии
-- однострочный комментарий
{-
многострочный
комментарий
-}
3.10. Отступы — это синтаксис
Haskell чувствителен к отступам (layout rule).
Правило: код, который относится к одному блоку, должен иметь одинаковый или больший отступ.
Плохо:
let x = 5
y = 7 -- ошибка
Хорошо:
let x = 5
y = 7
Или с фигурными скобками (редко используют):
let { x = 5; y = 7 }
3.11. Операторы и их приоритет
Инфиксные операторы:
2 + 3 * 4 -- 14, потому что * имеет больший приоритет
Можно делать функцию инфиксной с помощью обратных кавычек:
div 10 2
10 `div` 2
И наоборот — оператор в префиксной форме:
(+) 2 3
3.12. Полезные встроенные функции
fst, snd — для пар
head, tail, last, init
null, length
take, drop
reverse
elem
maximum, minimum
sum, product
and, or
zip, zipWith
words, unwords, lines, unlines
Примеры:
Prelude> head [1,2,3]
1
Prelude> tail [1,2,3]
[2,3]
Prelude> take 3 [1..10]
[1,2,3]
Prelude> [1..5]
[1,2,3,4,5]
Prelude> [1,3..10]
[1,3,5,7,9]
Prelude> elem 3 [1,2,3,4]
True
Упражнения к главе 3
1. Напишите функцию, которая возвращает большее из двух чисел (без использования max).
2. Напишите функцию signum (знак числа): -1, 0, 1.
3. Создайте функцию, которая по двум катетам считает гипотенузу.
4. Напишите функцию, которая проверяет, является ли год високосным.
5. Используя where, напишите функцию площади треугольника по формуле Герона.
ГЛАВА 4. ФУНКЦИИ, ПАТТЕРН-МАТЧИНГ И РЕКУРСИЯ
4.1. Определение функций
Самый простой способ:
add :: Int -> Int -> Int
add x y = x + y
Можно частично применять:
add5 = add 5
-- add5 :: Int -> Int
4.2. Паттерн-матчинг
Одна из самых мощных возможностей языка.
Факториал через паттерны:
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
Важно: порядок уравнений имеет значение. Более специфичные паттерны должны идти раньше.
Паттерны для списков:
head' :: [a] -> a
head' [] = error "пустой список"
head' (x:_) = x
tail' :: [a] -> [a]
tail' [] = error "пустой список"
tail' (_:xs) = xs
Длина списка:
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs
Сумма:
sum' :: Num a => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
4.3. as-паттерны
Иногда нужно и разобрать, и сохранить целое:
capital :: String -> String
capital "" = "Пустая строка"
capital all@(x:xs) = "Первая буква " ++ [x] ++ " остальное: " ++ xs
4.4. Паттерны в лямбдах и case
case выражение of
паттерн1 -> результат1
паттерн2 -> результат2
Пример:
describeList :: [a] -> String
describeList xs = "Список " ++ case xs of
[] -> "пустой"
[x] -> "из одного элемента"
_ -> "из нескольких элементов"
4.5. Рекурсия — основной способ итерации
В Haskell нет циклов for/while в классическом виде. Всё делается рекурсией или функциями высшего порядка.
Хвостовая рекурсия (хвостовой вызов) важна для производительности, но GHC умеет оптимизировать многие случаи.
Пример аккумулятора:
sumTail :: Num a => [a] -> a
sumTail xs = go 0 xs
where
go acc [] = acc
go acc (x:xs) = go (acc + x) xs
4.6. Рекурсия по нескольким аргументам
zip' :: [a] -> [b] -> [(a,b)]
zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x,y) : zip' xs ys
4.7. Взаимная рекурсия
even' :: Int -> Bool
even' 0 = True
even' n = odd' (n - 1)
odd' :: Int -> Bool
odd' 0 = False
odd' n = even' (n - 1)
4.8. Локальные функции с where/let
Часто вспомогательные функции делают локальными:
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
let smaller = filter (<= x) xs
bigger = filter (> x) xs
in quicksort smaller ++ [x] ++ quicksort bigger
Или с where:
quicksort (x:xs) = quicksort smaller ++ [x] ++ quicksort bigger
where
smaller = [a | a <- xs, a <= x]
bigger = [a | a <- xs, a > x]
4.9. Ошибки и частичные функции
error :: String -> a
undefined :: a
Лучше избегать частичных функций (head, tail, !! и т.д.) в продакшен-коде. Позже мы научимся использовать Maybe и Either.
4.10. Примеры полезных рекурсивных функций
-- Разворот списка
reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
-- Более эффективно с аккумулятором
reverse'' :: [a] -> [a]
reverse'' xs = go [] xs
where
go acc [] = acc
go acc (x:xs) = go (x:acc) xs
-- Элемент по индексу
(!!) :: [a] -> Int -> a
[] !! _ = error "индекс слишком большой"
(x:_) !! 0 = x
(_:xs) !! n = xs !! (n - 1)
-- take
take' :: Int -> [a] -> [a]
take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n - 1) xs
Упражнения к главе 4
1. Напишите функцию, вычисляющую n-е число Фибоначчи (сначала наивно, потом с аккумуляторами).
2. Реализуйте функцию elem самостоятельно через рекурсию.
3. Напишите функцию, которая вставляет элемент в отсортированный список, сохраняя порядок.
4. Реализуйте merge (слияние двух отсортированных списков).
5. Напишите функцию, которая проверяет, является ли список палиндромом.
ГЛАВА 5. СПИСКИ, СТРОКИ И ОСНОВНЫЕ ОПЕРАЦИИ
5.1. Списки — центральная структура данных
Синтаксис:
[1,2,3]
1:2:3:[] -- то же самое
[1..10]
[1,3..20]
['a'..'z']
5.2. Основные операции
(++) :: [a] -> [a] -> [a] -- конкатенация
(:) :: a -> [a] -> [a] -- cons
head, tail, last, init
null :: [a] -> Bool
length :: [a] -> Int
reverse :: [a] -> [a]
take, drop, splitAt
elem, notElem
maximum, minimum (требуют Ord)
sum, product (требуют Num)
and, or (для [Bool])
any, all
concat :: [[a]] -> [a]
concatMap
zip, zipWith, unzip
words, unwords, lines, unlines
5.3. Генераторы списков (list comprehensions)
Очень мощный и читаемый синтаксис:
[x * 2 | x <- [1..10]]
[x | x <- [1..20], even x]
[(x,y) | x <- [1..3], y <- [1..3]]
[(x,y) | x <- [1..3], y <- [1..3], x + y == 4]
С несколькими фильтрами:
[x | x <- [1..100], x `mod` 3 == 0, x `mod` 5 == 0]
С let внутри:
[x * x | x <- [1..10], let y = x * x, y > 50]
Для строк:
[c | c <- "Hello World", c `elem` ['A'..'Z']]
5.4. Строки
String = [Char]
Поэтому все списковые функции работают со строками.
"hello" ++ " world"
reverse "haskell"
length "привет" -- осторожно с Unicode!
Для серьёзной работы с текстом позже будем использовать Text (пакет text).
5.5. Бесконечные списки
Благодаря ленивости:
ones = 1 : ones
[1..]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 20 fibs
5.6. Полезные приёмы
-- Удаление дубликатов (требует Eq)
nub :: Eq a => [a] -> [a]
-- Сортировка
import Data.List (sort)
sort [3,1,4,1,5,9]
-- Группировка
group [1,1,1,2,2,3,3,3,3]
-- Подинтервалы
inits, tails
-- Транспонирование
transpose [[1,2,3],[4,5,6]]
5.7. Примеры реальных задач
-- Частотный анализ
import Data.List (sort, group)
frequency xs = map (\g -> (head g, length g)) . group . sort $ xs
-- Проверка на анаграмму
isAnagram s1 s2 = sort s1 == sort s2
-- Разбиение на слова и подсчёт
wordCount = length . words
Упражнения к главе 5
1. Напишите функцию, которая возвращает все чётные квадраты чисел от 1 до n.
2. С помощью list comprehension создайте таблицу умножения 10×10.
3. Реализуйте функцию, которая удаляет все гласные из строки.
4. Напишите функцию, которая находит все простые числа до n (решето Эратосфена).
5. Создайте бесконечный список факториалов и возьмите первые 10.
ГЛАВА 6. ВЫСШИЕ ФУНКЦИИ: MAP, FILTER, FOLD И КОМПАНИЯ
6.1. Функции высшего порядка
Функция высшего порядка — это функция, которая принимает функции как аргументы или возвращает функцию.
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (b -> a -> b) -> b -> [a] -> b
6.2. map
map (*2) [1..5] -- [2,4,6,8,10]
map toUpper "haskell" -- "HASKELL"
map length ["hello", "world"] -- [5,5]
Реализация:
map' :: (a -> b) -> [a] -> [b]
map' _ [] = []
map' f (x:xs) = f x : map' f xs
6.3. filter
filter even [1..10]
filter (>5) [1..10]
filter (/= ' ') "hello world"
Реализация:
filter' _ [] = []
filter' p (x:xs)
| p x = x : filter' p xs
| otherwise = filter' p xs
6.4. foldr и foldl
foldr — свёртка справа:
foldr (+) 0 [1,2,3,4] = 1 + (2 + (3 + (4 + 0)))
foldl — свёртка слева:
foldl (+) 0 [1,2,3,4] = (((0 + 1) + 2) + 3) + 4
Для ассоциативных операций и конечных списков результат одинаковый, но производительность и поведение на бесконечных списках различаются.
sum = foldr (+) 0
product = foldr (*) 1
and = foldr (&&) True
or = foldr (||) False
length = foldr (\_ acc -> acc + 1) 0
reverse = foldl (\acc x -> x : acc) []
6.5. foldl' — строгая версия
В Data.List есть foldl' — строгий слева fold. Рекомендуется использовать его вместо foldl почти всегда, чтобы избежать накопления thunk'ов.
6.6. Другие полезные функции
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (+) [1,2,3] [4,5,6] -- [5,7,9]
takeWhile, dropWhile
span, break
any, all
find
partition
nubBy, groupBy, sortBy, on
6.7. Композиция функций
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)
Пример:
sumOfSquaresOfEvens = sum . map (^2) . filter even
Очень важный стиль — point-free (бесточечный):
countEven = length . filter even
6.8. Лямбда-выражения
\x -> x * 2
\x y -> x + y
\(x,y) -> x + y
\xs -> length xs > 5
6.9. Сечения операторов
(2*) -- умножение на 2
(*2) -- то же
(>5) -- проверка > 5
(/10) -- деление на 10
(10/) -- 10 делить на что-то
6.10. Примеры
-- Подсчёт слов длиной больше 5
longWords = length . filter ((>5) . length) . words
-- Среднее значение
average xs = sum xs / fromIntegral (length xs)
-- Нормализация списка
normalize xs = map (/ sum xs) xs
Упражнения к главе 6
1. Реализуйте map, filter, foldr самостоятельно.
2. Напишите функцию, которая возводит все элементы списка в квадрат и отфильтровывает те, что меньше 20.
3. С помощью fold реализуйте length, reverse, map, filter.
4. Напишите point-free версию функции, которая считает количество гласных в строке.
5. Используя zipWith, создайте список частичных сумм.
ГЛАВА 7. МОДУЛИ, ПРОСТРАНСТВА ИМЁН И ОРГАНИЗАЦИЯ КОДА
7.1. Зачем нужны модули
• Разделение кода на логические части
• Сокрытие реализации (encapsulation)
• Избежание конфликтов имён
• Повторное использование
7.2. Объявление модуля
module Geometry









