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
, и такая функция является уникальной. Действительно, в нашем примере катаморфизм для reductor
is 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-коалгеброй для некоторого 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 могут быть созданы как сгибы, так и сгибы для каждого рекурсивного типа. Однако теоретическая модель, стоящая за этим, является более сложной, чем та, которую я представил выше, поэтому я намеренно избегал ее.
Надеюсь это поможет.