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ใดๆก็ตามเข้าด้วยกัน


  1. https://www.cs.cmu.edu/~crary/819-f09/Moggi91.pdf↩︎

  2. https://math.andrej.com/2016/08/06/hask-is-not-a-category/↩︎

  3. http://ericniebler.com/2014/04/27/range-comprehensions/↩︎