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 дозволяє визначати відповідність на конструкторах даних, тому ви можете визначити наступні функції, що працюють над IntStreams:
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 можна створити як складки, так і розгортання для кожного рекурсивного типу. Однак теоретична модель, що стоїть за цим, є більш складною, ніж та, яку я представив вище, тому я навмисно її уникав.
Сподіваюсь, це допомагає.