Таблица с результатами проверки домашних заданий и самостоятельной работы.
Если в крайней правой колонке написано "ДЗ проверяется", дождитесь результатов проверки.
Если вы заметили какое-либо иное несоответствие, напишите по электронной почте.
Алгоритм вычисления оценок приведён ниже.
22 марта в 9:00 в ауд. 300 те, кто не написал самостоятельную работу, смогут это сделать.
λ-термы строятся из переменных и констант с помощью двух конструкций:
Символ «λ» связывает переменную \(x\), как кванторы в логике первого порядка. Несвязанные переменные называются свободными. Связанные переменные можно переименовывать. Это преобразование называется α-конверсией: например, \(\lambda x.\lambda y. x =_\alpha \lambda z.\lambda t.z\).
Более содержательное преобразование термов — β-редукция: \( (\lambda x. u) v \to_\beta u[x := v]\). Это преобразование можно применить к любому подтерму вида \((\lambda x . u) v\) — такой подтерм называется редексом. Поэтому иногда бывает, что данный терм можно редуцировать многими способами. Через \(\twoheadrightarrow_\beta\) обозначим рефлексивно-транзитивное замыкание \(\to_\beta\), т.е. применение нескольких (возможно, ни одной) β-редукций.
Конфлюэнтность (свойство Чёрча – Россера). Если \( u \twoheadrightarrow_\beta v_1 \) и \( u \twoheadrightarrow_\beta v_2 \), то существует такой терм \(w\), что \( v_1 \twoheadrightarrow_\beta w \) и \( v_2 \twoheadrightarrow_\beta w \).
Терм \(u\) называется нормальной формой, если к нему нельзя применить ни одну редукцию (в нём нет редексов). Если \(u \twoheadrightarrow_\beta u' \) и \(u'\) является нормальной формой, то \(u'\) называется нормальной формой терма \(u\). В силу свойства Чёрча – Россера, нормальная форма данного терма (если она существует) единственна. Бывают, однако, термы без нормальной формы: например \( (\lambda x . (x x) ) (\lambda x. (x x )) \) редуцируется «петлёй» к себе.
Терм называется слабо нормализуемым (WN), если у него есть нормальная форма. Терм называется сильно нормализуемым (SN), если любая последовательность редукций, начинающаяся с этого терма, приводит к нормальной форме (эквивалентно: граф его редукций конечен и ацикличен).
Система простых типов. Типы строятся из примитивных (таких как Int, Bool, Char, ...) с помощью одной операции \(\to\); \(A \to B\) означает тип функций из типа \(A\) в тип \(B\).
Разметка термов типами может осуществляться двумя способами. При типизации по Чёрчу (жёсткой) каждой константе и каждой свободной переменной приписан определённый тип, и также опеределённый тип приписывается каждой переменной при λ-абстракции: вместо \(\lambda x . u\) пишем \(\lambda x:A .u \). По индукции теперь можно определить тип любого терма: если \(u\) имеет тип \(B\), то \(\lambda x:A. u \) имеет тип \(A \to B\); если \(u\) имеет тип \(A \to B \) и \(v\) имеет тип \(A\), то \((uv)\) имеет тип \(B\). Если же типы \(u\) и \(v\) несогласованы, то \((uv)\) не считается правильно построенным термом. При типизации по Чёрчу отсутствует всякий полиморфизм: например, тождественная функция \(\mathrm{id} = \lambda x. x\) должна быть своя для каждого типа: \(\lambda x : \mathrm{Int}. x\) типа \(\mathrm{Int} \to \mathrm{Int}\), \(\lambda x : \mathrm{Bool} \to \mathrm{Bool}. x\) типа \( (\mathrm{Bool} \to \mathrm{Bool}) \to (\mathrm{Bool} \to \mathrm{Bool})\) и т.д.
При типизации по Карри сами λ-термы не несут никакой информации о типах. Вместо этого, утверждения вида „терм \(u\) может иметь тип \(A\)“ (записывается \(u:A\)) выводятся (доказываются) в специальном логическом исчислении. Этот процесс называется type inference. Для свободных переменных и констант типы должны быть объявлены в контексте \(\Gamma\) — множестве утверждений вида \(x : B\), где \(x\) — переменная или константа, причём любая \(x\) встречается в \(\Gamma\) не более одного раза. Утверждение „в контексте \(\Gamma\) терм \(u\) может иметь тип \(A\)“ записывается как \(\Gamma \vdash u : A\). Исчисление для type inference в случае простых типов по Карри следующее: \[ \dfrac{(x : B) \in \Gamma}{\Gamma \vdash x : B}\,[\mbox{Var}] \qquad \dfrac{\Gamma \vdash u : (A \to B) \quad \Gamma \vdash v : A}{\Gamma \vdash (uv) : B}\,[\mbox{App}] \qquad \dfrac{\Gamma, x : A \vdash u : B}{\Gamma \vdash (\lambda x . u) : (A \to B)}\,[\mbox{Abs}] \]
Типизация по Карри допускает некоторый уровень полиморфизма: один терм может иметь несколько типов. Например, для \(\mathrm{id} = \lambda x . x\) утверждение \(\vdash (\lambda x . x) : (A \to A) \) (с пустым контекстом) выводится для любого типа \(A\). Таким образом, \(\mathrm{id}\) является полиморфной тождественной функцией, применимой к объекту любого типа.
Безопасность типов: если \(\Gamma \vdash u_1 : A \) и \( u_1 \twoheadrightarrow_\beta u_2 \), то \( \Gamma \vdash u_2 : A \). Иначе говоря, β-редукция не может испортить типы.
Теорема. Если \(\Gamma \vdash u : A \) для некоторого контекста \(\Gamma\) и некоторого типа \(A\), то терм \(u\) обладает свойством SN. Иначе говоря, всякий типизуемый терм сильно нормализуем.
Система типов 2-го порядка (λ2, система F). В этой системе добавляется ещё один конструктор типов: квантор всеобщности \(\forall\). Теперь типы строятся из примитивных типов и переменных по типам (обозначаются α, β, ...) следующим образом: если \(A\) и \(B\) — типы, а \(\alpha\) — переменная по типам, то \(A \to B\) и \(\forall \alpha. A \) — типы. Квантор \(\forall\) задаёт полиморфный тип: например, \((\lambda x . x) : \forall \alpha. (\alpha \to \alpha)\).
Громадная (даже чрезмерная) выразительная сила системы λ2 проистекает из того, что конструкции с \(\forall\) можно дальше использовать в построении типов. Например, в этой системе оказывается типизуемым терм \(\lambda x. (x x) : \forall \alpha . (\alpha \to \alpha) \to \forall \alpha . (\alpha \to \alpha) \). Действительно, если \(x\) имеет тип \(\forall \alpha . (\alpha \to \alpha) \), т.е. обозначает функцию, действующую на объект любого типа \(\alpha\) и возвращающую значение также типа \(\alpha\), то его можно применить к самому себе, получив значение того же типа, что и \(x\), т.е. \( \forall \alpha. (\alpha \to \alpha) \). Правила вывода типов в λ2 таковы: \[ \dfrac{(x : B) \in \Gamma}{\Gamma \vdash x : B}\,[\mbox{Var}] \qquad \dfrac{\Gamma \vdash u : (A \to B) \quad \Gamma \vdash v : A}{\Gamma \vdash (uv) : B}\,[\mbox{App}] \qquad \dfrac{\Gamma, x : A \vdash u : B}{\Gamma \vdash (\lambda x . u) : (A \to B)}\,[\mbox{Abs}] \] \[ \dfrac{\Gamma \vdash u : A}{\Gamma \vdash u : (\forall \alpha. A)}\, [\mbox{Gen}] \qquad \dfrac{\Gamma \vdash u : (\forall. \alpha A)}{\Gamma \vdash u : A[\alpha := B]} \, [\mbox{Inst}] \]
Как ни странно, система λ2 сохраняет свойство SN для всех типизуемых в ней термов (J.-Y. Girard). В частности, терм \((\lambda x. (xx))(\lambda x. (xx))\) в ней не типизуем. Однако, с другой стороны, проблема типизуемости (вывода типа) для данного терма в этой системе алгоритмически неразрешима (J. B. Wells). Это означает, что при использовании λ2 интерпретатор функционального языка не сможет сам восстановить типы, и их придётся явно указывать.
В Haskell систему типов 2-го порядка можно использовать с помощью ключа RankNTypes:
{-# LANGUAGE RankNTypes #-} lxx :: (forall a . a -> a) -> (forall a . a -> a) lxx = \x -> (x x) lxx2 :: (forall b . (b->b) -> (b->b)) -> (forall b. (b->b) -> (b->b)) lxx2 = \x -> (x x) dup = \f -> \x -> f (f x)Заметим, что здесь мы определили две разных версии \(\lambda x . (x x)\) с разными типами. Эти типы несравнимы: один нельзя получить из другого подстановкой типов (хотя кажется, что второй тип — это «частный случай» первого). К функции dup, выведенный тип которой: (t -> t) -> t -> t, применим только lxx2, но не lxx.
Выражение lxx2 dup (+2) 3 даёт 11. Действительно, dup означает «примени данную функцию дважды»; lxx2 dup применяет dup к самой себе, и означает «примени функцию четырежды». Значит, мы 4 раза применяем операцию «+2» к числу 3.
Система типов Хиндли – Милнера. TBA
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));Для использования qsort нужно создать функцию compar, сравнивающую два объекта.
bigrams = {"AB": [10, 11, 12], "BC": [5, -5, 8], "CD": [105, 1, 0], "DE": [6, 6], "EF": [15, 20, 15], "FG": [22, 11, 32], "GH": [20, 20, 20]} sorter = sorted(bigrams, key=lambda key: sum(bigrams[key]), reverse=True) for key in sorter: print(key, bigrams[key])Здесь функция, осуществляющая сравнение двух значений для сортировки (точнее, для вычисления ключа сортировки: вместо сравнения элементов \(u_i\) и \(u_j\), мы сначала вычисляем целочисленные ключи \(k(u_i)\) и \(k(u_j)\), а потом сравниваем уже их — этот способ, хотя и менее общий, более эффективен), определена непосредственно при вызове алгоритма сортировки (sorted) с помощью ключевого слова lambda. В математической нотации, \(\lambda x. v\), где \(v\) — выражение, содержащее переменную \(x\), означает функцию, по данному значению \(x\) вычисляющую \(v\).
Посмотрим на следующий код:
x=5 f=lambda y : y+x print f(2) x=7 print f(2)Значение f(2) изменилось: действительно, lambda создаёт новую безымянную функцию, которая, помимо своего аргумента y имеет также неявный доступ к переменной x. Меняется значение x — меняется поведение f.
Таким образом, переменные-функции ведут себя не совсем так, как обычные переменные: в следующем примере значение f, конечно, не поменяется:
x=5 f=x+2 print f x=7 print f
В Haskell эта трудность решена радикально: изменять значение x запрещено.
sort [] = [] sort [z] = [z] sort (x : xs) = if (head(sort xs) >= x) then x : sort xs else head(sort xs) : sort (x : tail xs) halve x = splitAt (length x `div` 2) x merge xs [] = xs merge [] ys = ys merge (x : xs) (y : ys) = if (x < y) then (x : (merge xs (y:ys))) else (y : (merge (x:xs) ys)) mergesort [] = [] mergesort [z] = [z] mergesort xs = merge (mergesort (fst (halve xs))) (mergesort (snd (halve xs)))Пример с решетом Эратосфена:
sieve [] = [] sieve (p : xs) = p : sieve [ x | x <- xs, x `mod` p /= 0 ] primes = sieve [2..](на самом деле с точки зрения эффективности это совсем не решето Эратосфена)
List comprehension — довольно мощное средство. Иногда можно программировать так, как будто пишешь математический текст:
primes = [ p | p <- [2..100], all (\y -> (p `mod` y /= 0)) [2..p-1] ] perfect = [ p | p <- [2..], p == sum [ y | y <- [1..p-1], p `mod` y == 0 ] ]
Ограничение допустимых типов:
rmnb :: (Num t, Show t) => t -> String rmnb n = "Room number " ++ (show n)rmnb нельзя теперь применить к нечисловому типу (напр., [...])
Подсказка о выигрывающей стратегии: для определения выигрывающей позиции вычисляют так называемую ним-сумму, записывая количества камней в кучках в двоичной системе и применяя поразрядно операцию «исключающее ИЛИ» (XOR). Если ним-сумма оказалась равной 0, то позиция проигрышная: либо камней нет вообще, либо любой ход приведёт к позиции с ненулевой ним-суммой. А вот позиция с ненулевой ним-суммой выигрышная: из неё всегда есть ход, переводящий её в позицию с ним-суммой, равной 0. (Как найти этот ход, остаётся в качестве упражнения.)
Будем задавать тип регулярных выражений рекурсивно:
data RegExp = Char | Con RegExp RegExp | Uni RegExp RegExp | Eps | Ite RegExpЗадача заключается в том, чтобы написать программу, проверяющую данное слово на соответствие регулярному выражению: checkregexp :: RegExp -> String -> Bool.
Для этого предлагается сначала построить по регулярному выражению недетерминированный конечный автомат (NFA), а потом запустить его на данном входном слове.
NFA — это набор \(\langle Q, \Sigma, \delta, q_0, F \rangle\), где \(Q\) — множество состояний, среди которых выделено начальное \(q_0\) и множество допускающих состояний \(F\), \(\Sigma\) — алфавит, в котором задано слово, а \(\delta \colon \Sigma \to (Q \to \mathcal{P}(Q))\) — функция перехода, по данному входному символу переводящая текущее состояние во множество состояний, в которые можно их него перейти (это и есть недетерминизм). Автомат принимает данное слово \(a_1\dots a_n\), если есть хотя бы один путь \( q_0 \to q_1 \to q_2 \to \ldots \to q_n \), такой что \( q_i \in \delta (a_i) (q_{i-1})\) и \(q_n \in F \).
Как мы видим, здесь появляется монада \(\mathcal{P}(Q)\) множества всех подмножеств данного множества \(Q\). Грубым приближением её является монада списка [Q]. Таким образом реализован, например, недетерминизм ходов коня в примере с ходом коня из LearnYouAHaskell:
-- from "Learn you a Haskell" -- Motto: [a] is a monad for non-determinism -- import Control.Monad -- add monoid structure class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a instance MonadPlus [] where mzero = [] mplus = (++) guard :: (MonadPlus m) => Bool -> m () guard True = return () guard False = mzero -- example: -- [1..10] >>= \x -> guard (x `mod` 3 == 0) >> return x type KnightPos = (Int,Int) moveKnight :: KnightPos -> [KnightPos] moveKnight (c,r) = do (c',r') <- [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1) ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2) ] guard (c' `elem` [1..8] && r' `elem` [1..8]) return (c',r') in3 :: KnightPos -> [KnightPos] in3 start = do first <- moveKnight start second <- moveKnight first moveKnight second canReachIn3 :: KnightPos -> KnightPos -> Bool canReachIn3 start end = end `elem` in3 start -- inMany :: Int -> KnightPos -> [KnightPos] -- inMany x start = return start >>= foldr (<=<) return (replicate x moveKnight) inMany2 1 start = moveKnight start inMany2 n start = inMany2 (n-1) start >>= moveKnight inMany3 1 start = moveKnight start inMany3 n start = do first <- inMany3 (n-1) start moveKnight first canReachIn :: Int -> KnightPos -> KnightPos -> Bool canReachIn x start end = end `elem` inMany2 x startТакая реализация NFA, однако, будет неэффективной: после нескольких шагов автомата в списке может оказаться несколько копий одного состояния (по одной для каждого пути), и потребуется экспоненциально много времени. Поэтому правильнее использовать монаду Set.
(To be continued...)