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 Niljako () -> 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 1jest zestaw jednostek (zestaw z jednym elementem), a A × Boperacja jest iloczynem krzyżowym dwóch zestawów Ai B(to znaczy zestawu par, w (a, b)którym aprzechodzą wszystkie elementy Ai bprzechodzą przez wszystkie elementy B).
Rozłączne połączenie dwóch zbiorów Ai Bjest zbiorem A | Bbę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 Ai B, ale z każdym z tych elementów „oznaczonymi” jako należącymi do jednego Alub jednego z nich B, więc kiedy wybieramy dowolny element z A | B, od razu będziemy wiedzieć, czy ten element pochodzi Aczy z B.
Możemy „połączyć” Nili Consfunkcje, aby utworzyły jedną funkcję działającą na zestawie 1 | (Int × IntList):
Nil|Cons :: 1 | (Int × IntList) -> IntList
Rzeczywiście, jeśli Nil|Consfunkcja 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|Consjest 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 joinedfunkcje mają podobny typ: oba wyglądają
f :: F T -> T
gdzie Fjest rodzajem transformacji, która trwa nasz typ i daje bardziej złożony typ, który składa się xi |operacje, zwyczajów Ti ewentualnie inne typy. Na przykład dla IntListi IntTree Fwyglą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 Tjest jakiś typ i fjest 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 ffunkcji jest taki sam dla każdego F Ti fsame mogą być dowolne. Na przykład (String, g :: 1 | (Int x String) -> String)lub (Double, h :: Int | (Double, Double) -> Double)dla niektórych gi hsą 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ć foldfunkcję 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 foldrfunkcji:
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 foldrfunkcja, ale jest z nią izomorficzna (to znaczy, możesz łatwo uzyskać jedną od drugiej i odwrotnie). Częściowo zastosowany foldrbę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 IntListtypu.
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 Nilczęści IntList, a druga część określa zachowanie funkcji po Consczęś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 btypy 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 reductorjest to funkcja typu F1 Int -> Int, podobnie jak w definicji F-algebry! Rzeczywiście, para (Int, reductor)jest algebrą F1.
Ponieważ IntListjest to początkowa algebra F1, dla każdego typu Ti dla każdej funkcji r :: F1 T -> Tistnieje funkcja, zwana katamorfizmem, dla rktórej konwertuje IntListsię Ti taka funkcja jest unikalna. Rzeczywiście, w naszym przykładzie catamorphism za reductorto sumFold. Uwaga, jak reductori sumFoldsą podobne: mają prawie taką samą strukturę! W reductordefinicji sużycie parametru (którego typ odpowiada T) odpowiada wykorzystaniu wyniku obliczenia sumFold xsw sumFolddefinicji.
Ż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ż appendfunkcję, 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
appendFoldto katamorfizm, w appendReductorktóry przemienia IntListsię 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ć unfoldsrekurencyjne 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 IntStreams:
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 IntStreamtypu. 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 Tjest jakiś typ. Od teraz będziemy definiować
F1 T = Int × T
Teraz F-węgielgebra jest parą (T, g), w której Tjest typem i gjest funkcją typu g :: T -> F T. Na przykład (IntStream, head&tail)jest węgiel F1. Ponownie, podobnie jak w F-algebrach, gi Tmoż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 IntStreamjest terminalem F-carbongebra. Oznacza to, że dla każdego typu Ti dla każdej funkcji p :: T -> F1 Tistnieje funkcja zwana anamorfizmem , która przekształca Tsię IntStreami 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 natsi natsBuilder. Jest bardzo podobny do połączenia, które zaobserwowaliśmy wcześniej z reduktorami i fałdami. natsjest 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 iteratejest 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.