F-алгебри та F-вуглегебри - це математичні структури, які допомагають міркувати про індуктивні типи (або рекурсивні типи ).
F-алгебри
Почнемо спочатку з F-алгебр. Я постараюся бути максимально простим.
Я думаю, ви знаєте, що таке рекурсивний тип. Наприклад, це тип для списку цілих чисел:
data IntList = Nil | Cons (Int, IntList)
Очевидно, що він є рекурсивним - дійсно, його визначення стосується самого себе. Його визначення складається з двох конструкторів даних, які мають такі типи:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Зауважте, що я написав тип Nil
як () -> IntList
, а не просто IntList
. Це фактично еквівалентні типи з теоретичної точки зору, оскільки ()
тип має лише одного мешканця.
Якщо ми напишемо підписи цих функцій більш заданим теоретичним способом, то отримаємо
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
де 1
є набір одиниць (набір з одним елементом) і A × B
операція є поперечним добутком двох множин A
і B
(тобто набір пар, (a, b)
де a
проходить через всі елементи A
і b
проходить через усі елементи B
).
Неперервне з'єднання двох множин A
і B
являє собою множину, A | B
яка є об'єднанням множин {(a, 1) : a in A}
і {(b, 2) : b in B}
. По суті це сукупність усіх елементів як з, так A
і B
з кожним із цих елементів, "позначених" як належність до того A
чи іншого B
, тож коли ми виберемо будь-який елемент, A | B
ми одразу дізнаємось, чи походить цей елемент A
чи з нього B
.
Ми можемо "об'єднати" Nil
і Cons
функції, тому вони будуть утворювати єдину функцію, що працює над набором1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Дійсно, якщо Nil|Cons
функція застосовується до ()
значення (яке, очевидно, належить 1 | (Int × IntList)
заданому), то воно поводиться так, ніби воно було Nil
; якщо Nil|Cons
воно застосовується до будь-якого значення типу (Int, IntList)
(такі значення також є у наборі1 | (Int × IntList)
, воно поводиться як Cons
.
Тепер розглянемо інший тип даних:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Він має такі конструктори:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
які також можна об'єднати в одну функцію:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Видно, що обидві ці joined
функції мають аналогічний тип: вони обидва схожі
f :: F T -> T
де F
це свого роду перетворення , яке приймає наш тип і дає більш складний тип, який складається з x
і |
операції, звичаї T
і , можливо , інші типи. Наприклад, для IntList
іIntTree
F
виглядає так:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Ми можемо відразу помітити, що будь-який алгебраїчний тип можна записати таким чином. Дійсно, саме тому їх називають "алгебраїчними": вони складаються з ряду "сум" (об'єднань) та "продуктів" (перехресних продуктів) інших типів.
Тепер ми можемо визначити F-алгебру. F-алгебра - це просто пара (T, f)
, де T
є певний тип і f
є функцією типу f :: F T -> T
. У наших прикладах F-алгебри є (IntList, Nil|Cons)
і (IntTree, Leaf|Branch)
. Однак зауважте, що незважаючи на той тип f
функцій, однаковий для кожного F, T
і f
вони можуть бути довільними. Наприклад, (String, g :: 1 | (Int x String) -> String)
або (Double, h :: Int | (Double, Double) -> Double)
для деяких g
іh
є також F-алгебрами відповідного F.
Після цього ми можемо ввести гомоморфізми F-алгебри, а потім початкові F-алгебри , які мають дуже корисні властивості. Фактично, (IntList, Nil|Cons)
це початкова F1-алгебра і (IntTree, Leaf|Branch)
є початковою F2-алгеброю. Я не буду представляти точні визначення цих термінів та властивостей, оскільки вони складніші та абстрактніші, ніж потрібно.
Тим не менш, той факт, що, скажімо, (IntList, Nil|Cons)
є F-алгеброю, дозволяє нам визначити fold
подібну функцію для цього типу. Як відомо, fold - це певна операція, яка перетворює деякий рекурсивний тип даних в одне кінцеве значення. Наприклад, ми можемо скласти список цілих чисел у єдине значення, що є сумою всіх елементів у списку:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Можна узагальнити таку операцію на будь-якому рекурсивному типі даних.
Далі йде підпис foldr
функції:
foldr :: ((a -> b -> b), b) -> [a] -> b
Зауважте, що я використовував дужки, щоб відокремити перші два аргументи від останнього. Це не реальна foldr
функція, але вона ізоморфна їй (тобто ви можете легко дістати одну від іншої і навпаки). Частково застосований foldr
матиме такий підпис:
foldr ((+), 0) :: [Int] -> Int
Ми можемо бачити, що це функція, яка бере список цілих чисел і повертає єдине ціле число. Давайте визначимо таку функцію з точки зору нашого IntList
типу.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Ми бачимо, що ця функція складається з двох частин: перша частина визначає поведінку цієї функції Nil
частиною IntList
, а друга частина визначає поведінку функції Cons
частково.
Тепер припустимо, що ми програмуємо не в Haskell, а на якійсь мові, яка дозволяє використовувати алгебраїчні типи безпосередньо в підписах типів (ну, технічно Haskell дозволяє використовувати алгебраїчні типи через кортежі та Either a b
тип даних, але це призведе до зайвої багатослівності). Розглянемо функцію:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Видно, що reductor
це функція типу F1 Int -> Int
, як і у визначенні F-алгебри! Дійсно, пара (Int, reductor)
- це F1-алгебра.
Оскільки IntList
є початковою алгеброю F1, для кожного типу T
і для кожної функції r :: F1 T -> T
існує функція, яка називається катаморфізмом для r
, яка перетворюється IntList
на T
, і така функція є унікальною. Дійсно, у нашому прикладі катаморфізм для reductor
є sumFold
. Зауважте, як reductor
і sumFold
схожі: вони мають майже однакову структуру! У reductor
визначенні s
параметр використання (типу якого відповідає T
) відповідає використанню результату обчислення sumFold xs
у sumFold
визначенні.
Просто для того, щоб зробити це більш зрозумілим і допомогти побачити викрійку, ось ще один приклад, і ми знову починаємо з отриманої функції складання. Розглянемо append
функцію, яка додає свій перший аргумент до другого:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Ось як це виглядає на нашому IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Ще раз спробуємо виписати редуктор:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
- це катаморфізм, для appendReductor
якого трансформується IntList
в IntList
.
Отже, по суті, F-алгебри дозволяють нам визначити "складки" на рекурсивних структурах даних, тобто операції, які зводять наші структури до деякого значення.
F-вуглегебри
F-вуглегебри - це так званий "подвійний" термін для F-алгебр. Вони дозволяють нам визначити unfolds
для рекурсивних типів даних, тобто спосіб побудувати рекурсивні структури з деякого значення.
Припустимо, у вас такий тип:
data IntStream = Cons (Int, IntStream)
Це нескінченний потік цілих чисел. Єдиний конструктор має такий тип:
Cons :: (Int, IntStream) -> IntStream
Або з точки зору множин
Cons :: Int × IntStream -> IntStream
Haskell дозволяє визначати відповідність на конструкторах даних, тому ви можете визначити наступні функції, що працюють над IntStream
s:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Ви, природно, можете "об'єднати" ці функції в одну функцію типу IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Зауважте, як результат функції збігається з алгебраїчним поданням нашого IntStream
типу. Подібне може бути зроблено і для інших рекурсивних типів даних. Можливо, ви вже помітили закономірність. Я маю на увазі сімейство функцій типу
g :: T -> F T
де T
якийсь тип. Відтепер ми визначимось
F1 T = Int × T
Тепер F-вуглегебра - це пара (T, g)
, де T
є тип і g
є функцією типу g :: T -> F T
. Наприклад, (IntStream, head&tail)
це F1-вуглегебра. Знову ж таки, як у F-алгебрах, g
і T
може бути довільним, наприклад, (String, h :: String -> Int x String)
також є F1-вуглегебра протягом деякої години.
Серед усіх F-вуглегебр є так звані кінцеві F-вуглегебри , які є подвійними до початкових F-алгебр. Наприклад, IntStream
є кінцева F-вуглегебра. Це означає, що для кожного типу T
і для кожної функції p :: T -> F1 T
існує функція, яка називається анаморфізмом , яка перетворюється T
на IntStream
, і така функція є унікальною.
Розглянемо наступну функцію, яка генерує потік послідовних цілих чисел, починаючи з заданої:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Тепер перевіримо функцію natsBuilder :: Int -> F1 Int
, тобто natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Знову ми можемо побачити деяку схожість між nats
та natsBuilder
. Це дуже схоже на з'єднання, яке ми спостерігали раніше з редукторами і складками. nats
є анаморфізмом для natsBuilder
.
Інший приклад - функція, яка приймає значення і функцію і повертає потік послідовних застосувань функції до значення:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Його функція конструктора наступна:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Тоді iterate
анаморфізм для iterateBuilder
.
Висновок
Отже, коротко кажучи, F-алгебри дозволяють визначати складки, тобто операції, які зводять рекурсивну структуру вниз до єдиного значення, а F-вуглегебри дозволяють зробити зворотне: побудувати [потенційно] нескінченну структуру з одного значення.
Насправді в Хаскеллі F-алгебри і F-вуглегебри збігаються. Це дуже приємна властивість, яка є наслідком наявності значення "нижнього" в кожному типі. Тож у Haskell можна створити як складки, так і розгортання для кожного рекурсивного типу. Однак теоретична модель, що стоїть за цим, є більш складною, ніж та, яку я представив вище, тому я навмисно її уникав.
Сподіваюсь, це допомагає.