осень 2026
осень 2026
Летучка 4 · 7 минут · Ваше ФИ: ____________________
(*2) . (+3) $ 4? ____Haskell — катана
Другие языки — швейцарский нож
Три недели мы писали на λ-исчислении. Как то же самое пишется в Haskell?
| λ-исчисление | Haskell |
|---|---|
| абстракция \lambda x.\,M | \x -> M |
| аппликация M\ N, группируется влево | M N |
| тип \alpha\to\beta, стрелка группируется вправо | a -> b |
| \mathbf{I}, \ \mathbf{K}, \ \mathbf{K}_* | id, const, flip const |
| композиция \mathbf{B} | (.) |
| \mathbf{pair}, \ \mathbf{fst}, \ \mathbf{snd} | (,), fst, snd |
| рекурсия через \mathbf{Y} | функция вызывает себя по имени |
| η-шаг \lambda x.\,M\ x \rightsquigarrow_\eta M | бесточечный стиль |
Какая сложность по памяти у рекурсивного факториала?
Линейная: каждое умножение ждёт, пока вернётся рекурсивный вызов.
Сначала рекурсия. Раскрываем fac 4 по определению.
fac 4
цепочка
рекурсия
fac 4
4 > 1, поэтому вызов превращается в умножение. Умножить пока нельзя: второй множитель ещё не вычислен.
4 * fac 3
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
То же самое внутри: ещё одно умножение встало в очередь.
4 * (3 * fac 2)
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
Выражение растёт с каждым вызовом: в нём хранится всё, что ещё предстоит умножить.
4 * (3 * (2 * fac 1))
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
fac 1 отвечает 1 без рекурсии. Только теперь умножениям есть что умножать.
4 * (3 * (2 * 1))
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
Очередь умножений разобрана. Память линейная: длина выражения росла вместе с числом вызовов.
24
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
Теперь аккумулятор. Начинаем с того же вызова, только функция другая.
fac' 4
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
fac' 4 раскрывается по определению в go 1 4: промежуточный результат 1, счётчик 4.
go 1 4
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
⇝ go 1 4
4 > 1: умножаем аккумулятор на счётчик сразу, счётчик уменьшаем. Ждать нечего.
go 4 3
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
⇝ go 1 4
⇝ go 4 3
Выражение не растёт: всё, что уже посчитано, лежит в аккумуляторе одним числом.
go 12 2
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
⇝ go 1 4
⇝ go 4 3
⇝ go 12 2
Счётчик дошёл до единицы — следующим шагом go вернёт аккумулятор.
go 24 1
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
⇝ go 1 4
⇝ go 4 3
⇝ go 12 2
⇝ go 24 1
Сравните две цепочки справа: первая расширяется с каждым шагом, вторая — нет.
24
цепочка
рекурсия
fac 4
⇝ 4 * fac 3
⇝ 4 * (3 * fac 2)
⇝ 4 * (3 * (2 * fac 1))
⇝ 4 * (3 * (2 * 1))
⇝ 24
аккумулятор
fac' 4
⇝ go 1 4
⇝ go 4 3
⇝ go 12 2
⇝ go 24 1
⇝ 24
Заполните пропуски, чтобы fib вычислял n-е число Фибоначчи за линейное время
infixr 9 .
infixr 8 ^
infixl 7 *
infixl 6 +, -
infixr 0 $
-- применение функции сильнее любого оператора и группируется влевоРасставьте все скобки:
(а) 2 + 3 * 4 ^ 2
(б) f x + g y * 2
(в) f . g $ x + 1
(а) 2 + (3 * (4 ^ 2)) \qquad (б) (f x) + ((g y) * 2) \qquad (в) (f . g) $ (x + 1)
($) :: (a -> b) -> a -> b
(&) :: a -> (a -> b) -> b
(.) :: (b -> c) -> (a -> b) -> a -> c
f :: Int -> a -> a
g :: Int -> Double
h :: Int -> Double -> IntПерепишите example x = f 1 (g (h 2 x)):
(а) через доллар; (б) через композицию; (в) в бесточечном стиле; (г) через &; (д) через let
ghci> :t id
ghci> :t id "something here"
id :: a -> a
id "something here" :: String
ghci> :t const id
ghci> :t flip const
const id :: b -> a -> a
flip const :: b -> c -> c — тот же тип: буквы компилятор выбирает сам, их можно переименовать. Это тип \mathbf{K}_* из летучки
ghci> :t const True 'a' 'b'
error: [GHC-83865]
• Couldn't match expected type ‘Char -> t’ with actual type ‘Bool’
• In the first argument of ‘const’, namely ‘True’
In the expression: const True 'a' 'b'
Переведите сообщение: какой тип компилятор ожидал, какой получил и откуда взялось ожидание?
Аппликация группируется влево: ((const True) 'a') 'b'. Результат const True 'a' имеет тип первого аргумента, а его применяют к 'b' — значит, первый аргумент обязан быть функцией Char -> t. Получен True :: Bool.
В П3 мы населяли тип композиции, ставя дырку \square и подписывая её тип. В Haskell дырка — это _, и её тип подписывает компилятор.
• Found hole: _ :: c
• Relevant bindings include
x :: a
g :: a -> b
f :: b -> c
Чем в терминах П3 является список «Relevant bindings»?
Контекстом типизации: \ \Gamma \equiv f^{\beta\to\gamma},\ g^{\alpha\to\beta},\ x^\alpha
\x -> M, a -> b, const — это \mathbf{K}$ и . и убирать точку η-шагомВ следующих сериях: сегодня все данные были числами и парами — дальше соберём собственные типы данных
make repl теперь открывает GHCi с вашим кодом на Haskell:t — тип выражения, :i — сведения об имени и приоритет оператора, :r — перечитать файл после правкиmake check перед тестами запускает линтер hlint{-# LANGUAGE ... #-}LambdaCase, ImplicitParams)RankNTypes)MultiParamTypeClasses, GADTs)LinearTypes)Cabal — Common Architecture for Building Applications and Libraries
ghcup, версия записана в cabal.projectcabal build — собрать проектcabal test — запустить тестыcabal repl — REPL с загруженным проектомmake build, make check, make replStack — надстройка над cabal, которая сама скачивает компилятор; встретится в чужих проектах
REPL (read-eval-print-loop)
Интерактивное окружение, исполняющее вводимые в него высказывания и тут же демонстрирующее результат.
make repl:r (reload) — перезагрузить интерпретатор:t (type) — посмотреть тип выражения:i (info) — посмотреть документацию функции, приоритет оператора:set — установить расширения языка и флаги компиляцииhlint) помогает находить ошибки в программахormolu, fourmoluIDE для Haskell на примере vscode
haskell-language-server через ghcup — он подберёт сборку под ваш компиляторhlint✱ То, что типизация остаётся валидной после подстановки в тип — теорема метатеории
Лемма подстановки типа для \lambda_\to
\begin{array}{ll} \Gamma\vdash M : A \Rightarrow [\alpha\mapsto B]\ \Gamma\vdash \phantom{[\alpha\mapsto B]\ }M : [\alpha\mapsto B]\ A & {\color{blue}(\lambda_\to \text{ по Карри})} \\ \Gamma\vdash M : A \Rightarrow [\alpha\mapsto B]\ \Gamma\vdash {\color{red}[\alpha\mapsto B]\ }M : [\alpha\mapsto B]\ A & {\color{red}(\lambda_\to \text{ по Чёрчу})} \end{array}
Если мы в языке разрешим приписывать типы термам, мы потеряем гибкость:
\begin{array}{l} \mathbf{K} : \alpha \to \beta \to \alpha \\ \mathbf{K}\ \mathtt{1} : {\color{red}\text{cannot match } \alpha \text{ with } \mathrm{Int}} \end{array}
Введём в теорию абстракцию на типах и аппликацию термов к типам!
\begin{array}{l} \mathbf{K} : \forall \alpha\ \beta.\,\alpha \to \beta \to \alpha \\ \mathbf{K}\ @\mathrm{Int} : \forall \beta.\,\mathrm{Int} \to \beta \to \mathrm{Int} \\ \mathbf{K}\ @\mathrm{Int}\ @\mathrm{Float}\ \mathtt{1} : \mathrm{Float} \to \mathrm{Int} \end{array}
Теперь выбор конкретного типа происходит на стороне пользователя функции
Такие функции называются полиморфными — умеют работать с различными типами
Haskell неявно приписывает forall по всем типовым переменным:
Типовые переменные всегда начинаются с маленькой буквы (a), в отличие от типовых констант (Int — конкретный тип)
Типовые применения компилятор выставляет автоматически неявно
Иногда бывает полезно типовые применения указывать вручную (TypeApplications)
В C++-like языках синтаксис примерно такой:
Практика 4. Введение в Haskell