Algebry F i algebry F są strukturami matematycznymi, które odgrywają kluczową rolę w rozumowaniu typów indukcyjnych (lub typów rekurencyjnych ).
Algebry F.
Zaczniemy od algebry F. Spróbuję być tak prosty, jak to możliwe.
Chyba wiesz, co to jest typ rekurencyjny. Na przykład jest to typ listy liczb całkowitych:
data IntList = Nil | Cons (Int, IntList)
Oczywiste jest, że jest rekurencyjne - w rzeczywistości jego definicja odnosi się do samego siebie. Jego definicja składa się z dwóch konstruktorów danych, które mają następujące typy:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Zauważ, że napisałem typ Nil
jako () -> IntList
, a nie po prostu IntList
. Są to w rzeczywistości typy równoważne z teoretycznego punktu widzenia, ponieważ ()
typ ma tylko jednego mieszkańca.
Jeśli napiszemy podpisy tych funkcji w bardziej teoretyczny sposób, otrzymamy
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
gdzie 1
jest zestaw jednostek (zestaw z jednym elementem), a A × B
operacja jest iloczynem krzyżowym dwóch zestawów A
i B
(to znaczy zestawu par, w (a, b)
którym a
przechodzą wszystkie elementy A
i b
przechodzą przez wszystkie elementy B
).
Rozłączne połączenie dwóch zbiorów A
i B
jest zbiorem A | B
będącym połączeniem zbiorów {(a, 1) : a in A}
i {(b, 2) : b in B}
. Zasadniczo jest to zestaw wszystkich elementów z obu A
i B
, ale z każdym z tych elementów „oznaczonymi” jako należącymi do jednego A
lub jednego z nich B
, więc kiedy wybieramy dowolny element z A | B
, od razu będziemy wiedzieć, czy ten element pochodzi A
czy z B
.
Możemy „połączyć” Nil
i Cons
funkcje, aby utworzyły jedną funkcję działającą na zestawie 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Rzeczywiście, jeśli Nil|Cons
funkcja jest zastosowana do ()
wartości (która oczywiście należy do 1 | (Int × IntList)
zestawu), zachowuje się tak, jakby była Nil
; jeśli Nil|Cons
jest zastosowany do dowolnej wartości typu (Int, IntList)
(takie wartości są również w zestawie 1 | (Int × IntList)
, zachowuje się jak Cons
.
Teraz rozważ inny typ danych:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Ma następujące konstruktory:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
które można również połączyć w jedną funkcję:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Można zauważyć, że obie te joined
funkcje mają podobny typ: oba wyglądają
f :: F T -> T
gdzie F
jest rodzajem transformacji, która trwa nasz typ i daje bardziej złożony typ, który składa się x
i |
operacje, zwyczajów T
i ewentualnie inne typy. Na przykład dla IntList
i IntTree
F
wygląda następująco:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Od razu możemy zauważyć, że każdy typ algebraiczny można zapisać w ten sposób. Rzeczywiście dlatego nazywane są „algebraicznymi”: składają się z szeregu „sum” (związków) i „produktów” (produktów krzyżowych) innych typów.
Teraz możemy zdefiniować algebrę F. Algebra F to tylko para (T, f)
, gdzie T
jest jakiś typ i f
jest funkcją typu f :: F T -> T
. W naszych przykładach F-algebrami są (IntList, Nil|Cons)
i (IntTree, Leaf|Branch)
. Zauważ jednak, że pomimo tego typu f
funkcji jest taki sam dla każdego F T
i f
same mogą być dowolne. Na przykład (String, g :: 1 | (Int x String) -> String)
lub (Double, h :: Int | (Double, Double) -> Double)
dla niektórych g
i h
są również algebrami F dla odpowiednich F.
Następnie możemy wprowadzić homomorfizmy algebry F, a następnie początkowe algebry F , które mają bardzo przydatne właściwości. W rzeczywistości (IntList, Nil|Cons)
jest to początkowa algebra F1 i (IntTree, Leaf|Branch)
jest to początkowa algebra F2. Nie przedstawię dokładnych definicji tych terminów i właściwości, ponieważ są one bardziej złożone i abstrakcyjne niż jest to konieczne.
Niemniej fakt, że powiedzmy, (IntList, Nil|Cons)
jest algebrą F, pozwala nam zdefiniować fold
funkcję podobną do tego typu. Jak wiadomo, fold jest rodzajem operacji, która przekształca niektóre rekurencyjne typy danych w jedną skończoną wartość. Na przykład możemy złożyć listę liczb całkowitych w jedną wartość, która jest sumą wszystkich elementów na liście:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Możliwe jest uogólnienie takiej operacji na dowolnym rekurencyjnym typie danych.
Poniżej znajduje się podpis foldr
funkcji:
foldr :: ((a -> b -> b), b) -> [a] -> b
Zauważ, że użyłem nawiasów klamrowych do oddzielenia dwóch pierwszych argumentów od ostatniego. To nie jest prawdziwa foldr
funkcja, ale jest z nią izomorficzna (to znaczy, możesz łatwo uzyskać jedną od drugiej i odwrotnie). Częściowo zastosowany foldr
będzie miał następujący podpis:
foldr ((+), 0) :: [Int] -> Int
Widzimy, że jest to funkcja, która pobiera listę liczb całkowitych i zwraca jedną liczbę całkowitą. Zdefiniujmy taką funkcję w kategoriach naszego IntList
typu.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Widzimy, że ta funkcja składa się z dwóch części: pierwsza część określa zachowanie tej funkcji po Nil
części IntList
, a druga część określa zachowanie funkcji po Cons
części.
Załóżmy teraz, że programujemy nie w języku Haskell, ale w jakimś języku, który pozwala na użycie typów algebraicznych bezpośrednio w podpisach typów (cóż, technicznie Haskell pozwala na użycie typów algebraicznych przez krotki i Either a b
typy danych, ale doprowadzi to do niepotrzebnej gadatliwości). Rozważ funkcję:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Można zauważyć, że reductor
jest to funkcja typu F1 Int -> Int
, podobnie jak w definicji F-algebry! Rzeczywiście, para (Int, reductor)
jest algebrą F1.
Ponieważ IntList
jest to początkowa algebra F1, dla każdego typu T
i dla każdej funkcji r :: F1 T -> T
istnieje funkcja, zwana katamorfizmem, dla r
której konwertuje IntList
się T
i taka funkcja jest unikalna. Rzeczywiście, w naszym przykładzie catamorphism za reductor
to sumFold
. Uwaga, jak reductor
i sumFold
są podobne: mają prawie taką samą strukturę! W reductor
definicji s
użycie parametru (którego typ odpowiada T
) odpowiada wykorzystaniu wyniku obliczenia sumFold xs
w sumFold
definicji.
Żeby było jaśniej i pomóc zobaczyć wzór, oto kolejny przykład, a my znów zaczniemy od wynikowej funkcji składania. Rozważ append
funkcję, która dołącza swój pierwszy argument do drugiego:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Tak to wygląda na naszych IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Ponownie spróbujmy napisać reduktor:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
to katamorfizm, w appendReductor
który przemienia IntList
się IntList
.
Zasadniczo algebry F pozwalają nam definiować „fałdy” rekurencyjnych struktur danych, to znaczy operacje, które redukują nasze struktury do pewnej wartości.
F-carbongebras
Węglowodory F to tak zwane „dualne” określenie dla algebr F. Pozwalają nam zdefiniować unfolds
rekurencyjne typy danych, czyli sposób konstruowania struktur rekurencyjnych z pewnej wartości.
Załóżmy, że masz następujący typ:
data IntStream = Cons (Int, IntStream)
To jest nieskończony strumień liczb całkowitych. Jego jedyny konstruktor ma następujący typ:
Cons :: (Int, IntStream) -> IntStream
Lub pod względem zestawów
Cons :: Int × IntStream -> IntStream
Haskell pozwala na dopasowanie wzorca do konstruktorów danych, dzięki czemu można zdefiniować następujące funkcje działające na IntStream
s:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Możesz oczywiście łączyć te funkcje w jedną funkcję typu IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Zauważ, że wynik funkcji pokrywa się z algebraiczną reprezentacją naszego IntStream
typu. Podobnie można zrobić w przypadku innych typów danych rekurencyjnych. Być może już zauważyłeś wzór. Mam na myśli rodzinę funkcji typu
g :: T -> F T
gdzie T
jest jakiś typ. Od teraz będziemy definiować
F1 T = Int × T
Teraz F-węgielgebra jest parą (T, g)
, w której T
jest typem i g
jest funkcją typu g :: T -> F T
. Na przykład (IntStream, head&tail)
jest węgiel F1. Ponownie, podobnie jak w F-algebrach, g
i T
może być dowolne, na przykład, (String, h :: String -> Int x String)
jest także węglową F1 przez jakiś czas.
Wśród wszystkich F-węgli F są takie tak zwane F-węgiel F terminali , które są dualne do początkowych F-algeb. Na przykład IntStream
jest terminalem F-carbongebra. Oznacza to, że dla każdego typu T
i dla każdej funkcji p :: T -> F1 T
istnieje funkcja zwana anamorfizmem , która przekształca T
się IntStream
i taka funkcja jest unikalna.
Rozważ następującą funkcję, która generuje strumień kolejnych liczb całkowitych, zaczynając od podanej:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Teraz sprawdźmy funkcję natsBuilder :: Int -> F1 Int
, to znaczy natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Ponownie widzimy pewne podobieństwo między nats
i natsBuilder
. Jest bardzo podobny do połączenia, które zaobserwowaliśmy wcześniej z reduktorami i fałdami. nats
jest anamorfizmem dla natsBuilder
.
Kolejny przykład: funkcja, która przyjmuje wartość i funkcję i zwraca strumień kolejnych zastosowań funkcji do wartości:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Jego funkcja konstruktora jest następująca:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Potem iterate
jest anamorfizm iterateBuilder
.
Wniosek
Krótko mówiąc, algebry F pozwalają definiować fałdy, to znaczy operacje, które redukują strukturę rekurencyjną do jednej wartości, a algebry F pozwalają robić coś przeciwnego: konstruować [potencjalnie] nieskończoną strukturę z jednej wartości.
W rzeczywistości w Haskell F-algebry i F-carbongebras pokrywają się. Jest to bardzo przyjemna właściwość, która jest konsekwencją obecności „dolnej” wartości w każdym typie. Tak więc w Haskell można tworzyć i rozkładać dla każdego typu rekurencyjnego. Jednak model teoretyczny stojący za tym jest bardziej złożony niż ten, który przedstawiłem powyżej, więc celowo tego uniknąłem.
Mam nadzieję że to pomoże.