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подобную функцию для этого типа. Как вы знаете, сложение - это своего рода операция, которая преобразует некоторый рекурсивный тип данных в одно конечное значение. Например, мы можем сложить список целых чисел в одно значение, которое является суммой всех элементов в списке:
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, и такая функция является уникальной. Действительно, в нашем примере катаморфизм для reductoris 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-коалгеброй для некоторого h.
Среди всех 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-коалгебры совпадают. Это очень приятное свойство, которое является следствием наличия значения «bottom» в каждом типе. Таким образом, в Haskell могут быть созданы как сгибы, так и сгибы для каждого рекурсивного типа. Однако теоретическая модель, стоящая за этим, является более сложной, чем та, которую я представил выше, поэтому я намеренно избегал ее.
Надеюсь это поможет.