wizzard0 ([info]wizzard0) wrote,

Алгебра данных и исчисление мутаций (перевод)

Решил перевести для не-англоговорящих коллег найденное
простое и наглядное введение в ADT.
Комментарии и исправления приветствуются :)

Ссылка на оригинал – в конце поста.


С увеличением популярности языков вроде F# и Haskell, многие люди впервые сталкиваются с понятием алгебраического типа данных. Когда этот термин употребляется без пояснений, он практически всегда становится источником путаницы. В каком смысле типы «алгебраические»? Есть какие-то параллели между обьектами матанализа и типами данных Haskell? Могу ли я создать тип-«полином»? Нужно ли мне помнить формулу квадрата суммы? Имеют ли смысл преобразования, например, дифференциального исчисления для алгебраических типов данных? Разве это не просто набор абстрактных слов?

Мы рассмотрим эти вопросы, и постараемся развеять тайну над этим важным концептом функциональных языков.

Алгебраические выражения из типов

Чтобы понять концепцию алгебраических типов данных, нам надо провести параллели между понятиями, которые мы связываем с алгеброй, и понятиями, которые мы связываем с типами данных

В алгебраических выражениях мы можем столкнуться с простой одинокой переменной:

x

В выражении, описывающем тип данных, мы можем столкнуться с обыкновенным типом:

int

Формулы в алгебре могут также быть композицией других выражений, например, мы можем встретить

x*x

И аналогично для типов данных:

int*int

«Произведение» двух типов на самом деле означает тип пар из этих типов. В некоторых языках это записывается как pair<int,int>, но, как станет видно дальше, лучше пока думать об этом как о произведении.

Конечно, формулу выше можно упростить до х**2 (икс квадрат), и т.д. Можно написать и int**2

Среди формул алгебры также можно встретить такое выражение:


x+x

Та же концепция существует для типов:

int+int

«Сумма» двух типов обозначает тип «выбор из двух вариантов», который может иметь либо значение левой части, либо правой (но не оба сразу). В этом примере значок «сумма» может показаться излишним, потому что в любом случае ясно, что значение этого типа имеет тип int.

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

type balance = int+int

Мы могли бы интерпретировать значения слева как «оставшиеся деньги» , а значения справа – как «оставшиеся долги»

Поэтому, если мы написали функцию для вывода баланса на экран, она может выглядеть, например, так:

string describe(balance b) {
if (b.is_left()) {
return “The bank owes you $” + b.left();
} else {
return “You owe the bank $” + b.right();
}
}


Для многих людей сумма типов менее очевидна, чем произведение типов. Если вспомнить про аналогию произведения с типом pair<int,int>, то для суммы аналогом являются обьединения (unions) в С, или boost:variant<int,int> в С++.

Теперь мы можем использовать эти функции-комбинирующие-типы чтобы составлять сложные выражения:

(int*int*string)+(date*(string+int))

Точно так же, как мы могли их использовать для составления алгебраических выражений:

(x*x*y)+(z*(y+x))

Но, пожалуй, вначале мы посмотрим на штуку попроще:

1

Что означает «единица» на уровне типов? Так обозначается «единичный тип», который имеет только одно значение, обозначаемое как (). Иметь экземпляр единичного типа не очень-то полезно – ведь наперед известно, что там внутри :). Но он полезен в выражениях, например, если рассмотреть такое уравнение:

1+1=2

Теперь «два» - это тип «бит» (или булево значение), поскольку экземпляр этого типа может принимать либо «левое значение» (заранее известное), либо «правое». Может, вам поможет такой пример:


int+int=2*int

Иными словами, сумма двух типов int – то же самое, что и произведение boolean*int (значение переменной типа boolean скажет вам, по какую сторону суммы находится значение типа int )

Кроме того, как и ожидалось,

1*int=int

Другими словами, иметь пару из единичного типа и int – это то же самое, что иметь просто int (т.к. мы всегда знаем, какое значение имеет экземпляр единичного типа)

Вот еще одно натуральное число, которое заслуживает особого внимания:

0

Это особый тип, известный как «пустое множество» или «void» (слегка отличающийся от типа void в C-подобных языках, простите за перегруженность терминов). В отличие от единичного типа, который имеет одно значение, тип-ничто не имеет значений. Экземпляр этого типа нельзя создать.

Это означает, например, что

1+0=1

Потому что если у вас есть сумма единичного типа слева и пустое множество справа, то у этого типа могут быть только значения «слева» (потому что значение справа нельзя создать), поэтому можно с уверенностью предположить, что эту сумму типов можно упростить до единичного типа.

Аналогично,

1*0=0

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

Типы, описанные до сих пор, весьма полезны, и, надеюсь, понятно, как они отражают аналогичные алгебраические выражения. Однако полезны и более сложные типы, и более сложные выражения (невзирая на то, что на первый взгляд они могут показаться излишне сложными)

Например, как описать список чисел (list<int>) в виде алгебраических выражений? Чтобы ближе подойти к решению, можно рассмотреть возможные варианты списков, которые мы можем создать. Например, список чисел может быть пустым, может состоять из одного числа, из двух, трех и т.д. Каждое «или» здесь следует понимать как сумму, и таким образом (приняв, что пустой список эквивалентен экземпляру единичного типа), мы приходим к типу, описанному бесконечной суммой:

1+int+int**2+int**3+…+int**n

Мы можем оставить это выражение как есть, или переписать его в виде суммы ряда («Σ-нотация»). Однако, в мире типов такое бесконечное расширение описывается при помощи рекурсивных типов. Рекурсивный тип имеет обозначение (символ), и описывается выражением, внутри которого этот символ обозначает значение этого же типа. В случае нашего списка целых чисел, это записывается так:

μX.(1+int*X)

Это можно произносить как «мю икс, один плюс инт умножить на икс». Забудьте о «мю», если вас смущают греческие буквы, оно всего лишь значит «смотрите, это рекурсивный тип». Важно то, что такой тип следует рассматривать как бесконечно раскрывающееся выражение, которое вы получите, если будете много раз подставлять весь тип вместо переменной Х. К примеру, если подставить его один раз, мы получим:

1+int*(μX.(1+int*X))

И еще раз:


1+int*(1+int*(μX.(1+int*X)))

При помощи свойства дистрибутивности операций * и +, это можно упростить до:

1+int+int**2 * (μX.(1+int*X))

И если мы будем продолжать выполнять эту подстановку дальше, мы получим такую же бесконечную сумму, как мы ожидали для этого списка int’ов.

Хотя этот формализм выглядит далеким от языка описания типов в F#, вы можете быстро туда вернуться, если заменить «мю» на слово «type», «.» на «=», и воспользоваться пустыми конструкторами для обозначения единичных типов. Например:

type IntList = Nil | Cons of int*IntList

аналогичен

μIntList.(1+int*IntList)

Опять тот самый тип списков целых чисел, который мы уже определили (если не считать использования IntList вместо X)

Такое обозначение рекурсивных типов данных немного непривычно, но оно дает те же самые бесконечные ряды, которые мы видим в алгебре, и очень удобно для рекурсивных типов с несколькими ветвями рекурсии, например, вот тип двоичных деревьев Int’ов:

μX.(int+X*X)

Иными словами, бинарное дерево является либо листом (содержащим значение типа int), либо веткой (содержащей еще пару бинарных деревьев)

Мы можем выразить очень большую категорию типов таким способом (более подробно см. «Библия теории типов» (англ.)). Более того, существующие знания алгебры могут быть перенесены в этот мир, где мы находим много похожих вещей. Но эта формализация типов идет глубже, и у нас есть еще нерассмотренные аналогии.

∂ для данных

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

Мы раньше обсуждали способы воссоздания функций-мутаторов, вдохновленные самой известной структурой данных в своем роде: the Zipper

В 2001 году Конор МакБрайд сделал замечательное открытие о алгебраических типах данных, и концепцию молнии, которую он развил 4 года спустя (вместе с соавторами Эбботом, Ганой и Альтенкирхом)

Ключом этого открытия является концепция контекста-с-одной-дыркой. Контекст с дыркой можно рассматривать как тип данных «с дыркой в нём». Это понятие тесно связано с понятием мутации, поскольку значение может быть «обновлено» «затыканием» этой дырки.

Рассмотрим простой пример, тройку целых чисел. Ее тип будет int*int*int, или int**3. Теперь мы можем взять значение этого типа:

(98, 76, 54)

Мы хотим «обновить» одно из этих значений, при помощи «проделывания дыры» в структуре данных там, где мы хотим изменить конкретное значение. Другими словами, мы можем изменить первое, второе или третье значение этого кортежа (используя «_» для обозначения дырки):

(_, 76, 54) 
(98, _, 54)
(98, 76, _)

Эти три случая представляют собой все способы «проделывания дыры» в этом кортеже данных, и мы можем отразить это при помощи соответствующего типа. Поскольку случаев три, тип должен быть суммой трех элементов. Каждый элемент состоит из 2 чисел (в разных сочетаниях), поэтому мы можем довольно просто вывести этот тип «однодырочного контекста»:

(int*int)+(int*int)+(int*int)

Иными словами, первые int*int соответствуют типу «(_, 76, 54)», вторые int*int соответствуют типу «(98, _, 54)», и третьи int*int соответствуют типу «(98, 76, _)».

Неплохо бы упростить это выражение, поэтому мы вначале введем квадраты

int**2+int**2+int**2

а потом соединим слагаемые:

3*int**2

Иными словами, тип однодырочного контекста троек целых чисел – один из 3 пар целых чисел.

Теперь странная штука, мы начали с типа

int**3

И, найдя ее «однодырочный контекст», мы получили тип

3*int**2

Это ужасно похоже на производную из дифференциального исчисления!

На самом деле это именно то, что открыл МакБрайд: производная алгебраического типа данных – это тип его «однодырочных контекстов». Расширив эту операцию до рекурсивных типов (см. Его статью), мы можем использовать операцию дифференцирования чтобы автоматически выводить функции для изменения и итерации по произвольным структурам данных.

Статьи по теме:

До МакБрайда, Андре Джоэл изучал концепцию комбинаторных видов (построенную на концепции производящих функций – формализм, сходный с алгебраическими типами данных), где также было открыто использование производной.

МакБрайд также является одним из создателей языка программирования Epigram, языка с мощной теорией типов.

В следующей статье мы рассмотрим реализацию концепции «производных от типов» в краткой программе на Haskell.

* Оригинал статьи: http://blog.lab49.com/archives/3011



  • Post a new comment

    Error

    Your reply will be screened

    Your IP address will be recorded 

  • 28 comments

[info]lionet

October 31 2009, 23:59:12 UTC 2 years ago

См. также:

http://community.livejournal.com/fprog/2921.html

[info]deni_ok

November 1 2009, 06:31:18 UTC 2 years ago

Прекрасная статья, спасибо за перевод! Будем пиарить.

[info]zhengxi

November 1 2009, 08:05:58 UTC 2 years ago

Интересная статья.

Не очень очевидна эквивалентность 1+int+int**2+int**3+…+int**n и μX.(1+int*X)
Первый ведь vector, а второй list, так ?

[info]deni_ok

November 1 2009, 08:20:33 UTC 2 years ago

Re: Интересная статья.

Чистая алгебра: подстановка рекурсивного определения и, затем, раскрытие скобок. Эти шаги повторяются неограниченным образом
μX.(1+int*X)=
1+int*(μX.(1+int*X))=
1+int*(1+int*(μX.(1+int*X)))=
1+int+int^2*(μX.(1+int*X))=
1+int+int^2*(1+int*(μX.(1+int*X)))=
1+int+int^2+int^3*(μX.(1+int*X))=
1+int+int^2+int^3*(1+int*(μX.(1+int*X)))=
1+int+int^2+int^3+int^4*(μX.(1+int*X))=
...

[info]zhengxi

November 1 2009, 08:26:37 UTC 2 years ago

Re: Интересная статья.

алгебра-то понятна.

но: на первом варианте не реализовать car/cdr, а на втором не получить длину за O(1).

[info]deni_ok

November 1 2009, 09:00:54 UTC 2 years ago

Re: Интересная статья.

Это всё уже разговоры про семантику некоторой реализации ATD, в которой эти формы записи различаются. Речь же как раз о том, что алгебраически это - одно и то же.

[info]zhengxi

November 1 2009, 09:23:13 UTC 2 years ago

Вот в этом и был вопрос: имеет ли для ADT значение форма записи или нет.

Алгебраически и int==short*2==bool**32 и любой сложный ADT можно превратить в массив бит, так ?
А из массива бит - в другой сложный ADT.
И это будет одно и тоже ?

[info]zhengxi

November 1 2009, 09:33:19 UTC 2 years ago

Или вот, проще:

2+2 и 2*2 - это одно и то же?

[info]deni_ok

November 1 2009, 09:48:50 UTC 2 years ago

Re: Или вот, проще:

Самый интересный подвопрос здесь 1*1=1? ;-)
Если осознать, почему это так, то может и другие вопросы пропадут?

[info]zhengxi

November 1 2009, 19:49:24 UTC 2 years ago

Re: Или вот, проще:

Тогда возникнет вопрос, почему в Хаскеле это не так ? :)
() и ((),()) - разные типы

[info]deni_ok

November 1 2009, 20:19:43 UTC 2 years ago

Re: Или вот, проще:

и Nil, который тоже маркируется 1 разный, отличный от них.
1 здесь - символ метаязыка, объединяющий все 0-арные конструкторы данных.

[info]deni_ok

November 1 2009, 08:33:27 UTC 2 years ago

Re: Интересная статья.

Там опечатка, сумма должна быть бесконечной:
1+int+int**2+int**3+…+int**n+…

[info]_winnie

November 1 2009, 11:57:26 UTC 2 years ago

Re: Интересная статья.

Странно. Это множество конечных списков. Но среди них нет бесконечного.

[info]deni_ok

November 1 2009, 12:07:43 UTC 2 years ago

Re: Интересная статья.

И сколько же элементов содержит множество допустимых длин конечных списков? :-)

[info]_winnie

November 1 2009, 12:12:43 UTC 2 years ago

Re: Интересная статья.

Все натуральные числа. Но ни одно натуральное число не является длиной бесконечного списка, который тоже входит в data X = Nil () | List Int X

[info]deni_ok

November 1 2009, 12:29:39 UTC 2 years ago

Re: Интересная статья.

Это какой-то спор о терминах. Сумма
μX.(1+int*X)=1+int+int**2+int**3+…+int**n+…
должна быть бесконечной в том же смысле, в котором бесконечно число натуральных чисел
μX.(1+X)=1+1+1+1+…+1+…
Это стандартный математический термин: либо мы суммируем до конечного n, либо, имея в виду отсутствие верхней границы, пишем бесконечность в качестве верхнего предела (и говорим тоже - бесконечность). В том же Хаскелле конечный Int определяется в документации (GIH) как сумма
data Int     = -65532 | ... | -1 | 0 | 1 | ... | 65532
а бесконечный Integer как
data Integer = ... | -2 | -1 | 0 | 1 | 2 | ...

[info]_winnie

November 1 2009, 12:49:47 UTC 2 years ago

Re: Интересная статья.

>Это какой-то спор о терминах.
Не-не-не. Да, конечных списков - бесконечное число, но бесконечного списка среди них нет.


Мне просто показалось интересным, что в сумму (1+int+int**2+int**3+…+int**n+…) входят все списки конечной длины, но не входит бесконечный список. А в μX.(1+int*X) - вроде как входит. Или нет. Неясно. В соответсвующем определении на Haskell - входит, а в записи μX.(1+int*X) - не знаю.

[info]deni_ok

November 1 2009, 13:11:08 UTC 2 years ago

Re: Интересная статья.

Мне не очень понятно, что такое бесконечный список, как нечто актуально данное. Выражение с μ дает корекурсивное определение списка. Выполняя эту корекурсию получаем сначала первое слагаемое суммы: пустой список (1), потом отделяется второе слагаемое: список из одного целого (int), на следующем шаге к ним присоединяется третье слагаемое: список из двух целых (int*int) и так далее (потенциально до бесконечности). В таком (потенциальном) смысле бесконечность присутствует и там и там совершенно одинаковым образом, не являясь какой-то особой проблемой. То, что я предложил дописать хвостик +… в конце лишь отражает тот факт, что мы не имеем верхней границы в таком описании.

[info]gds

November 1 2009, 13:44:49 UTC 2 years ago

Re: Интересная статья.

а почему для описания типов алгебраически-строящихся данных используются корекурсивные типы, фактически не ограничивающие значение с типом "list α" конечной длиной?
Нет ли здесь антисемитизма?

[info]deni_ok

2 years ago

[info]gds

2 years ago

[info]deni_ok

2 years ago

[info]nivanych

November 1 2009, 15:09:49 UTC 2 years ago

Re: Интересная статья.

Бесконечные списки, построенные на алгебраических типах,
существенно зависят от возможности использовать общерекурсивное описание.
Коиндукция этого не требует.

[info]zhengxi

November 1 2009, 08:36:21 UTC 2 years ago

Если 2 - это bool, то int - это 232
int+int+int+int = 4*232 = 234
int*int*int*int = (232)4 = 2128
int*int+int*int = 2*264 = 265

ADT как число = мощность множества
ceil(log2(ADT)) = минимальное число бит которым его можно представить

[info]a_ilichevskii

November 1 2009, 10:20:36 UTC 2 years ago

Спасибо! совершенно роскошно.

[info]_winnie

November 1 2009, 11:57:56 UTC 2 years ago

Сууупер! Спасибо!

[info]holmic

November 1 2009, 15:41:43 UTC 2 years ago

спасибо, про Zipper'ы особенно понравилось

[info]beroal

November 7 2009, 07:54:21 UTC 2 years ago

Могу ли я создать тип-«полином»?
Наверное, стоило назвать эти типы данных «полиномиальными».
Create an Account
Forgot your login or password?
Facebook Twitter More login options
English • Español • Deutsch • Русский…