осень 2026
осень 2026
Летучка 5 · 7 минут · Ваше ФИ: ____________________
String -> Int -> Bool: _________________________\x -> (x, x): ______________$, где это возможно: f (g x) (h y) ≡ ________________________fac 0 = 1; fac n = n * fac (n - 1). Один шаг без арифметики: fac 3 ⇝ __________________[10, 20, 30] сопоставили с образцом x : xs. Чему равно xs? ____________Напишите типы Dog и Parrot.
data объявляет новый тип Pet. Справа от = перечислены способы построить его значение.
Cat, Dog и Parrot — новые конструкторы данных. | разделяет варианты.
После имени конструктора записаны типы его аргументов. У Parrot аргументов нет.
Типы аргументов становятся входами конструктора. Тип результата у всех трёх — Pet.
Какую ветку выберет showPet (Cat (CatD "murka" 2))? Какие имена свяжет образец?
showPet (Cat (CatD "murka" 2))
Parrot и Dog не подходят. В последней ветке совпадают оба конструктора: Cat и CatD.
showPet (Cat (CatD "murka" 2))
"murka"
name связывается с "murka". Образец _ принимает возраст 2, не связывая имени.
showPet (Cat (CatD "murka" 2))
"murka"
name =
"murka"
Берём правую часть выбранной ветки и подставляем значение name.
"Cat: " ++
"murka"
name =
"murka"
Конкатенация строк даёт результат "Cat: murka".
"Cat: murka"
Закончите функцию, чтобы она обрабатывала каждый вариант Pet. Возраст попугая в нашем типе не хранится.
Проредуцируем весь терм. Жёлтый фрагмент перепишется следующим кликом.
height
(Node
(Leaf 1)
2
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
height: правило Node
Раскрыли корень; теперь переписываем левый вызов.
1 + max
(height (Leaf 1))
(height
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
height: правило Node
height: база Leaf 1
Высота слева известна. Весь ожидающий контекст остаётся на экране.
1 + max
1
(height
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
Правое поддерево тоже Node: ещё два вызова в прежнем контексте.
1 + max
1
(1 + max
(height (Leaf 3))
(height (Leaf 5)))
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
Левый лист справа дал 1; осталось вычислить последний лист.
1 + max
1
(1 + max
1
(height (Leaf 5)))
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
Все листья вычислены. Сначала внутренний max.
1 + max
1
(1 + max
1
1)
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
max 1 1 ⇝ 1
В правом поддереве осталось сложение.
1 + max
1
(1 + 1)
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
max 1 1 ⇝ 1
1 + 1 ⇝ 2
Высота справа равна 2. Теперь внешний max.
1 + max
1
2
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
max 1 1 ⇝ 1
1 + 1 ⇝ 2
max 1 2 ⇝ 2
Осталось последнее сложение у корня.
1 + 2
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
max 1 1 ⇝ 1
1 + 1 ⇝ 2
max 1 2 ⇝ 2
1 + 2 ⇝ 3
Полный терм проредуцирован до 3.
3
шаги редукции
height: правило Node
height: база Leaf 1
height: правило Node 4
height: база Leaf 3
height: база Leaf 5
max 1 1 ⇝ 1
1 + 1 ⇝ 2
max 1 2 ⇝ 2
1 + 2 ⇝ 3
результат 3
Напишите sumTree :: Tree Int -> Int целиком. Функция должна складывать все значения дерева: и в листьях, и во внутренних узлах.
Проредуцируем sumTree на том же дереве.
sumTree
(Node
(Leaf 1)
2
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
sumTree: правило Node
У корня остаются значение 2 и вызов справа.
sumTree (Leaf 1) + 2
+ sumTree
(Node
(Leaf 3)
4
(Leaf 5))
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
Левый лист дал 1. Переписываем правый вызов.
1 + 2
+ sumTree
(Node
(Leaf 3)
4
(Leaf 5))
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
Спускаемся к Leaf 3; внешняя сумма остаётся видимой.
1 + 2
+ sumTree (Leaf 3) + 4
+ sumTree (Leaf 5)
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
Leaf 3 дал 3; осталось вычислить последний лист.
1 + 2
+ 3 + 4
+ sumTree (Leaf 5)
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
Теперь суммируем значения внутри правого поддерева.
1 + 2
+ 3 + 4
+ 5
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
3 + 4 ⇝ 7
Правый узел прибавляет последнее значение.
1 + 2
+ 7 + 5
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
3 + 4 ⇝ 7
7 + 5 ⇝ 12
Правая сумма равна 12. Возвращаемся к внешнему контексту.
1 + 2
+ 12
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
3 + 4 ⇝ 7
7 + 5 ⇝ 12
1 + 2 ⇝ 3
Осталось сложить два результата.
3
+ 12
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
3 + 4 ⇝ 7
7 + 5 ⇝ 12
1 + 2 ⇝ 3
3 + 12 ⇝ 15
Полный терм проредуцирован до 15.
15
шаги редукции
sumTree: правило Node
sumTree: база Leaf 1
sumTree: правило Node 4
sumTree: база Leaf 3
sumTree: база Leaf 5
3 + 4 ⇝ 7
7 + 5 ⇝ 12
1 + 2 ⇝ 3
3 + 12 ⇝ 15
результат 15
Заполните пропуски. Функция f может менять тип содержимого, а форма дерева должна сохраниться.
height (transform f t) == height tПреобразуем (+ 10) каждое значение; исходное дерево слева остаётся прежним.
transform (+ 10)
(Node
(Leaf 1)
2
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
transform: правило Node
Node строится заново из двух преобразованных поддеревьев и нового значения.
Node
(transform (+ 10) (Leaf 1))
((+ 10) 2)
(transform (+ 10)
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
transform: правило Node
Для листа достаточно обернуть результат f x в Leaf.
Node
(Leaf ((+ 10) 1))
((+ 10) 2)
(transform (+ 10)
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
transform: правило Node
Теперь преобразуем значение в корне; левое поддерево уже готово.
Node
(Leaf 11)
((+ 10) 2)
(transform (+ 10)
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
transform: правило Node
Переходим к правому поддереву; полный контекст Node остаётся видимым.
Node
(Leaf 11)
12
(transform (+ 10)
(Node
(Leaf 3)
4
(Leaf 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
Правый Node собирается по той же схеме.
Node
(Leaf 11)
12
(Node
(transform (+ 10) (Leaf 3))
((+ 10) 4)
(transform (+ 10) (Leaf 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
Лист 3 превращается в Leaf 13.
Node
(Leaf 11)
12
(Node
(Leaf ((+ 10) 3))
((+ 10) 4)
(transform (+ 10) (Leaf 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
Для внутреннего значения 4 получаем 14.
Node
(Leaf 11)
12
(Node
(Leaf 13)
((+ 10) 4)
(transform (+ 10) (Leaf 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
Последнее поддерево — Leaf 5.
Node
(Leaf 11)
12
(Node
(Leaf 13)
14
(transform (+ 10) (Leaf 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
transform: правило Leaf
Осталось применить f к 5 и собрать последний Leaf.
Node
(Leaf 11)
12
(Node
(Leaf 13)
14
(Leaf ((+ 10) 5)))
шаги редукции
transform: правило Node
transform: правило Node 4
transform: правило Leaf
Новое дерево имеет прежнюю форму и значения 11–15.
Node
(Leaf 11)
12
(Node
(Leaf 13)
14
(Leaf 15))
шаги редукции
transform: правило Node
transform: правило Node 4
transform: правило Leaf
форма сохранена
Терм Just True — выражение, которое можно вычислить.
У терма Just True есть тип Maybe Bool.
У типа Maybe Bool есть кайнд *.
Знакомое дерево терма: применение Just к True.
Just True
Припишем листьям типы. В этом применении у Just параметр типа — Bool.
Just True
Аргумент имеет нужный тип Bool; всё применение получает тип Maybe Bool.
Just True
Приблизим Maybe Bool: тип тоже записан применением.
Maybe Bool
У дерева типа та же форма: применение Maybe к Bool.
Maybe Bool
Припишем листьям кайнды: Maybe :: * -> *, Bool :: *.
Maybe Bool
Правило применения то же: аргумент подходит, корень получает метку результата.
Maybe Bool \mathrel{::}\ast
Определите кайнды Tree, Tree Bool, Either String и Either String Int.
:t спрашивает тип терма; :k спрашивает кайнд типаNode — конструктор данных; Tree — конструктор типа-- Схематически: встроенный список устроен так
-- data List a = Nil | Cons a (List a)
[] -- пустой список
x : xs -- голова и хвостНапишите безопасную функцию взятия головы списка: на пустом списке возвращайте Nothing.
map применяет функцию к каждому элементу. Предскажите map (* 2) [1,2,3].
map (* 2) [1,2,3]
шаги
map: преобразуем элементы
Применяем (* 2) к голове; результат становится новой головой.
2 : map (* 2) [2,3]
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
Рекурсивный вызов получает хвост [2,3]. Следующая голова — 2.
2 : map (* 2) [2,3]
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
Для 2 получаем 4. Порядок элементов сохраняется.
2 : 4 : map (* 2) [3]
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
(* 2) 2 ⇝ 4
В оставшемся хвосте [3] ещё один элемент.
2 : 4 : map (* 2) [3]
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
(* 2) 2 ⇝ 4
Получили 6, но хвост результата ещё задаётся вызовом на [].
2 : 4 : 6 : map (* 2) []
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
(* 2) 2 ⇝ 4
(* 2) 3 ⇝ 6
map f [] = []: пустой вход даёт пустой хвост результата.
[2,4,6]
шаги
map: преобразуем элементы
(* 2) 1 ⇝ 2
(* 2) 2 ⇝ 4
(* 2) 3 ⇝ 6
map f [] ⇝ []
filter проверяет каждый элемент. Предскажите filter odd [1,2,3].
filter odd [1,2,3]
шаги
filter: проверяем odd
odd 1 = True: сохраняем 1 и продолжаем с хвостом.
1 : filter odd [2,3]
шаги
filter: проверяем odd
odd 1 ⇝ True
Следующая голова — 2. Проверка может дать другой результат.
1 : filter odd [2,3]
шаги
filter: проверяем odd
odd 1 ⇝ True
odd 2 = False: пропускаем 2; новой ячейки результата нет.
1 : filter odd [3]
шаги
filter: проверяем odd
odd 1 ⇝ True
odd 2 ⇝ False
У хвоста [3] снова проверяем голову.
1 : filter odd [3]
шаги
filter: проверяем odd
odd 1 ⇝ True
odd 2 ⇝ False
odd 3 = True: добавляем 3 после 1, сохраняя порядок.
1 : 3 : filter odd []
шаги
filter: проверяем odd
odd 1 ⇝ True
odd 2 ⇝ False
odd 3 ⇝ True
filter p [] = []: конец входа завершает результат [1,3].
[1,3]
шаги
filter: проверяем odd
odd 1 ⇝ True
odd 2 ⇝ False
odd 3 ⇝ True
filter p [] ⇝ []
zip соединяет элементы попарно. Предскажите zip [1,2,3] ['a','b'].
zip [1,2,3] ['a','b']
шаги
zip: берём две головы
Первые головы вместе образуют одну пару (1,'a').
(1,'a') : zip [2,3] ['b']
шаги
zip: берём две головы
(1,'a'): новая пара
Рекурсивный вызов получает оба хвоста: [2,3] и ['b'].
(1,'a') : zip [2,3] ['b']
шаги
zip: берём две головы
(1,'a'): новая пара
Вторая пара — (2,'b'). Теперь второй список пуст.
(1,'a') : (2,'b') : zip [3] []
шаги
zip: берём две головы
(1,'a'): новая пара
(2,'b'): новая пара
Один вход закончился: zip xs [] = []. Для 3 пары нет.
[(1,'a'),(2,'b')]
шаги
zip: берём две головы
(1,'a'): новая пара
(2,'b'): новая пара
zip [3] [] ⇝ []
Напишите dupEvens :: [a] -> [a], дуплицирующую элементы с индексами 0, 2, 4, … Для [10,20,30,40] результат — [10,10,20,30,30,40]. Используйте zip, map и concat.
Композиция применяется справа налево. Сначала добавим индексы.
dupEvens = concat . map f . zip [0..]
f (i, x) = if odd i then [x] else [x, x]
После zip у каждого элемента есть индекс. Теперь map f выберет один или два экземпляра.
dupEvens = concat . map f . zip [0..]
f (i, x) = if odd i then [x] else [x, x]
После map f получился список коротких списков. Теперь concat соединит их.
dupEvens = concat . map f . zip [0..]
f (i, x) = if odd i then [x] else [x, x]
После concat получился плоский список. Индексы 0 и 2 дали по два экземпляра.
dupEvens = concat . map f . zip [0..]
f (i, x) = if odd i then [x] else [x, x]
type или newtype?Нужно запретить перестановку аргументов publish :: ModuleId -> CourseId -> String. Какой вариант подходит?
newtype: ошибку представления можно поймать по типам до запуска.
Проверка возвращает Bool. Тип следующей функции ничего не говорит о положительности номера.
Проверка: дальше остаётся Int
Проверку нужно помнить при каждом вызове.
Справа конструктор CourseId скрыт: создавать такие значения может только парсер.
Проверку можно забыть или передать другое число. publish 0 подходит по типам.
Проверка: дальше остаётся Int
Проверку нужно помнить при каждом вызове.
Справа конструктор CourseId скрыт: создавать такие значения может только парсер.
Парсер сохраняет положительный номер в CourseId. Из Just получаем значение с проверенным свойством.
Проверка: дальше остаётся Int
Проверку нужно помнить при каждом вызове.
Справа конструктор CourseId скрыт: создавать такие значения может только парсер.
publish принимает CourseId. После Just cid передаём cid дальше без повторных проверок n > 0.
Проверка: дальше остаётся Int
Проверку нужно помнить при каждом вызове.
Справа конструктор CourseId скрыт: создавать такие значения может только парсер.
Номер 0 отбрасывается на границе. Обойти парсер, передав publish обычный Int, нельзя.
Проверка: дальше остаётся Int
Проверку нужно помнить при каждом вызове.
Справа конструктор CourseId скрыт: создавать такие значения может только парсер.
xs — исходный список [3,4]. У (:) есть голова и хвост.
xs = [3,4]
что изменилось
xs: два узла (:)
Для ys = 1 : xs создаётся новая голова; старые узлы остаются на месте.
xs = [3,4]
ys = 1 : xs
что изменилось
xs: два узла (:)
новая голова 1
Хвост новой головы ссылается на уже существующий xs.
xs = [3,4]
ys = 1 : xs
что изменилось
xs: два узла (:)
новая голова 1
хвост ys — прежний xs
Для zs = 2 : ys нужна ещё одна новая голова.
xs = [3,4]
ys = 1 : xs
zs = 2 : ys
что изменилось
xs: два узла (:)
новая голова 1
хвост ys — прежний xs
новая голова 2
Все три имени доступны одновременно; xs — общий хвост ys и zs.
xs = [3,4]
ys = 1 : xs
zs = 2 : ys
что изменилось
xs: два узла (:)
новая голова 1
хвост ys — прежний xs
новая голова 2
хвост zs — прежний ys
Утилита печатает справку без параметров или запускается с обязательным форматом Plain/JSON и выводом в stdout либо файл.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions = BadOptions Mode (Maybe Format) (Maybe FilePath)Спроектируйте Options, исключающий неверные состояния. Сравните с BadOptions число состояний для двух путей a.txt, b.txt (Nothing означает stdout).
Поля BadOptions выбираются независимо: 2 режима, 3 варианта формата и 3 варианта пути.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions =
BadOptions Mode (Maybe Format) (Maybe FilePath)
data Target = Stdout | File FilePath
data Options = Help | Run Format Target
считаем состояния
2 режима; 3 формата; 3 пути
Каждая комбинация полей даёт значение. Получается 2 × 3 × 3 = 18 состояний.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions =
BadOptions Mode (Maybe Format) (Maybe FilePath)
data Target = Stdout | File FilePath
data Options = Help | Run Format Target
считаем состояния
2 режима; 3 формата; 3 пути
BadOptions: 2 × 3 × 3 = 18
Для справки допустима только комбинация без формата и пути: 1 из 9.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions =
BadOptions Mode (Maybe Format) (Maybe FilePath)
data Target = Stdout | File FilePath
data Options = Help | Run Format Target
считаем состояния
2 режима; 3 формата; 3 пути
BadOptions: 2 × 3 × 3 = 18
Help: 1 допустимое состояние
Для запуска нужен формат: 2 × 3 = 6 состояний. Вместе со справкой допустимы 7 из 18.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions =
BadOptions Mode (Maybe Format) (Maybe FilePath)
data Target = Stdout | File FilePath
data Options = Help | Run Format Target
считаем состояния
2 режима; 3 формата; 3 пути
BadOptions: 2 × 3 × 3 = 18
Help: 1 допустимое состояние
Run: 2 × 3 = 6; всего 7
Options хранит выбор ветки: Help либо Run. Получается 1 + 2 × 3 = 7 состояний.
data Mode = HelpMode | RunMode
data Format = Plain | JSON
data BadOptions =
BadOptions Mode (Maybe Format) (Maybe FilePath)
data Target = Stdout | File FilePath
data Options = Help | Run Format Target
считаем состояния
2 режима; 3 формата; 3 пути
BadOptions: 2 × 3 × 3 = 18
Help: 1 допустимое состояние
Run: 2 × 3 = 6; всего 7
Options: 1 + 2 × 3 = 7
Maybe, type и newtype по смыслу задачиghci> :kind Maybe Maybe
<interactive>:1:7: error: [GHC-83865]
• Expecting one more argument to ‘Maybe’
Expected a type, but ‘Maybe’ has kind ‘* -> *’
• In the first argument of ‘Maybe’, namely ‘Maybe’
In the type ‘Maybe Maybe’
Maybe ждёт обычный тип, а ему передали конструктор типа.
Pair Int Char \equiv forall c. (Int -> Char -> c) -> c
\begin{aligned} &\mathbf{Pair} : * \to * \to * \\ &\mathbf{Pair} = \lambda \alpha^*\,\beta^*.\,\forall \gamma.\,(\alpha\to\beta\to\gamma)\to\gamma \\ &\mathbf{pair} : \forall \alpha\,\beta.\,\alpha\to\beta\to\mathbf{Pair}\ \alpha\ \beta \\ &\mathbf{pair} = \Lambda \alpha^*\,\beta^*.\,\lambda x^\alpha\,y^\beta.\,(\Lambda \gamma^*.\,\lambda f^{\alpha\to\beta\to\gamma}.\,f\ x\ y) \\ &\mathbf{fst} : \forall \alpha\,\beta.\,\mathbf{Pair}\ \alpha\ \beta\to\alpha \\ &\mathbf{fst} = \Lambda \alpha^*\,\beta^*.\,\lambda p^{\mathbf{Pair}\ \alpha\ \beta}.\,p\ \alpha\ (\mathbf{K}\ \alpha\ \beta) \end{aligned}
coerce: подробностиnewtype позволяет разделить типы без дополнительной обёртки во время исполнения#include <string>
struct CatT {
std::string name;
int age;
};
CatT createBarsik() { return CatT{"barsik", 4}; }
std::string getName(const CatT& cat) { return cat.name; }Поля — компоненты произведения; конструктор создаёт значение, доступ к полю извлекает компонент.
struct Pet {
enum { CAT, DOG, PARROT } tag;
union {
struct { const char *name; int age; } cat;
struct { const char *name; int age; } dog;
} value;
};
/* tag и value должны согласовываться */
switch (pet.tag) {
case CAT: printf("Cat: %s", pet.value.cat.name); break;
case DOG: printf("Dog: %s", pet.value.dog.name); break;
case PARROT: printf("Parrot"); break;
}std::variant<Cat, Dog, Parrot> pet{Cat{"barsik", 4}};
if (std::holds_alternative<Cat>(pet)) {
std::cout << "Cat " << std::get<Cat>(pet).name;
} else if (std::holds_alternative<Dog>(pet)) {
std::cout << "Dog " << std::get<Dog>(pet).name;
} else if (std::holds_alternative<Parrot>(pet)) {
std::cout << "Parrot";
}class Pet: pass
class Cat(Pet):
def __init__(self, name): self.name = name
class Dog(Pet):
def __init__(self, name): self.name = name
class Parrot(Pet): pass
if isinstance(pet, Cat):
print("Cat " + pet.name)
elif isinstance(pet, Dog):
print("Dog " + pet.name)
elif isinstance(pet, Parrot):
print("Parrot")
else:
raise AssertionError("unreachable code") # No exhaustiveness checksealed class Pet
class Cat(val name: String) : Pet()
class Dog(val name: String) : Pet()
class Parrot : Pet()
fun showPet(pet: Pet): String = when (pet) {
is Cat -> "Cat: " + pet.name
is Dog -> "Dog: " + pet.name
is Parrot -> "Parrot"
}Иерархия sealed позволяет компилятору проверять полноту выражения when.
Практика 5. Типы данных