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
ที่แปลงลงในIntList
IntList
ดังนั้นโดยพื้นฐานแล้ว 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 ช่วยให้คุณสามารถจับคู่รูปแบบกับตัวสร้างข้อมูลเพื่อให้คุณสามารถกำหนดฟังก์ชันต่อไปนี้ที่ทำงานกับIntStream
s:
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 สามารถสร้างทั้งการพับและการกางออกได้สำหรับการเกิดซ้ำทุกประเภท อย่างไรก็ตามแบบจำลองทางทฤษฎีที่อยู่เบื้องหลังสิ่งนี้มีความซับซ้อนมากกว่าแบบที่ฉันได้นำเสนอไว้ด้านบนดังนั้นฉันจงใจหลีกเลี่ยงมัน
หวังว่านี่จะช่วยได้