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
join = concat
return x = [x]Operator Bindสำหรับmonadแบบlistนั้นให้มาโดยสูตรทั่วไปอย่างfmapตามมาด้วยjoinที่ในกรณีนี้ให้เรา
as >>= k = concat (fmap k 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ของจำนวนเต็มที่สามารถมาจากด้านของสามเหลี่ยมมุมฉาก
triples = do
z <- [1..]
x <- [1..z]
y <- [x..z]
guard (x^2 + y^2 == z^2)
return (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 -> [()]
guard True = [()]
guard False = []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
triples = [(x, y, z) | z <- [1..]
, x <- [1..z]
, y <- [x..z]
, x^2 + y^2 == z^2]นี่คือแค่การแต่ง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
runReader (Reader f) e = f eมันอาจจะสร้างผลลัพธ์ที่แตกต่างออกไปสำหรับค่าที่แตกต่างกันของสิ่งแวดล้อม
สังเกตว่าทั้งfunctionที่returnReaderและการกระทำReaderตัวมันเองนั้นpure
ในการเขียนbindจากmonadReader เริ่มจากสังเกตว่าคุณต้องสร้างfunctionที่นำสิ่งแวดล้อมeเข้ามาและสร้างb
ra >>= k = Reader (\e -> ...)ข้างในlambdaเราสามารถดำเนินการการกระทำraในการสร้างa
ra >>= k = Reader (\e -> let a = runReader ra e
in ...)เราสามารถนำaไปยังความต่อเนื่องkในการได้มาที่การกระทำใหม่rb
ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in ...)สุดท้ายแล้ว เราสามารถใช้งานการกระทำrbกับสิ่งแวดล้อมe
ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in runReader rb e)ในการเขียนreturnเราสามารถสร้างการกระทำที่ละเลยสิ่งแวดล้อมและreturnค่ามี่ไม่ได้เปลี่ยนไป
นำทุกอย่างเข้ามาด้วยกัน หลังจากบางการทำให้ง่ายขึ้น เราได้นิยามมาแบบนี้
instance Monad (Reader e) where
ra >>= k = Reader (\e -> runReader (k (runReader ra e)) e)
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)
runWriter (Writer (a, w)) = (a, w)ฏ้ในสิ่งที่เราได้เห็นก่อนหน้านี้ ในการที่จะทำให้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)
runState (State f) s = f sสถานะแรกเริ่มที่แตกต่างกันอาจจะไม่แค่สร้างผลลัพธ์ที่แตกต่างแต่ก็สถานะสุดท้ายที่แตกต่างกันด้วย
การเขียนของbindสำหรับmonadStateนั้นคล้ายอย่างมากกับmonadReaderนอกเหนือจากว่าต้องมีความรอบคอบในการนำสถานะที่ถูกต้องในแต่ละขั้น
sa >>= k = State (\s -> let (a, s') = runState sa s
sb = k a
in runState sb s')นี้คือinstanceเต็ม
instance Monad (State s) where
sa >>= k = State (\s -> let (a, s') = runState sa s
in runState (k a) s')
return a = State (\s -> (a, s))ก็ได้มีลูกศรKleisliตัวช่วยที่อาจจะถูกใช้ในการแปรเปลี่ยนสถานะ หนึ่งในพวกมันกู้สถานะมาสำหรับการตรวจสอบ
get :: State s s
get = State (\s -> (s, s))และอีกตัวหนึ่งแทนที่มันกับสถานะที่ใหม่โดยสิ้นเชิง
put :: s -> State s ()
put s' = State (\s -> ((), 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
runCont (Cont k) h = k hการประกอบกันของความต่อเนื่องนั้นมีชื่อเสียงในความยากของมัน ดังนั้นการทำงานผ่านmonadและโดยเฉพาะเครื่องหมายdoจึงเป็นความได้เปรียบอย่างมาก
เรามาตามหาการเขียนสำหรับbind เริ่มจากเรามาดูsignatureเดี่ยวๆของมัน
(>>=) :: ((a -> r) -> r) ->
(a -> (b -> r) -> r) ->
((b -> r) -> r)เป้าหมายของเราคือการสร้างfunctionที่นำhandler (b -> r)และสร้างผลลัพธ์r ดังนั้นนั้นคือจุดเริ่มของเรา
ka >>= kab = Cont (\hb -> ...)ข้างในlambda เราต้องการที่จะเรียกfunctionkaกับhandlerที่เหมาะสมที่แทนการคำนวณที่เหลือ เราจะเขียนhandlerนี้ในฐานะlambda
runCont ka (\a -> ...)ในกรณีนี้ ส่วนที่เหลือของการคำนวณเกี่ยข้องกับ การเรียกkabแรกกับaและก็นำhbไปยังการกระทำที่เป็นผลลัพธ์ที่ตามมาkb
runCont ka (\a -> let kb = kab a
in runCont kb hb)ในที่คุณได้เห็นความต่อเนื่องนั้นถูกประกอบจากข้างในไปข้างนอก handlerสุดท้ายhbถูกเรียกจากชั้นข้างในสุดของการคำนวณ ในที่นี้คือinstanceเต็มๆของมัน
instance Monad (Cont r) where
ka >>= kab = Cont (\hb -> runCont ka (\a -> runCont (kab a) hb))
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 mainreturnแทบโดยทันที่และงานสกปรกเริ่มขึ้นในตอนนั้น มันเป็นในตอนการดำเนินการของการกระทำ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 ()
main = do
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ใดๆก็ตามเข้าด้วยกัน