Haskell, 02 лекция (от 05 октября)

Материал из eSyr's wiki.

Перейти к: навигация, поиск
TRUE=λab.a
FALSE=λab.b
IF=λcab.cab

IF FALSE 1 0 → (λcab.cab) FALS 1 0 →β (λab.FALSE a b) 1 0 → (λab.(λab.b)ab) 1 0 →α (λ ab(λxy.y)ab) 1 0 →→β (λab.b) 1 0 →→β 0

AND=λab.aba OR=λab.aab NOT=λcab.cba

L=(λx.xx)(λx.xx) Попробуем произвести подстановку: L=(λx.xx)(λx.xx)

Получается, что сколько бы раз мы не подставляли, получаем его же. Это выражение не имеет нормальной формы.

Попробуем записать следующее выражение:

TRUE 1 L

Каким образом мы можем его вычислить:

1-й вариатн: раскрываем TRUE

(λab.a) 1 L →β 1

2) TRUE 1 (λx.xx)(λx.xx) →β и тут мы зациклились.

Получается, что в зависимости от порядка раскрытия, мы можем найти или не найти нормальную форму.


Нормальная редукционная стратегия - способ редукции λ-терма, где на каждом шаге редукции выбирается наиболее левый и наиболее внешний β-редакс.

что это значит:

MN ⇒ 1) M, 2) N

Вначале мы редуцируем M, потом N ("самый левый")

(λx.M)N ⇒ 1) (λx.M)N 2) M, N

Если мы здесь используем определение формальной системы, где есть эта конверсия (η: λx.Mx ⇒ M), то все эти конверсии происх. после β-редукции, в конце.

Определение 2. Аппликативная редукционная стратегия - способ редукции λ-терма, где на каждом шаге выбирается β-редакс, не содержащий других β-редаксов.

В первом примере в первом случае была нормальная редукционная стратегия, в втором - аппликативная. Они моделирует следующие алгоритмы работы в ЯП: при вызове функции иы можем как вычислить аргументы до передачи функции, так и передать их внутрь. В случае С всегда используется аппликативная стратегия. При этом может быть такая ситуация: функция использует второй аргумент только если первый не равен нулю. При этом, если мы подадим такой вызов: f(0, 3/0), то оно рухнет, хотя второй аргумент даже не задействован. В первом случае такого не происходит, но оно работает медленно. Поэтому нормальная стратегия работает медленно, но всегда приводит к результатам. В современных функциональных языках используется модифицированная стратегия, называемая ленивой нормальной стратегией. Идея в том, что мы всё ещё не вычислили значение выражения, но вместо него мы используем дерево, и передаём не всё дерево, а ссылку на него. Это позволяет как ускорить процесс, так и всегда находить результат. Но в языке C существует пример использования нормальной стратегии: это встроенный оператор if. Именно потому, что он встроенный (вы не можете написать свой такой же), он может её использовать. Мы вычислим каждую из ветвей только после вычисления условия.

В языке haskell if можно определить следующим образом:

if :: Bool → a → a → a
if true a b = a
if false a b = b

В первой строчке задаются типы выражений. Тип Bool можно определить следующим образом:

data Bool = True | False

Вернёмся к λ-исчислению. Теперь понятно, каким образом происх вычисления.

Арифметика. Здесь используются нумералы Чёрча. Нумералы Чёрча это функция.

Определим 0 как FALSE для удобства.

0 = FALSE = λfx.x
1 = λfx.fx
2 = λfx.f(fx)
...
n = λfx.f(...f(fx)...)

Теперь определим арифметику.

add = λmnfx.mf(nfx)
succ=λnfx.f(nfx)
mlt=λmnf.m(nf)
exp=λmn.nm

легко проверить, что

add 3 2

и

5=λfx.f(f(f(f(fx)))))

эквивалентны.

Немного сложнее будет с вычитанием. Существует расширение, позволяющее добавлять отрицательные числа, но лектор не видит смысла его показывать, там всё ещё сложнее и запись ещё длиннее. Поэтому, функцию, которая производит вычитание, или хотя бы уменьшение на единицу... мы должны определить pred как функцию, возвращающую предыдущее число или если 0 то 0. Для этого мы должны уметь определять, является ли число нулём. Предикат для этого записывается следующим образом:

iszero=λn.n(λx.FALSE)TRUE

Попробуем раскрыть его для нуля:

iszero 0 → 0(λx.FALSE)TRUE → (λfx.x)(λx.FALSE)TRUE → TRUE

Легко проверить, что если бы вместо x было бы хотя бы fx, то получилось бы FALSE. Обратите внимание, как производятся вычисления: вычисления производятся через функцию, передаваему в аргументе.

В случае pred мы можем записать его как

pred=λn.n(λgk. iszero(g 1) k (add (gk) 1)) (λv.0)0

Наконец, вычитание

sub=λmn.n PRED m

Это определение наиболее очевидно.

Таким образом, имея нумералы Чёрча, мы не требуем от языка наличия арифметики, тем не менее, обычно она есть, дабы вычисления работали быстрее.

Далее, списки. Мы говорили, что λ-исчисление это также способ моделирования структур данных. Попробуем смоделировать пару чисел.

Определим пару как:

PAIR=λxyf.fxy

Что даёт это определение: если мы определим пару из двух чисел, которую мы потом будем использовать, то редуцируется это следующим образом:

PAIR 4 2 →β λf.f 4 2

Это функция, которая, в зависимости от того, какой аргумент вы подадите, выдаёт первый элемент или второй (в терминах лиспа - car/cdr)

CAR=FIRST=λp.p TRUE
CDR = SND = λp.p FALSE
SND(PAIR 4 2) → (λp.p FALSE)(PAIR 4 2) → (PAIR 4 2)FALSE → (λf.f 4 2) FALSE → FALSE 4 2 → 2

С использованием пары можем определить лисповский список. Что это такое? Это пара, где первый элемент это элемент, а второй — указатель на вторую пару такого же вида, где второй элемент тоже указатель на пару, где второй элемент — nil.

В лиспе конструктор списка — cons, первый элемент списка — car, получение хвоста списка — cdr. Также существует функция isnull, которая возвращает T, если список пуст.

(3 4 5)
(car '(3 4 5)) = 3
(cdr '(3 4 5)) = '(4 5)
(isnull (cdr (cdr (cdr '(3 4 5))))) = T
nil = λx.x
cons = λxy.PAIR FALSE (PAIR xy)
isnull = FIRST
car = λx.FIRST(SND x)
cdr = &lamda;x.SND(SND x)

Это уже можно использовать как список.

Не хватает одного элемента Тьюринг-полного языка — цикла. Наиболее простым методом, с т.з. лектора, является рекурсия. Как её организовать? Всё тот же Хаскел Карри решил использовать определение неподвижной точки. Неподвижная точка — такая точка из области определения функции, что значение функции в этой точке развно этой точке.

x — НТ f(x): x ∈ D(f), f(x) = x

Комбинатор:

λf(λx.f(xx))(λx.f(xx)) — Y-комбинатор

Вообще говоря, комбинаторов неподвижной точки существует очень много. Для примера лектор напишет только один, написанный Аланом Тьюрингом:

(λx.λy.(y(xxy)))(λx.λy.(y(xxy)))

Общее свойство этих комбинаторов одно: у них есть некоторая часть, которая повторяется. Свойство на самом деле, если мы попробуем раскодировать комбинацию и подставить функцию, то его можно записать следующим обазом:

Для некоторого ∀a∈Λ Ya=a(Ya)

Как это использовать для рекурсии? Попробуем записать определение факториала: n! = 1, если n=0; n(n-1)!

Для определения факториала требуется цикл. Определим функцию итерация факториала:

IFACT=λf.λn.if(iszero n) 1 (mult n (f (pred n)))

после чего вычислим

(Y IFACT)5 = IFACT (Y IFACT) 5

Таким образом мы можем видеть что аппарат λ-исчисления является Тьюринг-полным, несмотря на то, что мы не имеем переменных, не имеем ..., организуем всё мы можем.

Так как функциональные языки основываются на λ-исчислении, а в λ-исчислении аппаратом для организации итеративных вычислений используется рекурсия, то она же используется и для ФЯП.


Теория функционального программирования. Язык Haskell


00 01 02 03 04 05 06 07 08 09 10 11 12


Календарь

Сентябрь
23 28
Октябрь
05 12 19 26
Ноябрь
02 09 16 23 30
Декабрь
07 14


Эта статья является конспектом лекции.

Эта статья ещё не вычитана. Пожалуйста, вычитайте её и исправьте ошибки, если они есть.
Личные инструменты
Разделы