F-algebras dan F-coalgebras adalah struktur matematika yang berperan dalam penalaran tentang tipe induktif (atau tipe rekursif ).
F-aljabar
Kami akan mulai dulu dengan F-algebras. Saya akan berusaha sesederhana mungkin.
Saya kira Anda tahu apa itu tipe rekursif. Misalnya, ini adalah tipe untuk daftar bilangan bulat:
data IntList = Nil | Cons (Int, IntList)
Jelas bahwa itu adalah rekursif - memang, definisi itu merujuk pada dirinya sendiri. Definisinya terdiri dari dua konstruktor data, yang memiliki tipe berikut:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Perhatikan bahwa saya telah menulis tipe Nil
as () -> IntList
, bukan hanya IntList
. Ini sebenarnya adalah tipe yang setara dari sudut pandang teoretis, karena ()
tipe hanya memiliki satu penduduk.
Jika kita menulis tanda tangan dari fungsi-fungsi ini dengan cara yang lebih teoritis, kita akan mendapatkannya
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
di mana 1
satu set unit (set dengan satu elemen) dan A × B
operasi adalah produk silang dari dua set A
dan B
(yaitu, set pasangan (a, b)
mana a
melewati semua elemen A
dan b
melewati semua elemen dari B
).
Disjoint penyatuan dua set A
dan B
merupakan set A | B
yang merupakan penyatuan set {(a, 1) : a in A}
dan {(b, 2) : b in B}
. Pada dasarnya itu adalah satu set semua elemen dari keduanya A
dan B
, tetapi dengan masing-masing elemen ini 'ditandai' sebagai milik salah satu A
atau B
, jadi ketika kita mengambil elemen dari A | B
kita akan segera tahu apakah elemen ini berasal A
atau dari B
.
Kami dapat 'bergabung' Nil
dan Cons
fungsi, sehingga mereka akan membentuk satu fungsi yang bekerja pada set 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Memang, jika Nil|Cons
fungsi diterapkan pada ()
nilai (yang, jelas, milik 1 | (Int × IntList)
set), maka berperilaku seolah-olah itu Nil
; jika Nil|Cons
diterapkan pada nilai tipe apa pun (Int, IntList)
(nilai tersebut juga ada di set 1 | (Int × IntList)
, itu berlaku sebagai Cons
.
Sekarang pertimbangkan tipe data lain:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Ini memiliki konstruktor berikut:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
yang juga bisa digabung menjadi satu fungsi:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Dapat dilihat bahwa kedua joined
fungsi ini memiliki tipe yang sama: keduanya terlihat seperti
f :: F T -> T
di mana F
adalah jenis transformasi yang mengambil tipe kita dan memberikan tipe yang lebih kompleks, yang terdiri dari x
dan |
operasi, penggunaan T
dan mungkin tipe lainnya. Misalnya, untuk IntList
dan IntTree
F
terlihat sebagai berikut:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Kita dapat segera melihat bahwa semua jenis aljabar dapat ditulis dengan cara ini. Memang, itulah sebabnya mereka disebut 'aljabar': mereka terdiri dari sejumlah 'jumlah' (serikat pekerja) dan 'produk' (produk silang) dari jenis lain.
Sekarang kita dapat mendefinisikan aljabar F. Aljabar F hanyalah sepasang (T, f)
, di mana T
ada beberapa jenis dan f
merupakan fungsi dari jenis f :: F T -> T
. Dalam contoh-contoh kami F-aljabar adalah (IntList, Nil|Cons)
dan (IntTree, Leaf|Branch)
. Namun, perlu diketahui bahwa meskipun jenis f
fungsinya sama untuk masing-masing F, T
dan f
dirinya sendiri dapat arbitrer. Misalnya, (String, g :: 1 | (Int x String) -> String)
atau (Double, h :: Int | (Double, Double) -> Double)
untuk beberapa g
dan h
juga F-aljabar untuk F. yang sesuai
Setelah itu kita dapat memperkenalkan homomorfisme aljabar F-aljabar dan kemudian memulai aljabar F-aljabar , yang memiliki sifat yang sangat berguna. Sebenarnya, (IntList, Nil|Cons)
adalah aljabar F1 awal, dan (IntTree, Leaf|Branch)
merupakan aljabar F2 awal. Saya tidak akan menyajikan definisi yang tepat dari istilah dan properti ini karena mereka lebih kompleks dan abstrak daripada yang dibutuhkan.
Meskipun demikian, fakta bahwa, katakanlah, (IntList, Nil|Cons)
aljabar-F memungkinkan kita untuk mendefinisikan fold
fungsi seperti pada tipe ini. Seperti yang Anda ketahui, fold adalah semacam operasi yang mengubah beberapa tipe data rekursif dalam satu nilai terbatas. Misalnya, kita bisa melipat daftar bilangan bulat menjadi nilai tunggal yang merupakan jumlah semua elemen dalam daftar:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Dimungkinkan untuk menggeneralisasi operasi semacam itu pada setiap tipe data rekursif.
Berikut ini adalah tanda tangan foldr
fungsi:
foldr :: ((a -> b -> b), b) -> [a] -> b
Perhatikan bahwa saya telah menggunakan kawat gigi untuk memisahkan dua argumen pertama dari yang terakhir. Ini bukan foldr
fungsi nyata , tetapi isomorfis dengannya (yaitu, Anda dapat dengan mudah mendapatkan satu dari yang lain dan sebaliknya). Sebagian diterapkan foldr
akan memiliki tanda tangan berikut:
foldr ((+), 0) :: [Int] -> Int
Kita dapat melihat bahwa ini adalah fungsi yang mengambil daftar bilangan bulat dan mengembalikan bilangan bulat tunggal. Mari kita definisikan fungsi seperti itu dalam hal IntList
tipe kita .
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Kita melihat bahwa fungsi ini terdiri dari dua bagian: bagian pertama mendefinisikan perilaku fungsi ini pada Nil
bagian dari IntList
, dan bagian kedua mendefinisikan perilaku fungsi pada Cons
bagian.
Sekarang anggaplah kita memprogram bukan dalam Haskell tetapi dalam beberapa bahasa yang memungkinkan penggunaan tipe-tipe aljabar secara langsung dalam tipe tanda tangan (yah, secara teknis Haskell memungkinkan penggunaan tipe-tipe aljabar melalui tupel dan Either a b
tipe data, tetapi ini akan mengarah pada verbositas yang tidak perlu). Pertimbangkan fungsi:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Dapat dilihat bahwa reductor
ini adalah fungsi dari tipe F1 Int -> Int
, seperti dalam definisi aljabar F! Memang, pasangan (Int, reductor)
adalah aljabar F1.
Karena IntList
merupakan F1-aljabar awal, untuk setiap jenis T
dan untuk setiap fungsi r :: F1 T -> T
terdapat fungsi, yang disebut catamorphism untuk r
, yang bertobat IntList
untuk T
, dan fungsi tersebut adalah unik. Memang, dalam contoh kita, katamorfisme reductor
adalah sumFold
. Perhatikan bagaimana reductor
dan sumFold
mirip: mereka memiliki struktur yang hampir sama! Dalam reductor
definisi s
parameter, penggunaan (tipe yang sesuai dengan T
) sesuai dengan penggunaan hasil perhitungan sumFold xs
dalam sumFold
definisi.
Hanya untuk membuatnya lebih jelas dan membantu Anda melihat polanya, berikut adalah contoh lain, dan kita kembali mulai dari fungsi lipat yang dihasilkan. Pertimbangkan append
fungsi yang menambahkan argumen pertama ke argumen kedua:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Ini tampilannya pada kita IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Sekali lagi, mari kita coba menuliskan reduktor:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
adalah katamorfisme appendReductor
yang ditransformasikan IntList
menjadi IntList
.
Jadi, pada dasarnya, F-algebras memungkinkan kita untuk mendefinisikan 'lipatan' pada struktur data rekursif, yaitu operasi yang mengurangi struktur kita ke beberapa nilai.
F-coalgebras
F-coalgebras adalah istilah yang disebut 'ganda' untuk F-algebras. Mereka memungkinkan kita untuk menentukan unfolds
tipe data rekursif, yaitu cara untuk membangun struktur rekursif dari beberapa nilai.
Misalkan Anda memiliki tipe berikut:
data IntStream = Cons (Int, IntStream)
Ini adalah aliran bilangan bulat tanpa batas. Satu-satunya konstruktor memiliki jenis berikut:
Cons :: (Int, IntStream) -> IntStream
Atau, dalam hal set
Cons :: Int × IntStream -> IntStream
Haskell memungkinkan Anda untuk mencocokkan pola pada konstruktor data, sehingga Anda dapat menentukan fungsi berikut yang bekerja pada IntStream
s:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Anda secara alami dapat 'menggabungkan' fungsi-fungsi ini menjadi satu fungsi tipe IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Perhatikan bagaimana hasil fungsi bertepatan dengan representasi aljabar dari IntStream
tipe kita . Hal serupa juga dapat dilakukan untuk tipe data rekursif lainnya. Mungkin Anda sudah memperhatikan polanya. Saya mengacu pada keluarga fungsi tipe
g :: T -> F T
dimana T
beberapa tipe. Mulai sekarang kita akan mendefinisikan
F1 T = Int × T
Sekarang, F-coalgebra adalah pasangan (T, g)
, di mana T
adalah tipe dan g
merupakan fungsi dari tipe g :: T -> F T
. Sebagai contoh, (IntStream, head&tail)
adalah F1-coalgebra. Sekali lagi, seperti dalam F-algebras, g
dan T
dapat berubah-ubah, misalnya, (String, h :: String -> Int x String)
juga merupakan F1-coalgebra untuk beberapa jam.
Di antara semua F-coalgebras ada yang disebut terminal F-coalgebras , yang merupakan dwi-aljabar F-coal. Sebagai contoh, IntStream
adalah terminal F-coalgebra. Ini berarti bahwa untuk setiap jenis T
dan untuk setiap fungsi p :: T -> F1 T
terdapat fungsi, yang disebut anamorphism , yang dikonversi T
menjadi IntStream
, dan fungsi tersebut adalah unik.
Pertimbangkan fungsi berikut, yang menghasilkan aliran bilangan bulat berturut-turut mulai dari yang diberikan:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Sekarang mari kita periksa fungsi natsBuilder :: Int -> F1 Int
, yaitu natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Sekali lagi, kita dapat melihat beberapa kesamaan antara nats
dan natsBuilder
. Ini sangat mirip dengan koneksi yang telah kami amati dengan reduktor dan lipatan sebelumnya. nats
adalah anamorphism untuk natsBuilder
.
Contoh lain, fungsi yang mengambil nilai dan fungsi dan mengembalikan aliran aplikasi berturut-turut fungsi ke nilai:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Fungsi pembangunnya adalah sebagai berikut:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Maka iterate
merupakan anamorphism untuk iterateBuilder
.
Kesimpulan
Jadi, singkatnya, F-aljabar memungkinkan untuk menentukan lipatan, yaitu operasi yang mengurangi struktur rekursif menjadi nilai tunggal, dan F-coalgebra memungkinkan untuk melakukan yang sebaliknya: membangun struktur [berpotensi] tak terbatas dari nilai tunggal.
Faktanya dalam Haskell F-algebras dan F-coalgebras bertepatan. Ini adalah properti yang sangat bagus yang merupakan konsekuensi dari keberadaan nilai 'bawah' di setiap jenis. Jadi di Haskell lipatan dan lipatan dapat dibuat untuk setiap jenis rekursif. Namun, model teoritis di balik ini lebih kompleks daripada yang saya sebutkan di atas, jadi saya sengaja menghindarinya.
Semoga ini membantu.