F-cebirleri ve F-kömürler, endüktif tipler (veya özyinelemeli tipler ) hakkında akıl yürütmede etkili olan matematiksel yapılardır .
F-cebiri
İlk önce F-cebirleriyle başlayacağız. Mümkün olduğunca basit olmaya çalışacağım.
Sanırım yinelemeli tipin ne olduğunu biliyorsun. Örneğin, bu tamsayıların listesi için bir türdür:
data IntList = Nil | Cons (Int, IntList)
Özyinelemeli olduğu açıktır - gerçekten de tanımı kendini ifade eder. Tanımı, aşağıdaki türlere sahip iki veri yapıcısından oluşur:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Ben sadece Nil
as () -> IntList
değil yazdım IntList
. Bunlar aslında teorik açıdan eşdeğer türlerdir, çünkü ()
türün sadece bir sakini vardır.
Bu işlevlerin imzalarını daha kuramsal bir şekilde yazarsak,
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
burada 1
bir birim grubu (bir elemanı ile ayarlanır) ve A × B
işlem iki set enine bir üründür A
ve B
(olduğundan, çift grubu tüm elemanları geçer ve tüm elemanları geçer(a, b)
a
A
b
B
).
İki kümenin ayrık birlik A
ve B
kümesidir A | B
setleri birleşmedir {(a, 1) : a in A}
ve {(b, 2) : b in B}
. Esasen her iki tüm unsurların bütünüdür A
ve B
mensup ya olarak değil, 'işaretlenmiş' bu unsurların her biri ile A
ya da B
, bu yüzden biz herhangi bir unsur almaya gelirken A | B
biz hemen bu eleman gelip gelmediğini bilecek A
veya gelenB
.
'Birleştirebiliriz' Nil
ve Cons
fonksiyonlar, böylece bir set üzerinde çalışan tek bir fonksiyon oluşturacaklar 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Gerçekten de, Nil|Cons
fonksiyon ()
değere uygulanırsa (ki bu açık bir şekilde 1 | (Int × IntList)
kümeye aittir ), sanki sanki öyle davranır Nil
; Nil|Cons
herhangi bir tür değere uygulanırsa (Int, IntList)
(bu değerler kümede de yer alıyorsa) olarak 1 | (Int × IntList)
davranır Cons
.
Şimdi başka bir veri türü düşünün:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Aşağıdaki kuruculara sahiptir:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
ayrıca tek bir fonksiyonda birleştirilebilir:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Bu joined
fonksiyonların her ikisinin de benzer tipte olduğu görülebilir : her ikisi de
f :: F T -> T
burada F
eden türü alır ve aşağıdakilerden oluşmaktadır daha karmaşık tip verir dönüşümün bir tür x
ve |
işlemleri, kullanımları T
ve muhtemelen diğer türleri. Örneğin, IntList
ve IntTree
F
aşağıdaki gibi görünür:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Herhangi bir cebirsel türün bu şekilde yazılabileceğini hemen fark edebiliriz. Aslında, bu yüzden 'cebir' olarak adlandırılırlar: diğer türlerin bir dizi 'toplamı' (birliği) ve 'ürünleri' (çapraz ürünler) içerirler.
Şimdi F-cebirini tanımlayabiliriz. F-cebiri sadece bir çifttir (T, f)
, burada T
bir türdür ve tipin f
bir fonksiyonudur f :: F T -> T
. Örneklerimizde F-cebirleri (IntList, Nil|Cons)
ve (IntTree, Leaf|Branch)
. Bununla birlikte, bu tür f
fonksiyonlara rağmen her bir F için aynı olduğunu T
ve f
kendilerinin keyfi olabileceğini unutmayın. Örneğin (String, g :: 1 | (Int x String) -> String)
veya (Double, h :: Int | (Double, Double) -> Double)
bazıları için g
ve h
karşılık gelen F için F-cebiridir.
Daha sonra F-cebir homomorfizmlerini ve daha sonra çok faydalı özelliklere sahip olan ilk F-cebirlerini tanıtabiliriz . Aslında, (IntList, Nil|Cons)
bir ilk F1 cebiridir ve (IntTree, Leaf|Branch)
bir ilk F2 cebiridir. Gerektiğinden daha karmaşık ve soyut oldukları için bu terimlerin ve özelliklerin tam tanımlarını sunmayacağım.
Bununla birlikte, örneğin, (IntList, Nil|Cons)
F-cebiri olduğu gerçeği fold
, bu tipte-benzeri bir fonksiyon tanımlamamıza izin verir . Bildiğiniz gibi, katlama, bazı özyinelemeli veri tipini sonlu bir değere dönüştüren bir işlemdir. Örneğin, bir tamsayı listesini listedeki tüm öğelerin toplamı olan tek bir değere katlayabiliriz:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Bu işlemi herhangi bir özyinelemeli veri türü üzerinde genelleştirmek mümkündür.
Aşağıdakiler foldr
işlevin bir imzasıdır :
foldr :: ((a -> b -> b), b) -> [a] -> b
İlk iki argümanı sonuncusundan ayırmak için kaşlı ayraç kullandığımı unutmayın. Bu gerçek bir foldr
işlev değildir , ancak onun için izomorfiktir (yani, kolayca diğerinden alabilirsiniz ve tersi). Kısmen başvuruda foldr
şu imza bulunur:
foldr ((+), 0) :: [Int] -> Int
Bunun tamsayıların listesini alıp tek bir tamsayı döndüren bir işlev olduğunu görebiliriz. Bu işlevi türümüze göre tanımlayalım IntList
.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Bu fonksiyonun iki kısımdan oluştuğunu görüyoruz: ilk kısım bu fonksiyonun bir Nil
kısmındaki davranışını, IntList
ikinci kısım fonksiyonun bir Cons
kısmındaki davranışını tanımlar .
Şimdi, Haskell'de değil, cebirsel tiplerin doğrudan tip imzalarında kullanılmasına izin veren bir dilde programladığımızı varsayalım (iyi, teknik olarak Haskell, cebirsel tiplerin tuples ve Either a b
veri tipi aracılığıyla kullanılmasına izin veriyor , ancak bu gereksiz ayrıntılara yol açacak). Bir işlev düşünün:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
F-cebirinin tanımında olduğu gibi reductor
tipin bir fonksiyonu olduğu görülebilir F1 Int -> Int
! Aslında, çift (Int, reductor)
bir F1 cebiridir.
Çünkü IntList
bir başlangıç F1 cebir her tür için, olduğu T
ve her bir işlev için r :: F1 T -> T
adında bir işlevi vardır vardır, catamorphism için r
, dönüştürür IntList
için T
, ve bu fonksiyon benzersizdir. Gerçekten de, bizim örneğimizde bir catamorphism reductor
olduğunu sumFold
. Nasıl reductor
ve sumFold
benzer olduklarına dikkat edin: neredeyse aynı yapıya sahiptirler! Olarak reductor
tanımlı s
parametre kullanım (tip olan tekabül T
hesaplama sonucunun kullanımına tekabül) sumFold xs
içinde sumFold
tanımı.
Sadece daha açık hale getirmek ve deseni görmenize yardımcı olmak için, başka bir örnek ve yine ortaya çıkan katlama işlevinden başlıyoruz. append
İlk argümanını ikincisine ekleyen işlevi düşünün :
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Şu şekilde görünüyor IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Yine, redüktörü yazmaya çalışalım:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
bir catamorphism olan appendReductor
olan dönüşümleri IntList
içine IntList
.
Yani, esasen, F-cebirleri, yinelemeli veri yapıları, yani yapılarımızı bir değere indiren operasyonlar üzerinde 'kıvrımlar' tanımlamamıza izin verir.
F-coalgebras
F-kömürgebras, F-cebirleri için 'ikili' terimdir. unfolds
Yinelemeli veri türleri, yani bir değerden yinelemeli yapılar inşa etmenin bir yolunu tanımlamamıza izin veriyorlar .
Aşağıdaki türe sahip olduğunuzu varsayalım:
data IntStream = Cons (Int, IntStream)
Bu sonsuz bir tamsayı akışıdır. Tek kurucusu aşağıdaki tiptedir:
Cons :: (Int, IntStream) -> IntStream
Veya setler açısından
Cons :: Int × IntStream -> IntStream
Haskell veri yapıcıları üzerinde desen eşleşmesine izin verir, böylece IntStream
s üzerinde çalışan aşağıdaki işlevleri tanımlayabilirsiniz :
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Doğal olarak bu işlevleri tek tip işlevde 'birleştirebilirsiniz' IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Fonksiyonun sonucunun tipimizin cebirsel temsiliyle nasıl çakıştığına dikkat edin IntStream
. Benzer şey diğer özyinelemeli veri türleri için de yapılabilir. Belki de deseni zaten fark ettiniz. Tür fonksiyonlardan oluşan bir aileden söz ediyorum
g :: T -> F T
bir tür nerede T
. Şu andan itibaren tanımlayacağız
F1 T = Int × T
Şimdi, F-coalgebra bir çift (T, g)
, T
bir tür ve g
tip bir fonksiyonudur g :: T -> F T
. Örneğin (IntStream, head&tail)
, bir F1 kömürüdür. Yine, tıpkı F-cebirlerinde olduğu gibi, g
veT
keyfi olabilir, örneğin, (String, h :: String -> Int x String)
bazı h için de F1-kömürgebradır.
Tüm F-kömürler arasında ilk F-cebirlerine çift olan terminal F-kömürler de denir . Örneğin IntStream
, bir terminal F kömürüdür. Bu, her tür T
ve her işlev için p :: T -> F1 T
, anamorfizm adı verilen ve dönüştüren bir fonksiyonun var olduğu anlamına gelir.T
için IntStream
ve böyle işlevini benzersizdir.
Verilen işlevden başlayarak ardışık bir tamsayı akışı oluşturan aşağıdaki işlevi göz önünde bulundurun:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Şimdi bir işlevi inceleyelim natsBuilder :: Int -> F1 Int
, yani natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Yine, nats
ve arasında bazı benzerlikler görebiliriz natsBuilder
. Daha önce redüktörler ve katlar ile gözlemlediğimiz bağlantıya çok benzer.nats
için bir anamorfizmdir natsBuilder
.
Başka bir örnek, bir değeri ve işlevi alan ve işlevin birbirini izleyen uygulamalarının akışını değere döndüren bir işlevdir:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Oluşturucu işlevi şu şekildedir:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
O zaman iterate
için bir anamorfizm iterateBuilder
.
Sonuç
Kısacası, F-cebirleri kıvrımların tanımlanmasına, yani özyinelemeli yapıyı tek bir değere indirgeyen operasyonlara izin verir ve F-kömürgebras bunun tersini yapmasına izin verir: tek bir değerden [potansiyel olarak] sonsuz bir yapı inşa eder.
Aslında Haskell'de F-cebirleri ve F-kömürgebrasları çakışır. Bu, her tipte 'alt' değerin varlığının bir sonucu olan çok güzel bir özelliktir. Yani Haskell'de her kıvrımlı tip için hem kıvrımlar hem de katlar oluşturulabilir. Bununla birlikte, bunun arkasındaki teorik model, yukarıda sunduğumdan daha karmaşıktır, bu yüzden kasten kaçındım.
Bu yardımcı olur umarım.