24  Comonads (Sketch)

ในตอนนี้เราได้ครอบคลุมmonadsหมดแล้ว เราสามารถใช้ผลประโยชน์ของdualityและได้comonadsมาแบบฟรีๆอย่างง่านๆโดนการย้อนทางลูกศรและทำงานในcategoryตรงข้าม

จำได้ว่าในระดับที่เรียบง่ายที่สุดmonadsนั้นเกี่ยวกับการประกอบกันลูกศรKleisliอย่าง

a -> m b

ที่mคือfunctorที่คือmonad ถ้าเราใช้ตัวอักษรw(ที่ก็คือmกลับหัว)สำหรับcomonad เราสามารถนิยามลูกศรco-Kleisliในฐานะmorphismของtypeนี้

w a -> b

operator fishในแบบเดียวกันสำหรับลูกศรco-Kleisliนั้นถูกนิยามไว้ว่า

(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)

สำหรับลูกศรco-Kleisliในการก่อให้เกิดcategory เราก็ต้องมีลูกศรco-Kleisliที่เป็นidentityที่ถูกเรียกว่าextract

extract :: w a -> a

มันคือdualของreturn เราก็ได้ทำการบังคับกฏของการสลับกลุ่มและรวมไปถึงกฏidentityด้านช้ายและขวา นำสิ่งเหล่านี้เข้าด้วยกัน เราสามารถนิยามcomonadในHaskellว่า

class Functor w => Comonad w where
    (=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
    extract :: w a -> a

ในการปฏิบัติเราจะใช้primitivesที่แต่ต่างเล็กน้อย ในที่เราจะได้เห็นในอีกไม่ช้า

คำถามคือ อะไรคือการใช้งานของcomonadsในการเขียนโปรแกรม

24.1 เขียนโปรแกรมกับComonads

เรามาเปรียบเทียบmonadกับcomonad monadให้วิธีการในการใล่ค่าไปยังในภาชนะโดยการใช้return มันไม่ได้ให้คุณการเข้าถึงของค่าหรือค่าต่างๆที่ถูกเก็บข้างใน แน่นอนว่าdata structuresที่เขียนmonadsอาจจะให้การเข้าถึงเนื้อหาของพวกมัน แต่นั้นถูกมองว่าเป็นสิ่งเสริมเพิ่มเติม ได้ไม่มีinterfaceเดียวกันสำหรับการดึงค่าจากmonad และเราได้เห็นตัวอย่างของmonadIOที่มีความภูมิใจมันเองที่ไม่เคยที่จะเเปิดเผยเนื้อหาข้างใน

comonadในอีกทางหนึ้งให้วิธีการในการดีงค่าๆหนึ่งจากมัน มันไม่ได้ให้ให้วิธีการในการใส่ค่าเข้าไป ดังนั้นถ้าคุณต้องการที่จะคิดถึงcomonadในฐานะภาชนะ มันนั้นในทุกๆครั้งมาด้วยเนื้อหาขางในที่มีอยู่แล้วและมันอนุญาตให้คุณได้ส่องเข้าไปในมัน

เหมือนกับลูกศรKleisliนำค่าและสร้างผลัพธ์ที่ถูกประดับแล้ว(มันกประดับมันด้วยบริบท)ลูกศรco-Kleisliนำค่าคู่กับบริบททั้งหมดและสร้างผลลัพธ์ออกมา มันคือตัวแทนของการคำนวนที่มีบริบท (contextual computation)

24.2 ComonadsแบบProduct

จำmonad monadได้หรือเปล่า? เราได้นำเสนอมันในการแก้ใขปัญหาของการเขียนการคํานวณที่ต้องการบางenvironmentที่อ่านได้อย่างเดียวe การคำนวณแบบนี้สามารถถูกแสดงในฐานะfunctionแบบpureในรูปของ

(a, e) -> b

เราได้ใช้การcurryในการแปลงมันไปยังลูกศรKleisliอย่าง

a -> (e -> b)

แต่สังเกตว่าfunctionเหล่านี้นั้นมีรูปแบบของลูกศรco-Kleisliอยู่แล้ว เรามาแปลงargumentsของพวกมันไปยังรูปแบบfunctorที่สะดวกกว่านี้

data Product e a = Prod e a deriving Functor

เราสามารถนิยามการoperatorของการประกอบกันย่างง่ายดายโดยการทำให้environmentเดียวกันพร้อมใช้กับลูกศรที่เรากำลังทำการประกอบ

(=>=) :: (Product e a -> b) -> (Product e b -> c) -> (Product e a -> c)
f =>= g = \(Prod e a) -> let b = f (Prod e a)
                          c = g (Prod e b) 
                      in c

การเขียนของextract แค่ทิ้งenvironmentออกไป

extract (Prod e a) = a

ไม่แปลกใจที่comonad productสามารถถูกใช้ในการกระทำการคำนวณแบบเดียวกับเป๊ะๆในฐานะmonad reader ในแบบหนึ่ง การเขียนแบบcomonadicของenvironmentนั้นเป็นธรรมชาติมากว่า (มันตาม จิตวิญญาณของ”การคำนวณในบริบท”) ในอีกทางหนึ่งmonadsมากับารแต่งsyntaxให้ง่ายขึ้น(syntactic sugar)ที่สะดวกของเครื่องหมายdo

ความเชื่อมต่อระหว่างmonad readerและcomonad productนั้นลึกกว่านี้ ที่ต้องเกี่ยวกับความจริงที่ว่าfunctor readerคือadjointด้านขวาของfunctor product แต่โดยทั่วไปแล้วcomonadsครอบคลุมความมายการคำนวณที่แต่ต่างกับmonads เราจะเห็นตัวอย่างมากขึ้นหลังจากนี้

มันง่ายมากที่จะgeneralizecomonadProduct ไปยังtypesแบบproductทั่วๆไปจนรวมไปถึงtuplesและrecords

24.3 แยกชิ้นส่วนการคำนวณ

ต่ามต่อจากขบวนการของการทำให้เป็นduality เราสามารถเริ่มไปและทำให้เป็นdualityของbindและjoinทางmonad ในอีกแบบหนึ่งเราสามารถที่ขบวนการเดียวกันที่เราได้ใช้กับmonads ที่เราศึกษาโครวสร้างของoperator fish แนวทางแบบนี้ดู่น่าจะให้ความรู้ได้มากกว่า

จุดเริ่มคือการรู้ว่าoperatorของการประกอบกันต้องสร้างลูกศรco-Kleisliที่นำw aและสร้างc วิธีทางเดียวในการสร้างcคือการใช้functionที่สองไปกับargumentของtypew bอย่าง

(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
f =>= g = g ... 

แต่เราสามารถในการสร้างtypew bที่อาจจะถูกให้กับg? เราได้มีargumentของtypew aพร้อมอยู่แล้วและfunctionf :: w a -> bคำตอบคือการนิยามdualของbindที่ถูกเรียกว่าextend

extend :: (w a -> b) -> w a -> w b

ในการใช้extendเราสามารถเขียนการประกอบกันว่า

f =>= g = g . extend f

เราสามารถทำการแยกชิ้นส่วนextendหรือเปล่า? คุณอาจจะอยากที่จะพูดว่าทำไมไม่แค่ใช้งานfunctionw a -> bไปกับargumentw aแต่คุณก็จะรู้ในทันที่ว่าคุณไม่มีทางในการแปลงbไปยังw b จำได้ว่าcomonadไม่ให้ทางในการliftค่า ในจุดๆนี้ ในการสร้างแบบคล้ายๆกับของmonads เราได้ใช้fmap วิธีเดียวเราอาจจะใช้fmapก็ถ้าเรามีบางอย่างของtypew (w a)ที่พร้อมใช้ ถ้าเราสามารถเปลี่ยนw aไปยังw (w a) และเพื่อความสะดวกที่ก็อาจจะเป็นdualของjoinเป๊ะๆ เราเรียกมันว่าduplicate

duplicate :: w a -> w (w a)

ดังนั้นแค่เหมือนกับนิยามของmonadเราได้มีสามนิยามที่เหมือนกันสำหรับcomonadอย่าง การใช้ลูกศรco-Kleisli extendหรือduplicateในที่นี้คือนิยามของHaskellที่นำมาโดยตรงจากlibraryControl.Comonad

class Functor w => Comonad w where
    extract :: w a -> a
    duplicate :: w a -> w (w a)
    duplicate = extend id
    extend :: (w a -> b) -> w a -> w b
    extend f = fmap f . duplicate

ในการที่มีการเขียนแบบมาตราฐานของextendในรูปแบบของduplicateและในทางกลับกัน ดังนั้นคุณแค่ต้องoverrideตัวไดตัวหนึ่ง

แนวคิดข้างหลังfunctionsเหล่านี้มีมาจากแนวคิดที่ว่าโดนทั่วไปแล้วcomonadสามารถถูกคิดในฐานะภาชนะที่มีวัตถุต่างของtypea(comonadแบบproductนั้นคือสิ่งที่เป็นพิเศษของแค่ค่าๆหนึ่ง) แนวคิดของค่า”ตอนๆนี้” สิ่งที่สามารถเข้าถึงได้ง่ายผ่านextractลูกศรco-Kleisliได้แสดงการคำนวณบางอย่างที่เพ่งเล็งไปที่ค่าในปัจจุบันแต่มันก็มีการเข้าถึงสำหรับทุกๆค่าที่อยู่รอบๆทั้งหมด ให้คิดถึงเกมของชีวิต(game of life)ของConway ในแต่ละช่อง(cell)ได้มีค่า(ที่โดยทั่วไปแล้วเป็นแค่TrueหรือFalse) comonadที่ตรงกับเกมของชีวิตก็จะเป็นตารางของช่อง”ในตอนๆนี้”

ดังนั้นแล้วduplicateทำอะไร? มันนำภาชนะแบบcomonadicw aและสร้างภาชนะของภาชนะw (w a) แนวคิดคือว่าแต่ละภาชนะนั้นเพ่งเล็งไปยังaในw aที่แตกต่างกัน ในเกมของชีวิตคุณอาจจะมีตารางของตาราง ที่แต่ละช่องของตารางข้างนอกนั้นเก็บตารางข้างในเอาไว้ที่ทำการเพ่งเล็งบนช่องที่แตกต่างออกไป

ในตอนนี้เรามาดูที่extend มันนำลูกศรco-Kleisliและภาชนะแบบcomonadicw aที่เต็มไปด้วยaต่างๆ มันนำการคำนวณมาใช้สำหรับทุกๆaต่างๆเหล่านี้แทนที่มันด้วยbต่างๆ ผลลัพธ์คือภาชนะแบบcomonadicที่เต็มไปด้วยbต่างๆ extendนำมันโดยการ เคลื่อนสอ่งที่เพ่งเล็งจากaตัวหนึ่งไปยังอีกตัวและทำการใช้ลูกศรco-Kleisliไปยังพวมมันแต่ละตัว ในเกมของชีวิตลูกศรco-Kleisliก็จะคำนวณแต่ละสถานะใหม่ของช่องในตอนๆนี้ ในการทำแบบนั้น มันอาจจะดูไปในบริบท(ที่ก็เดาว่าเป็นช่องรอบๆที่ใกล้ที่สุด) การเขียนแบบมาตราฐานของextendแสดงให้เห็นถึงขบวนการนี้ ในตอนแรกเราเรียกduplicateในการสร้างจุดที่ต้องเพ่งเล็งทั้งหมดแล้วก็ทำการใช้fไปยังพวมมันแต่ละอัน

24.4 ComonadแบบStream

ขบวนการนี้ของการเลื่อนจุดที่สนใจจากสมาชิกหนึ่งไปยังภาชนะของอีกอันหนึ่งนั้นแลดงให้เห็นอย่างดีที่สุดโดยกับตัวอย่างของstreamไม่มีที่สิ้นสุด streamอย่างนี้คือเหมือนกับlistแต่แค่มันไม่ต้องมีconstructorว่างอย่าง

data Stream a = Cons a (Stream a)

มันคือFunctorอย่างชัดๆ

instance Functor Stream where
    fmap f (Cons a as) = Cons (f a) (fmap f as)

จุดสนใจของstreamคือสมาชิกแรกของมันดังนั้นนี้คือการเขียนของextract

extract (Cons a _) = a

duplicateสร้างstreamของstream ที่แต่ละตัวเพ่งเล็งไปที่สมาชิกที่แตกต่างกัน

duplicate (Cons a as) = Cons (Cons a as) (duplicate as)

สมาชิกแรกคือstreamดังเดิม สมาชิกที่สองคือหางของstreamดังเดิม มาชิกที่สองคือหางของมันเองและไปเรื่อยๆไม่มีที่สิ้นสุด

ที่คือinstanceตัวเต็ม

instance Comonad Stream where
    extract (Cons a _) = a 
    duplicate (Cons a as) = Cons (Cons a as) (duplicate as)

มันคือวิธีการที่functionalอย่างมากในการมองไปยังstreams ในภาษาแบบimperative เราอาจจะเรื่อมด้วยmethodadvanceที่เลื่อนstreamโดยหนึ่งตำแหน่ง ในที่นี้duplicateสร้างstreamที่ถูกเลื่อนใรการทำแค่ครังเดียว ความlazyของHaskellทำให้สิ่งนี้เป็นไปได้และแม้กระทั่งเป็นที่ต้องการ แน่นอน ในการทำให้Streamนั้นใช้งานได้จริง เราอาจจะก็เขียนรูปแบบที่คล้ายกันของadvanceว่า

tail :: Stream a -> Stream a
tail (Cons a as) = as

แต่มันไม่เคยเป็นส่วนหนึ่งของinterfaceของcomonad

ถ้าคุณมีประสบการกับการการประมวลผลสัญญาณดิจิทัล(digital signal processing)คุณจะเห็นในทันที่ว่าลูกศรco-Kleisสำหรับstreamนั้นคือแค่filterแบบdigitalและextendนั้นสร้างstreamที่ผ่านการfilterแล้ว

ในฐานะตัวอย่างที่ง่ายๆ เรามาเขียนfilterแบบmoving average ในที่นี้คือfunctionที่บวกสมาชิกnตัวของstreamไว้

sumS :: Num a => Int -> Stream a -> a
sumS n (Cons a as) = if n <= 0 then 0 else a + sumS (n - 1) as

นี้คือfunctionในการคำนวณค่าเฉลี่ยของสมาชิกnตัวแรกเอาไว้ในstream

average :: Fractional a => Int -> Stream a -> a
average n stm = (sumS n stm) / (fromIntegral n)

ในการใช้งานบาส่วนแล้วของaverage nคือลูกศรco-Kleisดังนั้น เราสามารถextendมันไปยังทั้งstreamได้เลย

movingAvg :: Fractional a => Int -> Stream a -> Stream a
movingAvg n = extend (average n)

ผลลัพธ์คือของstreamของrunning averages

streamนั้นคือตัวอย่างของcomonadหนิ่งมิติทางเดียว มันสามารถทำได้เป็นสองทิศทางหรือขยายไปยังสองหรือมากกว่ามิตืได้ง่ายกว่า

24.5 ComonadแบบCategory

ในการนิยามcomonadในทฤษฎีcategoryนั้นเป็นการฝึกหัดที่ตรงไปตรงมาของduality เหมือนกับmonad เราเรืิ่มกับendofunctor\(T\) การแปลงแบบธรรมชาติสองตัวอย่าง\(\eta\)และ\(\mu\)ที่นิยามmonadนั้นแค่กลับทางสำหรับcomonadอย่าง

\[ \begin{align*} \varepsilon & :: T \to I \\ \delta & :: T \to T^2 \end{align*} \]

ส่นประกอบของการแปลงเหล่านี้ตรงกับextractและduplicate กฏของComonadนั้นคือถาพสะท้อนของกฎของmonad ไม่มีsurpriseในที่นี้

แล้วก็ได้มีการได้มาของmonadจากadjunction dualityนั้นย้อนadjunctionคือadjointด้านช้ายกลายมาเป็นadjointด้านขวาและในทางกลับกัน และเนื่องด้วยการประกอบกัน\(R\circ L\)นิยามmonad\(L\circ R\)ต้องนิยามcomonad counitของadjunctionอย่าง

\[ \varepsilon :: L \circ R \to I \]

นั้นก็คือ\(\varepsilon\)เดียวกันที่เราเห็นในนิยามของcomonad(หรือในส่วนแระกอบ ในฐานะextractของHaskell) เราก็สามารถใช้unitของadjunctionอย่าง

\[ \eta :: I \to R \circ L \]

ในการใส่\(R\circ L\)ในตรงการของ\(L\circ R\)และทำการสร้าง\(L \circ R \circ L \circ R\) การสร้าง\(T^2\)จาก\(T\)ได้นิยาม\(\delta\)และสิ่งนี้ได้ทำให้นิยามของcomonadสมบูรณ์

เราก็ได้เห็นว่าmonadนั้นคือmonoid dualของประโยคนี้ก็อาจจะต้องการใช้งานของcomonoid แล้วอะไรคือcomonoid? นิยามดั้งเดิมของmonoidในฐานะcategoryที่มีสมาชิกเดียวไม่ได้มีdualที่น่าสนใจอะไร ในตอนที่คุณย้อนทิศทางของendomorphismsทั้งหมด คุณได้มีอีกmonoidหนึ่ง แต่จำได้ว่าในแนวทางของเรากับmonad เราใช้นิยามที่กว้างกว่าของmonoidในฐานะวัตถุในcategoryแบบmonoidal การสร้างนั้นมาจากสองmorphismsอย่าง

\[ \begin{align*} \mu & :: m \otimes m \to m \\ \eta & :: i \to m \end{align*} \]

การย้อนของMorphismเหล่านี้สร้างcomonoidในcategoryแบบmonoid

\[ \begin{align*} \delta & :: m \to m \otimes m \\ \varepsilon & :: m \to i \end{align*} \]

เราสามารถเขียนนิยามของcomonoidในHaskell

class Comonoid m where
    split :: m -> (m, m)
    destroy :: m -> ()

แต่มันก็จะตรงไปตรงมา แน่นอนว่าdestroyไม่สนใจargumentของมัน

destroy _ = ()

splitคือแค่คู่ของfunctions

split x = (f x, g x)

ในตอนนี้พิจารณากฏของcomonoidที่คือdualของกฏunitของmonoid

lambda . bimap destroy id . split = id
rho . bimap id destroy . split = id

ในที่นี้lambdaและrhoคือunitorsด้านช้ายและขวาตามลำดับ(ลองดูนิยามของcategoryแบบmonoid) นำมันเข้ามาในนิยามเราก็ได้

lambda (bimap destroy id (split x))
= lambda (bimap destroy id (f x, g x))
= lambda ((), g x)
= g x

ที่จะพิสูจน์ว่าg = id ในทางเดียวกันกฏที่สองนั้นขยายไปยังf = id โดยสรุปแล้ว

split x = (x, x)

ที่แสดงว่าในHaskell(และโดยทั่วไปในcategory\(\textbf{Set}\))ทุกๆวัตถุนั้นคือcomonoidที่ตรงไปตรงมา

โชคดีที่ว่าได้มีcategoryแบบmonoidอื่นๆที่น่าสนใจมากกว่าที่ทำการนิยามcomonoid หนึ่งในนั้นคือcategoryของendofunctors และมันกลับมาเป็นที่ว่า เหมือนกับmonadคือmonoidในcategoryของendofunctors

comonadคือcomonoidในcategoryของendofunctors

24.6 Comonadกักเก็บ

อีกตัวอย่างหนึ่งที่สำคัญของcomonadคือdualของmonadสถานะ มันถูกเรียกว่าcomonad costateหรือcomonadกักเก็บ

เราได้เห็นแล้วว่าmonadสถานะนั้นถูกสร้างโดยadjunctionที่นิยามexponentialsว่า

\[ \begin{align*} L\ z & = z\times{}s \\ R\ a & = s \Rightarrow a \end{align*} \]

เราจะใช้adjunctionเดียวกันในการนิยามcomonadของcostate comonadนั้นถูกนิยามโดยการประกอบกันอย่าง\(L\circ R\)

\[ L\ (R\ a) = (s \Rightarrow a)\times{} \]

แปลงสิ่งนี้ไปยังHaskell เราเริ่มด้วยadjunctionระหว่างfunctorProductในทางด้านช้ายและfunctorReaderในทางด้านขวา การประกอบกันของProductหลังReaderนั้นตรงกันกับนิยามดังต่อไปนี้

data Store s a = Store (s -> a) s

counitของadjunctionที่นำมาที่วัตถุ\(a\)คือmorphismอย่าง

\[ \varepsilon_a :: ((s \Rightarrow a)\times{}s) \to a \]

หรือในสัญลักษณ์ของHaskell

counit (Prod (Reader f) s)) = f s

มันกลายมาเป็นextractของเรา

extract (Store f s) = f s

unitของadjunctionคือ

unit :: a -> Reader s (Product a s)
unit a = Reader (\s -> Prod a s)

สามารถถูกเขียนใหม่ในฐานะconstructorข้อมูลที่ถูกใช้บางส่วนว่า

Store f :: s -> Store f s

เราสร้าง\(\delta\)หรือduplicateในฐานะการประกอบกันแนวนอนว่า

\[ \begin{align*} \delta & :: L \circ R \to L \circ R \circ L \circ R \\ \delta & = L \circ \eta \circ R \end{align*} \]

เราได้แอบ\(\eta\)ผ่าน\(L\)ช้ายสุดที่คือfunctorProduct มันหมายความถึงการกระทำกับ\(\eta\)หรือStore f ในส่วนประกอบด้านช้ายของคู่(นั้นคือสิ่งที่fmapสำหรับProductจะทำ) เรามี

duplicate (Store f s) = Store (Store f) s

(จำไว้ว่าในสูตรสำหรับ\(\delta,L\)และ\(R\)เป็นตัวแทนสำหรับการ แปลงแบบธรรมชาติแบบidentityที่ส่วนประกอบนั้นคือmorphisms identity)

ในที่นี้นิยามโดยสมบูรณ์ของcomonadStoreคือ

instance Comonad (Store s) where
    extract (Store f s) = f s
    duplicate (Store f s) = Store (Store f) s

คุณอาจจะคิดถึงส่วนของReaderในStoreในฐานะภาชนะที่ทั่วไปของaต่างๆที่คู่กับกุญแจของสมาชิกในtypes ตัวอย่างเช่นถ้าsคือInt แล้วReader Int aคือstreamสองทางที่ไม่มีที่สิ้นสุดของa Storeจับคู่ภาชนะนี้กับค่าของtypeของkey ตัวอย่างเช่นReader Int aนั้นถูกจับคู่กับInt ในกรณีนี้extractได้ใช้จำนวณเต็มนี้ที่เป็นตัวชี้ไปยังstreamที่ไม่มีที่สิ้นสุด คุณอาจจะคิดถึงส่วนประกอบที่สองของStoreในฐานะตำแหน่งปัจจุบัน

ตามมาต่อจากตัวอย่างนี้duplicateสร้างstreamที่ไม่มีที่สิ้นสุดใหม่ที่มีตัวชี้เป็นInt streamนี้เก็บstreamsในฐานะสมาชิกของมัน โดยเฉพาะเช่นที่ตำแหน่งในตอนนี้ มันเก็บstreamดั้งเดิมเอาไว้ แต่ถ้าคุณใช้บางIntอีกตัว(ทั้งบวกและลบ)ในฐานะกุญแจ คุณก็จะได้streamที่ถูกเลื่อนที่ในตัวชี้ใหม่

โดนทั่วไปคุณสามารถทำให้ตัวเองมั่นใจว่าในตอนที่extractกระทำบนStoreที่ถูกduplicate มันได้สร้างStoreดั้งเดิม(ในความเป็นจริงแล้ว กฏทางidentityสำหรับcomonadบอกว่าextract . duplicate = id)

comonadStoreมีบทบาทที่สำคัญในฐานะพื้นฐานทางทฤษฎีสำหรับlibraryLens ในแนวคิดแล้วcomonadอย่างStore s aนั้นรวมแนวคิดที่ของ”การเพ่งเล็ง” (เหมือนlens) ไปยังบางโครงสร้างย่อยของtypea โดยการใช้typesในฐานะตัวชี้ โดยเฉพาะเช่นfunctionของtype

a -> Store s a

นั้นเท่ากับคู่ของfunctionsต่างๆว่า

set :: a -> s -> a
get :: a -> s

ถ้าaคือtypeของproductsetก็อาจจะถูกเขียนในฐานะการจัดวางfieldของtypesข้างในaในขณะเดียวกันก็ทำการreturning รูปแบบของaที่ถูกดัดแปลง ในทางเดียวกันgetก็อาจจะถูกเขียนให้อ่านค่าของfieldsจากa เราจะมาสำรวจแนวคิดเหล่านี้ในบทถัดไป

24.7 โจทย์ท้าทาย

  1. ลองเขียนเกมของชีวิตของConwayโดยการใช้comonadStore คำใบ้: อะไรคือtypeคุณต้องเลือกสำหรับs?