22 Monadและผลตามมา(Effect) (Sketch)
ในตอนนี้เรารู้ว่าmonadมีสำหรับอะไร (มันอนุญาติในการประกอบfunctionที่ตกแต่งแล้ว) คำถามที่น่าสนใจเป็นอย่างมากคือทำไมfunctionที่ตกแต่งแล้วนั้นสำคัญอย่างมากในการเขียนโปรแกรมแบบfunctional เราได้เห็นตัวอย่างแล้วนั้นก็คือmonadWriter
ที่การตกแต่งให้เราได้สร้างและรวบรวมlogของการเรียกของหลากหลายfunctionทั้งหมด ปัญหาที่อาจจะถูกแก้ในทางกลับกันโดยการใช้functionที่ไม่pure (ตัวอย่างเช่นการเข้าถึงและแก้ไขสถานะที่เป็นสากล)นั้นถูกแก้ด้วยfunctionแบบpure
22.1 ปัญหา
ในที่นี้คือรายกาอย่างสั้นของปัญหาที่คล้ายกัน ที่นำมาจากpaperของEugenio Moggiที่สำคัญ1 ทั้งหมดนี้สามารถแก้ไขได้โดยการบะทิ้งความpureของfunction
- ความไม่สมบูรณ์(Partiality): การคำนวนที่อาจจะไม่มีวันจบ
- ความกำหนดไม่ได้(Nondeterminism): การคำนวนที่อาจจะreturnหลายๆค่า
- ผลข้างเคียง(Side effects): การคำนวนที่เข้าถึงและแก้ไขสถานะที่เป็นสากล
- สถานะที่แค่อ่านได้(Read-only)หรือสภาวะแวดล้อม(environment)
- สถานะที่แค่เขียนได้(Write-only)หรือlog
- สถานะที่ทั้งอ่านและเขียนได้(Read/write state)
- Exception: functionบางส่วนที่อาจจะล้มเหลว
- ความต่อเนื่อง(Continuations): ความสามารถในการsaveสถานะของโปรแกรมและนำกลับมาตามต้องการ
- Inputแบบมีปฏิสัมพันธ์(Interactive Input)
- Outputแบบมีปฏิสัมพันธ์(Interactive Output)
สิ่งที่น่าทึ่งคือว่าปัญหาเหล่านี้แาจจะถูกแก้โดยการใช้เคล็ดลับที่ฉลาดแบบเดียวกันคือการแปลงให้เป็นfunctionที่ตกแต่งแล้ว แน่นอนว่าการตกแต่งจะแตกต่าอย่างสิ้นเชิงในแต่ละกรณี
เราได้สำนึกได้ว่าในจุดๆนี้ ไม่มีความต้องการที่ว่าการตกแต่งนั้นเป็นแบบmonad มันเป็นแค่ในตอนที่เรายืนยันในการประกอบกัน (ความสามารถในการแยกประกอบfunctionที่ตกแต่งแล้วไปยังfunctionที่ตกแต่งแล้ว)ที่เราจำเป็นต้งใช้monad อีกครั้งเนื่องด้วยแต่ละการตกแต่งนั้นแตกต่างกัน การประกอบกันแบบmonadก็จะถูกเขียนออกมาแตกต่างกันด้วนแต่รูปแบบโดยรวมนั้นเหมือนกัน มันเป็นรูปแบบที่เรียบง่ายมากๆคือการประกอบกันนั้นมีคุณสมบัติการเปลี่ยนหมู่และมาคู่กับidentity
ในส่วนถัดไปนั้นจะหนักไปทางตัวอย่างของHaskell คุณสามารถอ่านแบบคร่าวๆหรือแค่ข้ามมันถ้าคุณอยากจะกลับไปยังทฤษฎีcategoryหรือถ้าคุณคุ้นเคยกับการเรียนmonadของHaskellออกมา
22.2 คำตอบ
เริ่มด้วย เรามาวิเคราะห์วิธีที่เราใช้monadWriter
เราเริ่มกับfunctionแบบpureที่ดำเนินการในบางหน้าที่ (ถ้ามีargument) มันได้สร้างผลลัพธ์บางอย่างออกมา เราแทนที่functionด้วยอีกfunctionหนึ่งที่ตกแต่งoutputเริ่มแรกแล้วโดยการคู่มันกับstring ที่ก็คือคำตอบของเรากับปัญหาของการlog
เราไม่สามารถหยุดในที่นี้ได้เพราะว่า โดยทั่วไปแล้ว เราไม่ต้องการที่จะทำงานกับคำตอบที่เป็นหนึ่งเดียว เราต้องการที่จะแยกประกอบfunctionที่สร้างlogหนึ่งไปยังfunctionที่สร้างlogที่เล็กกว่า มันคือการประกอบกันของfunctionที่สร้างlogขนาดเล็กเหล่านี้ที่นำเราไปยังแนวคิดของmonad
สิ่งที่หน้าทึ่งจริงๆคือการที่ว่ารูปแบบเดียวกันของการตกแต่งtypeที่retun function(function return types)ใช้ได้สำหรับปัญหาหลายหลายที่มักจะต้องละทิ้งความpure เรามาดูในแต่ละข้อของรายการของเราและเลือกการตกแต่งที่ใช้ได้ในแต่ละปัญหา
22.2.1 ความไม่สมบูรณ์
เราทำการดัดแปลงtypeที่returnสำหรับทุกๆfunctionที่อาจจะไม่จบโดยการเปลี่ยนมันไปยังtype”ที่ถูกlift” (typeที่เก็บทุกๆค่าไว้ของtypeเริ่มต้นบวกกับค่า”bottom”\(\vdash\)ที่ป็นพิเศษ) ตัวอย่างเช่นในtypeBool
ในฐานะset อาจจะเก็บสองสมาชิกอย่างTrue
และFalse
Bool
ที่ถูกliftเก็บสามสมาชิกไว้ functionที่returnBool
ที่ถูกliftอาจจะผลิตTrue
หรือFalse
หรือดำเนินการแบบไม่มีที่สิ้นสุด
สิ่งที่ตลกคือว่าในภาษาที่lazyอย่างHaskell functionที่ไม่มีวันจบอาจจะreturnค่าออกมาและค่านี้อาจจะถูกส่งไปยังfunctionถัดไป เราเรียกค่าพิเศษนี้ว่าbottom ตราบเท่าที่ค่านี้นั้นไม่ถูกต้องการอย่างเปิดเผย(ตัวอย่างเช่น ในการจับคู่รูปแบบหรือผลิตในฐานะoutput) มันอาจจะถูกส่งไปรอบๆโดยที่ไม่ขัดการดำเนินการของprogram เพราะว่าในทุกๆfunctionของHaskellอาจจะเป็นไปได้ที่จะไม่มีวันหยุด typeทั้งหมดของHaskellนั้นคาดให้โดนlift นี่คือเหตุผลที่เรามักจะพูดเกี่ยวกับcategory\(\textbf{Hask}\)ขอtype(ที่ถูกlift)ของHaskellและfunctioแทนที่จะเป็น\(\textbf{Set}\)ที่ง่ายกว่า แต่มันไม่ชัดเจนว่า\(\textbf{Hask}\)เป็นcategorจริงๆหรือเปล่า (ลองดูในpostของAndrej Bauer2)
22.2.2 ความกำหนดไม่ได้
ถ้าfunctionสามารถreturnค่าต่างๆกัน มันก็อาจจะreturnพวกมันทั้งหมดในขณะเดียวกัน ฝนทางความหมาย functionที่กำหนดค่าไม่ได้นั้นเท่ากันกับfunctionที่return listของผลลัพธ์ สิ่งนี้สมเหตุสมผลอย่างมากในภาษาที่lazyและgarbage-collected ตัวอย่างเช่น ถ้าสิ่งที่คุณต้องการทั้งหมดคือค่าๆเดียว คุณแค่เอาส่วนหัวของlistและส่วนหางจะไม่จะไม่ถูกนำมาใช้ ถ้าคุณต้องการสุ่มค่า ใช้ตัวสร้างเลขสุ่มในการเลือกสมาชิกตัวที่\(n\)ของlist ความlazy แม้กระทั่งอนุญาติให้คุณในการreturn listของผลลัพธ์ที่ไม่สิ้นสุด
ในmonadแบบlist (การเขียนในHaskellของการคำนวณแบบกำหนดไม่ได้)join
ถูกเขียนในฐานะconcat
จำไว้ว่าjoin
นั้นควรที่จะทำให้ภาชนะของภาชนะแบน(concat
เชื่อมlistของlistไปยังlistตัวเดียว) return
สร้างlistที่มีสมาชิกเดียว
instance Monad [] where
= concat
join return x = [x]
Operator Bindสำหรับmonadแบบlistนั้นให้มาโดยสูตรทั่วไปอย่างfmap
ตามมาด้วยjoin
ที่ในกรณีนี้ให้เรา
>>= k = concat (fmap k as) as
ในที่นี้functionk
ที่ตัวมันเองสร้างlist นั้นถูกนำไปใช้กับทุกๆสมาชิกของlistas
ผลคือlistของlist ที่ถูกทำให้แบนโดยการใช้concat
ในมุมมองของprogrammer การทำงานกับlistนั้นง่ายกว่า ตัวอย่างเช่น การเรียกfunctionที่กำหนดไม่ได้ในloopหรือเขียนfunctionที่retunr iteratorมา(ถึงแม้ในC++สมัยใหม่3 การreturn rangeที่lazyนั้นแทบจะเหมือนกับการreturn listในHaskell)
ตัวอย่างที่ดีในการใช้ความกำหนดไม่ได้อย่างสร้างสรรค์ีือในการเขียนโปรแกรมของเกม ตัวอย่างเช่นในตอนที่คอมพิวเตอร์เล่นหมากรุกกับมนุษย์ มันไม่สามารถที่จะคาดเดาการเดินครั้งต่อไปของมนุษย์ แต่มันสามารถสร้างlistของความเป็นไปได้ของการเดินทั้งหมดและวิเคราะห์พวกมันแต่ละตัว ในแบบคล้ายกัน parserที่กำหนดไม่ได้อาจจะสร้างlistของการparseที่เป็นไปได้ทั้งหมดสำหรับเครื่องหมายที่ให้มา
ถึงแม้เราอาจจะตีความหfunctionที่return listในฐานะสิ่งที่กำหนดไม่ได้ การใช้งานของmonadแบบlistนั้นกว้างกว่า นั้นก็เพราะว่าการเชื่อมกันระหว่างการคำนวณที่สร้างlistต่างๆนั้น เป็นตัวแทนที่แบบfunctionalที่สมบูรณ์สำหรับการสร้างแบบiterative(อย่างloop)ที่ถูกใช้ในการเขียนโปรแกรมแบบimperative loopเดี่ยวมักจะสามารถถูกเขียนใหม่โดยการใช้fmap
ที่นำมาใช้เส่าตัวของloopในแต่ละสมาชิกของlist เครื่องหมายdo
ในmonadแบบlistสามารถถูกใช้ในการแทนที่loopในloopที่ชับช้อนได้
ตัวอย่างที่ผมชอบคือโปรแกรมที่สร้างtripleของPythagoreanที่คือtripleของจำนวนเต็มที่สามารถมาจากด้านของสามเหลี่ยมมุมฉาก
= do
triples <- [1..]
z <- [1..z]
x <- [x..z]
y ^2 + y^2 == z^2)
guard (xreturn (x, y, z)
ในบรรทัดแรกบอกเราว่าz
ได้นำสมาชิกจากlistที่จำนวนบวกไม่สิ้นสุด[1..]
แล้วx
นำสมาชิกจากlist(ที่สิ้นสุด)[1..z]
ของตัวเลขระหว่าง1
ไปยังz
สุดท้ายแล้วy
นำสมาชิกจากlistของตัวเลขระหว่างx
ไปยังz
เรามีสามตัวเลข\(1\le x\le y\le z\) ให้เราได้ใช้ functionguard
นำเครื่องหมายBool
และreturn listของunit
guard :: Bool -> [()]
True = [()]
guard False = [] guard
functionนี้(ที่คือสมาชิกของclassที่ใหญ่กว่าMonadPlus
)นั้นถูกใช้ในที่นี้ในการกรองtripleที่ไม่เป็นPythagorean แน่นอนว่าถ้าคุณดูไปที่การเขียนของbind(หรือoperatorที่ใกล้เคียง>>
) คุณจะสังเกตว่า ในการให้มาของlistว่าง มันสร้างlistว่างขึ้นมา ในอีกทางหนึ่งมนการให้มาของlistที่ไม่ว่าง(ในที่นี้คือlistที่มีสมาชิกเดียวที่เก็บunitไว้[()]
) binจะเรียกความต่อเหนื่องใรที่นี้คือreturn (x, y, z)
ที่สร้างlistที่มีสมาชิกเดียวกับtripleแบบPythagorean singletonทั้งหมดเหล่านี้จะถูกต่อกันโดยการปิดรอบๆbindในการสร้างผลลัพธ์สุดท้าย(ที่ไม่สิ้นสุด) แน่นอนว่าตัวที่เรียกtriples
มาจะไม่สามารถกลืนlistทั้งหมดได้ แต่นั้นไม่สำคัญเพราะว่าHaskellนั้นlazy
ปัญหาที่มักจะต้องมีsetของloopที่อยู่ข้างในloopในสามขั้นได้ถูกทำให้ง่ายขึ้นอยางมากด้วนการชั่วเหลือของmonadแบบlistและเครื่องหมายdo
แล้วถ้าสิ่งนี้นนั้นไม่เพียงพอ Haskelอนุญาติให้คุณทำให้โค้ดง่ายมากขึ้นโดยการใช้g list comprehension
= [(x, y, z) | z <- [1..]
triples <- [1..z]
, x <- [x..z]
, y ^2 + y^2 == z^2] , x
นี่คือแค่การแต่งsyntaxให้ง่ายขึ้น(syntactic sugar)สำหรับmonadแบบlist(พูดให้ชัดเจนMonadPlus
)
คุณอาจจะเห็นการสร้างในภาษาfunctionalหรือimperativeภายใต้หน้ากากgeneratorsและcoroutines
22.2.3 สถานะที่แค่อ่านได้
Functionที่มีการเข้าถึงแบบอ่านได้อย่างเดียวของสถานะภายนอกหรือสภาวะแวดล้อม สามารถถูกแทนที่ด้วนfunctionที่นำสิ่งแวดล้อมในฐานะargumentเพิ่มเติม functionแบบpure(a, e) -> b
(ที่e
คือtypeของสิ่งแวดล้อม)ไม่ดูเหมือน(ในตอนแรก)ลูกศรKleisli แต่ในตอนทีเราuncurryมันไปเป็น a -> (e -> b)
เรามองเห็นการประดับในฐานะเพื่อนเก่าอย่างfunctor reader
newtype Reader e a = Reader (e -> a)
คุณอาจจะตีความfunctionที่returnReader
ในฐานะการสร้าง สิ่งที่ดำเนินการได้แบบเล็กๆ(mini-executable)อย่างการกระทำที่ถ้าให้สิ่งแวดล้อมมาจะสร้างผลที่ต้องการออกมา ได้มีfunctionช่วยเหลือrunReader
ในการดำเนินการการกระทำแบบนี้
runReader :: Reader e a -> e -> a
Reader f) e = f e runReader (
มันอาจจะสร้างผลลัพธ์ที่แตกต่างออกไปสำหรับค่าที่แตกต่างกันของสิ่งแวดล้อม
สังเกตว่าทั้งfunctionที่returnReader
และการกระทำReader
ตัวมันเองนั้นpure
ในการเขียนbindจากmonadReader
เริ่มจากสังเกตว่าคุณต้องสร้างfunctionที่นำสิ่งแวดล้อมe
เข้ามาและสร้างb
>>= k = Reader (\e -> ...) ra
ข้างในlambdaเราสามารถดำเนินการการกระทำra
ในการสร้างa
>>= k = Reader (\e -> let a = runReader ra e
ra in ...)
เราสามารถนำa
ไปยังความต่อเนื่องk
ในการได้มาที่การกระทำใหม่rb
>>= k = Reader (\e -> let a = runReader ra e
ra = k a
rb in ...)
สุดท้ายแล้ว เราสามารถใช้งานการกระทำrb
กับสิ่งแวดล้อมe
>>= k = Reader (\e -> let a = runReader ra e
ra = k a
rb in runReader rb e)
ในการเขียนreturn
เราสามารถสร้างการกระทำที่ละเลยสิ่งแวดล้อมและreturnค่ามี่ไม่ได้เปลี่ยนไป
นำทุกอย่างเข้ามาด้วยกัน หลังจากบางการทำให้ง่ายขึ้น เราได้นิยามมาแบบนี้
instance Monad (Reader e) where
>>= k = Reader (\e -> runReader (k (runReader ra e)) e)
ra return x = Reader (\e -> x)
22.2.4 สถานะที่แค่เขียนได้
สิ่งนี้คือแค่ตัวอย่างการlogแรกของเรา การประดับนั้นให้มาโดยfunctorWriter
newtype Writer w a = Writer (a, w)
เพื่อให้สมบูรณ์ก็ได้มีตัวช่วยที่เรียบง่ายrunWriter
ที่แยกออกconstructorของข้อมูล
runWriter :: Writer w a -> (a, w)
Writer (a, w)) = (a, w) runWriter (
ฏ้ในสิ่งที่เราได้เห็นก่อนหน้านี้ ในการที่จะทำให้Writer
ประกอบกันได้ w
จำเป็นต้องเป็นmonoid ในที่นี้instanceสำหรับWriter
ถูกเขียนในรูปแบบของbind operator
instance (Monoid w) => Monad (Writer w) where
Writer (a, w)) >>= k = let (a', w') = runWriter (k a)
(in Writer (a', w `mappend` w')
return a = Writer (a, mempty)
22.2.5 สถานะ
Functionที่มีการเข้าถึงสถานะแบบอ่านและเขียนรวมการประดับของReader
และWriter
คุณอาจจะคิดถึงมันในฐานะfunctionที่pureที่นำสถานะในฐานะargumentเพื่อเติมและสร้างคู่ของ ค่าและสถานะเป็นผลลัพธ์อย่าง(a, s) -> (b, s)
หลังจากการcurry เราได้รูปแบบของลูกศรKleisliมา a -> (s -> (b, s))
ที่การประดับถูกabstractedในfunctorState
อย่าง
newtype State s a = State (s -> (a, s))
ในอีกครั้งเราสามารถดูไปที่ลูกศรKleisliในฐานะการreturnของการกระทำที่สามารถถูกดำเนินการโดยการใช้functionช่วย
runState :: State s a -> s -> (a, s)
State f) s = f s runState (
สถานะแรกเริ่มที่แตกต่างกันอาจจะไม่แค่สร้างผลลัพธ์ที่แตกต่างแต่ก็สถานะสุดท้ายที่แตกต่างกันด้วย
การเขียนของbindสำหรับmonadState
นั้นคล้ายอย่างมากกับmonadReader
นอกเหนือจากว่าต้องมีความรอบคอบในการนำสถานะที่ถูกต้องในแต่ละขั้น
>>= k = State (\s -> let (a, s') = runState sa s
sa = k a
sb in runState sb s')
นี้คือinstanceเต็ม
instance Monad (State s) where
>>= k = State (\s -> let (a, s') = runState sa s
sa in runState (k a) s')
return a = State (\s -> (a, s))
ก็ได้มีลูกศรKleisliตัวช่วยที่อาจจะถูกใช้ในการแปรเปลี่ยนสถานะ หนึ่งในพวกมันกู้สถานะมาสำหรับการตรวจสอบ
get :: State s s
= State (\s -> (s, s)) get
และอีกตัวหนึ่งแทนที่มันกับสถานะที่ใหม่โดยสิ้นเชิง
put :: s -> State s ()
= State (\s -> ((), s')) put s'
22.2.6 Exception
Functionแบบimperativeที่throws exceptionนั้นเป็นfunctionบางส่วน (มันคือfuntionที่ไม่ถูกนิยามสำหรับบางค่าและargument) การเขียนexceptionที่ง่ายที่สุดในรูปแบบของfunctionสมบูรณ์ที่pureใช้functorMaybe
functionบางส่วนนั้นถูกเสริมไปยังfunctionสมบูรณ์ที่แค่returnJust a
ในตอนที่สมเหตุสมผล และNothing
ในตอนที่มันไม่ ถ้าเราต้องการที่ก็จะreturnบางข้อมูลเกี่ยวกับสาเหตุของความล้มเหลว เราสามารถใช้functorEither
แทนที่ (ที่typeแรกนั้นคงที่ตัวอย่างเช่นไว้กับString
)
ในที่นี้คือinstanceMonad
สำหรับMaybe
instance Monad Maybe where
Nothing >>= k = Nothing
Just a >>= k = k a
return a = Just a
สังเกตว่าการประกอบกันแบบmonadสำหรับMaybe
ได้ลัดวงจรอย่างถูกต้องของการคำนวณ (ความต่อเนื่องk
ที่ไม่เคยถูกเรียก) ในตอนที่errorนั้นถูกพบเห็น นั้นคือพฤติกรรมที่เราคาดหวังจากexception
22.2.7 ความต่อเนื่อง
มันคือสถานการณ์“ไม่ต้องเรียกเรา เราจะเรียกคุณ” ที่คุณอาจจะได้มีประสบการณ์หลังจากสัมภาษณ์งาน แทนที่จะได้คำตอบตรงๆ คุณต้องที่จะให้handlerหรือfunctionมี่ถูกเรียกคู่กับผลลัพธ์ การเขียโปรแกรมแบบนี้นั้นมีประโยชน์โดยเฉพาะในตอนที่ผลลัพธ์นั้นไม่ถูกรู้ในตอนที่ถูกเรียกเพราะว่า ตัวอย่างเช่น มันถูกประเมินโดยอีกthreadหนึ่งหรือนำมาจากwebsite remote ลูกศรKleisliในที่นี้return functionที่ยอมรับhandlerที่แทน”การคำนวณที่เหลือ”
data Cont r a = Cont ((a -> r) -> r)
Handlera -> r
ในตอนที่มันถูกเรียกในที่สุด สร้างผลลัพธ์ของtyper
และผลลัพธ์นี้นั้นถูกreturnในตอนท้าย ความต่อเนื่องนั้นถูกparameterizedโดยtypeของผลลัพธ์ (ในทางปฏิบัติ สิ่งนี้มักจะเป็นตัวชี้วัดสะถานะบางอย่าง)
ได้มีfunctionช่วยสำหรับการดำเนินการของการกระทำที่ถูกreturnedมาโดยลูกศรKleisli มันนำhandlerและส่งมันไปยังความต่อเนื่อง
runCont :: Cont r a -> (a -> r) -> r
Cont k) h = k h runCont (
การประกอบกันของความต่อเนื่องนั้นมีชื่อเสียงในความยากของมัน ดังนั้นการทำงานผ่านmonadและโดยเฉพาะเครื่องหมายdo
จึงเป็นความได้เปรียบอย่างมาก
เรามาตามหาการเขียนสำหรับbind เริ่มจากเรามาดูsignatureเดี่ยวๆของมัน
(>>=) :: ((a -> r) -> r) ->
-> (b -> r) -> r) ->
(a -> r) -> r) ((b
เป้าหมายของเราคือการสร้างfunctionที่นำhandler (b -> r)
และสร้างผลลัพธ์r
ดังนั้นนั้นคือจุดเริ่มของเรา
>>= kab = Cont (\hb -> ...) ka
ข้างในlambda เราต้องการที่จะเรียกfunctionka
กับhandlerที่เหมาะสมที่แทนการคำนวณที่เหลือ เราจะเขียนhandlerนี้ในฐานะlambda
-> ...) runCont ka (\a
ในกรณีนี้ ส่วนที่เหลือของการคำนวณเกี่ยข้องกับ การเรียกkab
แรกกับa
และก็นำhb
ไปยังการกระทำที่เป็นผลลัพธ์ที่ตามมาkb
-> let kb = kab a
runCont ka (\a in runCont kb hb)
ในที่คุณได้เห็นความต่อเนื่องนั้นถูกประกอบจากข้างในไปข้างนอก handlerสุดท้ายhb
ถูกเรียกจากชั้นข้างในสุดของการคำนวณ ในที่นี้คือinstanceเต็มๆของมัน
instance Monad (Cont r) where
>>= kab = Cont (\hb -> runCont ka (\a -> runCont (kab a) hb))
ka return a = Cont (\ha -> ha a)
22.2.8 Inputแบบมีปฏิสัมพันธ์
สิ่งนี้คือปัญหาที่จัดการยากและเป็นแหล่งของความสับสนหลากหลาย ชัดเจนว่าfunctionอย่างgetChar
ถ้ามันจะreturnตัวอักษรที่ถูกพิมพ์ไปยังkeyboardไม่สามารถที่จะpureได้ แต่ถ้ามันreturnอักษรข้างในภาชนะ? ตราบเท่าทีไม่มีวิธีในการดึงอักษรออกจากภาชนะนี้ เราสามารถอ้างได้ว่าfunctionนั้นpure ในทุกๆเวลาที่คุณเรียกgetChar
มันอาจจะreturnภาชนะเดียวกัน ในทางแนวคิดแล้วภาชนะนี้อาจจะเก็บsuperpositionของตัวอักษรที่เป็นไปได้ทั้งหมด
ถ้าคุณคุ้นเคยกับกลศาสตร์ควอนตัม คุณควรที่จะไม่มีปัญหาในการเช้าใจการเปรียบเทียบนี้ มันแค่เหมือนกับกล่องที่มีแมวของSchrödingerอยู่ข้างใน (ยกเว้นว่าไม่มีวิธีการในการเปิดและส่องข้างในกล่องนั้น) กล่องนั้นถูกนิยามโดยการใช้functor built-inพิเศษ ในตัวอย่างของเราgetChar
นั้นอาจจะถูกประกาศในฐานะลูกศรKleisli
getChar :: () -> IO Char
(ตามความเป็นจริงแล้ว เนื่องว่าfunctionจากtype unitนั้นเหมือนกับการเลือกค่าของtypeของreturn การประกาศของgetChar
นั้นจึงถูกทำให้ง่ายขึ้นโดยgetChar :: IO Char
)
ในการเป็นfunctorIO
ให้คุณแปลงเปลี่ยนเนื้อหาของมันโดยการใช้fmap
และในฐานะfunctorมันสามารถเก็บเนื้อหาของtypeใดๆก็ตามไม่ไช่แค่ตัวอักษร การใช้งานจริงๆของวิธีการนี้จะชัดเจนมากขึ้งในตอนที่คุณพิจารณาว่าในHaskellIO
นั้นเป็นmonad มันหมายความว่าคุณสามารถประกอบลูกศรKleisliที่ผลิตวัตถุIO
คุณอาจจะคิดว่าการประกอบกันแบบKleisli อาจจะอนุญาติคุณในการส่องไปที่เนื้อกาของวัตถุIO
(ก่อให้เกิด”การยุบลงของคลื่น”ถ้าเราจะตามจากการเปลียบเทียบของquantum) แน่นอนคุณอาจจะประกอบgetChar
กับลูกศรKleisliที่นำตัวอักษรและก็เปลี่ยนไปเป็นจำนวนเต็ม ข้อยกเว้นคือการที่ว่าลูกศรKleisliที่สองต้องreturnจำนวนเต็มนี้ในฐานะ(IO Int)
และอีกครั้ง เราจะจบลงที่superpositionของจำนวนเต็มที่เป็นไปได้ทั้งหมด และถัดๆไป แมวของSchrödingerนั้นไม่สามารถออกมาจากถุงได้ ในตอนที่คุณอยู่ข้างในmonadIO
มันจะไม่มีทางที่จะออกไป ได้ไม่มีความเท่ากันของrunState
หรือrunReader
สำหรับmonadIO
ไม่มีrunIO
ดังนั้นอะไรคือสิ่งที่คุณสามารถทำได้กับผลของลูกศรKleisli(ที่คือวัตถุIO
)นอกเหนือจากการประกอบมันกับลูกศรKleisliอีกตัว? ก็คุณสามารถreturnมันจากmain
ในHaskell main
นั้นมีsignatureอย่าง
main :: IO ()
และคุณมีอิสระในการคิดถึงมันในฐานะลูกศรKleisliว่า
main :: () -> IO ()
จากมุมมองนี้ โปรแกรมของHaskellนั้นคือแค่ลูกศรKleisliที่ใหญ่อันหนึ่งในmonadIO
คุณสามารประกอบมันจากลูกศรKleisliขนาดเล็กกว่าโดยการใช้การประกอบกันแบบmonad มันชึ้นอยู่กับระบบruntimในการทำบางอย่างกับวัตถุIO
ที่เป็นผลลัพธ์ (ที่ก็เรียกว่าการกระทำIO
)
สังเกตว่าลูกศรมันเองเป็นfunctionแบบpure (มันคือfunctionแบบpureทั้งหมด) งานสกปรกนั้นถูกส่งไปยังระบบ ในตอนที่มันดำเนินการในที่สุดของการกระทำของIO
ที่returnมาจากmain
มันทำทุกๆอย่างของสิ่งที่สกปรกอย่าง การอ่านinputของuser การแก้ไขfile การเขียนmessagesแปลกๆ การformat diskและอื่นๆ โปรแกรมHaskellนั้นไม่เคยที่จะทำให้มือสกปรก(ก็ยกเว้นในตอนที่มัยเรียกunsafePerformIO
แต่นั้นคืออีกเรื่องราวหนึ่ง)
แน่นอน เพราะว่าHaskellนั้นlazy main
returnแทบโดยทันที่และงานสกปรกเริ่มขึ้นในตอนนั้น มันเป็นในตอนการดำเนินการของการกระทำIO
ที่ผลของการคำนวณแบบpureนั้นถูกขอมาและประเมินค่าตามตำขอ ดังนั้นในความเป็นจริงแล้ว การดำเนินการของโปรแกรมนั้นคือการไปกลับระหว่างโค้กที่pure(Haskell)และdirty(ระบบ)
ได้มีการตีความในอีกทางของmonadIO
ที่แปลกประหลาดแต่สมเหตุสมผลในฐานะmodelทางคณิตศาสตร์ มันมองทั้งUniverseในฐานะวัตถุในโปรแกรม สังเกตว่าในทางแนวคิดแล้วmodelแบบimperativeมองUniverseในฐานะวัตถุสากลที่อยู่ข้างนอก ดังนั้นกระบวนการในการกระทำI/Oมีผลข้างเคียงโดยการมีปฏิสัมพันธ์กับวัตถุนั้น พวกมันทั้งอ่านและแก้ไขสถานะของUniverse
เราได้รู้แล้วในการทำงานกับสถานะในการเขียนโปรแกรมแบบfunctional (เราใช้monadสถานะ) แต่ไม่เหมือนจากสถานะง่ายๆ สถานะของUniverseไม่สามารถถูกอธิบายได้ง่ายโดยการใช้โครงสร้างข้อมูลมาตราฐาน แค่เราไม่ต้องทำแบบนั้นตราบเท่าที เราไม่เคยที่จะมีปฏิสัมพันธ์กับมัน มันพอแล้วที่เราสมมติว่าได้มีtypeRealWorld
และโดยความมหัศจรรย์ของวิศวกรรมทางcosmic runtimeสามารถที่จะให้วัตถุของtypeนี้มา การกระทำแบบIO
นันคือแค่function
type IO a = RealWorld -> (a, RealWorld)
หรือในรูปแบบของmonadState
type IO = State RealWorld
แต่>=>
และreturn
สำหรับmonadIO
ต้องถูกสร้างไปในภาษานั้น
22.2.9 Outputแบบมีปฏิสัมพันธ์
ในmonadIO
เดียวกันที่ถูกใช้ในการencapsulate outputแบบมีปฏิสัมพันธ์ RealWorld
นั้นควรที่จะเก็บอุปกรณ์outputทั้งหมด คุณอาจจะสงสัยว่าทำไมเราไม่สามารถแค่เรียกfunction outputจากHaskellและแกล้งว่าพวมมันไม้ได้ทำอะไรเลย ตัวอย่างเช่น ทำไมเราต้องมี
putStr :: String -> IO ()
แทนที่จะเป็นสิ่งที่ง่ายกว่าอย่าง
putStr :: String -> ()
มีอยู่สองเหตุผลคือHaskellนั้นlazyดังนั้นมันจะไม่เคยที่จะเรียกfunctionที่outputของมัน(ในที่นี้คือobjectแบบunit)ไม่ได้ถูกใข้ไปกับอะไร และถึงแม้มันจะไม่lazy มันก็อาจจะเปลียนลำดับของการเรียกแบบนี้อย่างอิสระและทำให้outputเปลี่ยนไป วิธีเดียวในการบังคับการดำเนินการของแต่ละขั้นของสองfunctionในHaskellคือผ่านการผูกกันของข้อมูล inputของfunctionหนึ่งอาจจะต้องขึ้นอยู่กับoutputของอีกตัวหนึ่ง การมีRealWorld
ที่ส่งระหว่างการกระทำIO
ได้บังคับการต่อเนื่องกัน
ในแนวคิดแล้วในโปรแกรม
main :: IO ()
= do
main putStr "Hello "
putStr "World!"
การกระทำที่พิมพ์“World!”ออกมา รับUniverseที่“Hello”นั้นอยู่บนหน้าจอแล้วเข้ามาในฐานะinput มันจะoutput Universeใหม่ด้วย”Hello World!“บนหน้าจอ
22.3 บทสรุป
แน่นอนว่าผมได้แค่แตะพื้นผิวของการเขียนโปรแกรมแบบmonad monadนั้นไม่แค่ทำในสิ่งที่ทำได้แบบทั่วๆไปด้วยผลข้างเคียงในการเขียนโปรแกรมแบบimperativeได้สำเร็จในfunctionแบบpure แต่มันก็ทำมันในระดับการควบคุมที่สูงและความปลอดภัยของtype แต่มันนั้นจะไม่มีข้อบกพร่อง ปัญหาส่วนใหญ่เกี่ยวกับmonadคือว่ามันไม่ประกอบกันอย่างง่ายๆระหว่างกัน ถึงจะเป็นอย่างนั้น คุณสามารถรวมmonadที่ไม่ชับช้อนส่วนใหญ่โดยการใช้libaryตัวแปลงmonad มันค่อนข้างง่ายในการสร้างการช้อนของmonadที่ประกอบกันอย่างสถานะกับexception แต่ไม่มีสูตรสําเร็จในการช้อนmonadใดๆก็ตามเข้าด้วยกัน