F-algebras และ F-coalgebras เป็นโครงสร้างทางคณิตศาสตร์ซึ่งเป็นเครื่องมือในการให้เหตุผลเกี่ยวกับประเภทอุปนัย (หรือประเภทแบบเรียกซ้ำ )
F-จีบ
เราจะเริ่มด้วย F-algebras ก่อน ฉันจะพยายามให้ง่ายที่สุด
ฉันเดาว่าคุณรู้ว่าประเภท recursive คืออะไร ตัวอย่างเช่นนี่เป็นประเภทสำหรับรายการจำนวนเต็ม:
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-algebra ได้ 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-algebras สำหรับ F ที่เกี่ยวข้อง
หลังจากนั้นเราสามารถแนะนำโฮโมมอร์ฟิซึมของ F-algebraแล้วเริ่มต้น F-algebrasซึ่งมีคุณสมบัติที่มีประโยชน์มาก ในความเป็นจริง(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ฟังก์ชั่นที่แท้จริงแต่มันเป็น isomorphic กับมัน (นั่นคือคุณสามารถได้รับอย่างใดอย่างหนึ่งจากที่อื่นและในทางกลับกัน) นำไปใช้บางส่วนfoldrจะมีลายเซ็นต่อไปนี้:
foldr ((+), 0) :: [Int] -> Int
เราจะเห็นว่านี่เป็นฟังก์ชั่นที่รับรายการจำนวนเต็มและส่งกลับจำนวนเต็มเดียว ให้มีกำหนดฟังก์ชั่นดังกล่าวในแง่ของเราIntListประเภท
sumFold :: IntList -> Int
sumFold Nil         = 0
sumFold (Cons x xs) = x + sumFold xs
เราจะเห็นว่าฟังก์ชั่นนี้ประกอบด้วยสองส่วน: ส่วนแรกกำหนดพฤติกรรมของฟังก์ชั่นนี้ในNilส่วนของIntListและส่วนที่สองกำหนดพฤติกรรมของฟังก์ชั่นในConsส่วน
ทีนี้สมมติว่าเรากำลังเขียนโปรแกรมไม่ได้อยู่ใน Haskell แต่ในบางภาษาที่อนุญาตให้ใช้พีชคณิตประเภทโดยตรงในลายเซ็นประเภท (ดีเทคนิค Haskell อนุญาตให้ใช้ประเภทพีชคณิตผ่าน tuples และEither a bประเภทข้อมูล แต่สิ่งนี้จะนำไปสู่ verbosity ที่ไม่จำเป็น) พิจารณาฟังก์ชั่น:
reductor :: () | (Int × Int) -> Int
reductor ()     = 0
reductor (x, s) = x + s
จะเห็นได้ว่าreductorเป็นฟังก์ชั่นของประเภทF1 Int -> Intเช่นเดียวกับในคำจำกัดความของ F-algebra! อันที่จริงทั้งคู่(Int, reductor)เป็น F1- พีชคณิต
เพราะIntListเป็น F1- พีชคณิตเริ่มต้นสำหรับแต่ละประเภทTและสำหรับแต่ละฟังก์ชั่นr :: F1 T -> Tมีฟังก์ชั่นที่เรียกว่าcatamorphismสำหรับrซึ่งแปลงIntListเป็นTและฟังก์ชั่นดังกล่าวเป็นเอกลักษณ์ อันที่จริงในตัวอย่างของเราสำหรับ catamorphism มีreductor 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
อีกครั้งลองเขียน reductor:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys ()      = ys
appendReductor ys (x, rs) = x : rs
appendFoldเป็น catamorphism สำหรับappendReductorที่แปลงลงในIntListIntList
ดังนั้นโดยพื้นฐานแล้ว F-algebras ทำให้เราสามารถกำหนด 'folds' ในโครงสร้างข้อมูลแบบเรียกซ้ำได้นั่นคือการดำเนินการที่ลดโครงสร้างของเราให้มีค่าบางอย่าง
F-coalgebras
F-coalgebras เรียกว่าคำว่า 'dual' สำหรับ F-algebras มันช่วยให้เราสามารถกำหนดunfoldsประเภทข้อมูลแบบเรียกซ้ำได้นั่นคือวิธีการสร้างโครงสร้างแบบเรียกซ้ำจากค่าบางค่า
สมมติว่าคุณมีประเภทต่อไปนี้:
data IntStream = Cons (Int, IntStream)
นี่คือกระแสจำนวนเต็มของอนันต์ มันสร้างเพียงอย่างเดียวมีประเภทต่อไปนี้:
Cons :: (Int, IntStream) -> IntStream
หรือในแง่ของชุด
Cons :: Int × IntStream -> IntStream
Haskell ช่วยให้คุณสามารถจับคู่รูปแบบกับตัวสร้างข้อมูลเพื่อให้คุณสามารถกำหนดฟังก์ชันต่อไปนี้ที่ทำงานกับIntStreams:
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-coalgebraคือคู่(T, g)ที่Tเป็นชนิดและเป็นหน้าที่ของประเภทg g :: T -> F Tตัวอย่างเช่น(IntStream, head&tail)F1-coalgebra อีกครั้งเช่นเดียวกับใน F-algebras gและTสามารถเป็นกฎเกณฑ์ได้เช่นกัน(String, h :: String -> Int x String)ก็เป็น F1-coalgebra สำหรับบางชั่วโมง
ในบรรดา F-coalgebras มีสิ่งที่เรียกว่าอาคาร F-coalgebrasซึ่งเป็นคู่กับ F-algebras เริ่มต้น ตัวอย่างเช่นIntStreamเป็นเทอร์มินัล F-coalgebra ซึ่งหมายความว่าสำหรับทุกประเภทTและทุกฟังก์ชั่นp :: T -> F1 Tที่มีอยู่ฟังก์ชั่นที่เรียกว่าanamorphismซึ่งจะแปลง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มันคล้ายกับการเชื่อมต่อที่เราสังเกตเห็นด้วย reductors และเท่าก่อนหน้านี้ natsเป็น anamorphism 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เป็น anamorphism iterateBuilderสำหรับ
ข้อสรุป
ดังนั้นในระยะสั้น F-algebras อนุญาตให้กำหนด folds นั่นคือการดำเนินการที่ลดโครงสร้างแบบเรียกซ้ำลงในค่าเดียวและ F-coalgebras อนุญาตให้ทำตรงกันข้าม: สร้างโครงสร้างอนันต์ที่อาจเกิดขึ้นจากค่าเดียว
ในความเป็นจริงใน Haskell F-algebras และ F-coalgebras เหมือนกัน นี่คือคุณสมบัติที่ดีมากซึ่งเป็นผลมาจากการปรากฏตัวของ 'ด้านล่าง' ค่าในแต่ละประเภท ดังนั้นใน Haskell สามารถสร้างทั้งการพับและการกางออกได้สำหรับการเกิดซ้ำทุกประเภท อย่างไรก็ตามแบบจำลองทางทฤษฎีที่อยู่เบื้องหลังสิ่งนี้มีความซับซ้อนมากกว่าแบบที่ฉันได้นำเสนอไว้ด้านบนดังนั้นฉันจงใจหลีกเลี่ยงมัน
หวังว่านี่จะช่วยได้