ในความเห็นต่ำต้อยของฉันคำตอบสำหรับคำถามที่มีชื่อเสียง"Monad คืออะไร" โดยเฉพาะคนที่ได้รับการโหวตมากที่สุดพยายามที่จะอธิบายสิ่งที่เป็น monad อย่างชัดเจนโดยไม่ต้องอธิบายว่าทำไม monads มีความจำเป็นจริงๆ พวกเขาสามารถอธิบายได้ว่าเป็นวิธีแก้ปัญหาหรือไม่
ในความเห็นต่ำต้อยของฉันคำตอบสำหรับคำถามที่มีชื่อเสียง"Monad คืออะไร" โดยเฉพาะคนที่ได้รับการโหวตมากที่สุดพยายามที่จะอธิบายสิ่งที่เป็น monad อย่างชัดเจนโดยไม่ต้องอธิบายว่าทำไม monads มีความจำเป็นจริงๆ พวกเขาสามารถอธิบายได้ว่าเป็นวิธีแก้ปัญหาหรือไม่
คำตอบ:
จากนั้นเรามีปัญหาใหญ่ครั้งแรก นี่คือโปรแกรม:
f(x) = 2 * x
g(x,y) = x / y
เราจะบอกได้อย่างไรว่า อะไรที่ต้องถูกประหารชีวิตก่อน ? เราจะสร้างลำดับของฟังก์ชั่นที่สั่ง (เช่นโปรแกรม ) โดยใช้ไม่เกินฟังก์ชั่นได้อย่างไร
การแก้ไข: ฟังก์ชั่นการเขียน ถ้าคุณต้องการที่แรกg
และจากนั้นเพียงแค่เขียนf
f(g(x,y))
ด้วยวิธีนี้ "โปรแกรม" เป็นฟังก์ชั่นเช่นกัน: main = f(g(x,y))
. ได้. แต่ ...
ปัญหาเพิ่มเติม: ฟังก์ชั่นบางอย่างอาจล้มเหลว (เช่นg(2,0)
หารด้วย 0) เราไม่มี "ข้อยกเว้น"ใน FP (ข้อยกเว้นไม่ใช่ฟังก์ชัน) เราจะแก้ปัญหาได้อย่างไร
วิธีแก้ปัญหา: มา อนุญาตให้ฟังก์ชั่นคืนสองสิ่ง : แทนที่จะมีg : Real,Real -> Real
(ฟังก์ชั่นจากสอง reals เป็นของจริง), อนุญาตให้g : Real,Real -> Real | Nothing
(ฟังก์ชั่นจากสอง reals เป็น (จริงหรือไม่มีอะไร))
แต่ฟังก์ชั่นควร (จะง่าย) ผลตอบแทนเพียงสิ่งหนึ่งที่
การแก้ไข: เรามาสร้างข้อมูลประเภทใหม่ที่จะส่งคืน "มวยชนิด " ที่ล้อมรอบอาจเป็นของจริงหรือไม่มีอะไรเลย g : Real,Real -> Maybe Real
ดังนั้นเราสามารถมี ได้. แต่ ...
สิ่งที่เกิดขึ้นในขณะนี้เพื่อf(g(x,y))
?f
Maybe Real
ไม่พร้อมที่จะกิน และเราไม่ต้องการที่จะเปลี่ยนฟังก์ชั่นที่เราสามารถเชื่อมต่อกับทุกการกินg
Maybe Real
การแก้ไข: ขอมีฟังก์ชั่นพิเศษเพื่อ "การเชื่อมต่อ" / "เขียน" / "ลิงก์" ฟังก์ชั่น ด้วยวิธีนี้เราสามารถปรับเอาท์พุทของฟังก์ชั่นหนึ่งเพื่อเลี้ยงสิ่งต่อไปนี้
ในกรณีของเรา: g >>= f
(เชื่อมต่อ / เขียนg
ถึงf
) เราต้องการ>>=
ที่จะได้รับg
ของที่ส่งออกตรวจสอบได้และในกรณีที่มันเป็นNothing
เพียงแค่ไม่ได้โทรf
และผลตอบแทนNothing
; หรือในทางกลับกันให้แยกกล่องบรรจุReal
แล้วป้อนเข้าf
ด้วย (อัลกอริทึมนี้เป็นเพียงการดำเนินการ>>=
ตามMaybe
ประเภท) นอกจากนี้โปรดทราบว่า>>=
จะต้องเขียนเพียงครั้งเดียวต่อ "ประเภทมวย" (กล่องที่แตกต่างกันขั้นตอนวิธีการปรับตัวที่แตกต่างกัน)
ปัญหาอื่น ๆ อีกมากมายที่เกิดขึ้นซึ่งสามารถแก้ไขได้โดยใช้รูปแบบเดียวกันนี้: 1. ใช้ "กล่อง" เพื่อประมวล / เก็บความหมาย / ค่าต่าง ๆ และมีฟังก์ชั่นเช่นg
นั้นคืนค่าเหล่านั้น 2. มีนักแต่งเพลง / ลิงเกอร์g >>= f
เพื่อช่วยเชื่อมต่อg
เอาท์พุทf
ของอินพุตดังนั้นเราจึงไม่จำเป็นต้องเปลี่ยนแปลงใด ๆf
เลย
ปัญหาที่น่าสังเกตที่สามารถแก้ไขได้โดยใช้เทคนิคนี้คือ:
มีสถานะโกลบอลที่ทุกฟังก์ชั่นในลำดับของฟังก์ชั่น ("โปรแกรม") สามารถแชร์: solution StateMonad
วิธีการแก้ปัญหา
เราไม่ชอบ "ฟังก์ชั่นที่ไม่บริสุทธิ์": ฟังก์ชั่นที่ให้ผลลัพธ์ที่แตกต่างกันสำหรับอินพุตเดียวกัน ดังนั้นให้ทำเครื่องหมายฟังก์ชั่นเหล่านั้นทำให้พวกเขากลับค่าที่ติดแท็ก / กล่อง: IO
monad
ความสุขทั้งหมด!
IO
monad นั้นเป็นอีกปัญหาหนึ่งในรายการIO
(จุดที่ 7) ในทางกลับกันIO
จะปรากฏเพียงครั้งเดียวและท้ายที่สุดดังนั้นอย่าเข้าใจ "เวลาส่วนใหญ่ที่พูดถึง ... เกี่ยวกับ IO"
Either
) คำตอบส่วนใหญ่เกี่ยวกับ "เราต้องใช้ functors ทำไม"
g >>= f
เพื่อช่วยเชื่อมต่อg
เอาต์พุตf
ของอินพุตกับอินพุตดังนั้นเราจึงไม่จำเป็นต้องเปลี่ยนแปลงอะไรf
เลย" มันไม่ถูกต้องเลย ก่อนในf(g(x,y))
, f
สามารถผลิตอะไร f:: Real -> String
มันอาจจะเป็น ด้วย "การจัดองค์ประกอบแบบ monadic" จะต้องเปลี่ยนเป็นผลิตMaybe String
มิฉะนั้นประเภทจะไม่พอดี ยิ่งกว่านั้น>>=
ตัวมันเองก็ไม่พอดี !! มันคือ>=>
องค์ประกอบนี้ใช่>>=
ไหม ดูการสนทนากับ dfeuer ภายใต้คำตอบของ Carl
คำตอบคือแน่นอน"เราทำไม่ได้" เช่นเดียวกับนามธรรมทั้งหมดไม่จำเป็น
Haskell ไม่ต้องการสิ่งที่เป็นนามธรรม monad ไม่จำเป็นสำหรับการดำเนินการ IO ในภาษาบริสุทธิ์ IO
ประเภทดูแลที่ดีด้วยตัวเอง desugaring เอกที่มีอยู่ของdo
บล็อกจะถูกแทนที่ด้วย desugaring ไปbindIO
, returnIO
และfailIO
ตามที่กำหนดไว้ในGHC.Base
โมดูล (ไม่ใช่โมดูลที่เป็นเอกสารเกี่ยวกับการแฮ็กดังนั้นฉันจะต้องชี้ไปที่แหล่งที่มาของเอกสารนั้น) ดังนั้นไม่จำเป็นที่จะต้องมีการทำ monad นามธรรม
ดังนั้นถ้ามันไม่จำเป็นทำไมมันมีอยู่? เพราะพบว่ารูปแบบการคำนวณหลายรูปแบบนั้นเป็นโครงสร้างแบบ monadic นามธรรมของโครงสร้างช่วยให้การเขียนรหัสที่ทำงานในทุกอินสแตนซ์ของโครงสร้างนั้น เพื่อให้รัดกุมยิ่งขึ้น - นำโค้ดกลับมาใช้ใหม่
ในภาษาที่ใช้งานได้เครื่องมือที่ทรงพลังที่สุดที่นำมาใช้ในการนำโค้ดกลับมาใช้ใหม่คือองค์ประกอบของฟังก์ชั่น ของเก่าดี(.) :: (b -> c) -> (a -> b) -> (a -> c)
ผู้ประกอบการมีพลังเหลือล้น มันทำให้ง่ายต่อการเขียนฟังก์ชั่นขนาดเล็กและกาวเข้าด้วยกันด้วยค่าใช้จ่าย syntactic หรือ semantic ที่น้อยที่สุด
แต่มีบางกรณีที่ประเภทไม่ได้ผลค่อนข้างถูกต้อง คุณจะทำอย่างไรเมื่อคุณมีfoo :: (b -> Maybe c)
และbar :: (a -> Maybe b)
? foo . bar
ไม่พิมพ์ดีดเพราะb
และMaybe b
ไม่ใช่ประเภทเดียวกัน
แต่ ... มันเกือบจะถูกต้องแล้ว คุณแค่อยากได้ระยะเพิ่มอีกนิด คุณต้องการที่จะสามารถที่จะรักษาราวกับว่ามันเป็นพื้นMaybe b
b
มันเป็นความคิดที่ไม่ดีเลยที่จะเอาเรื่องเหล่านี้เป็นประเภทเดียวกัน นั่นเป็นสิ่งเดียวกันกับพอยน์เตอร์พอยน์เตอร์ซึ่ง Tony Hoare ขึ้นชื่อว่ามีความผิดพลาดหลายพันล้านดอลลาร์หลายดังนั้นหากคุณไม่สามารถปฏิบัติต่อมันในรูปแบบเดียวกันบางทีคุณอาจหาวิธีที่จะขยายกลไกการ(.)
จัดองค์ประกอบภาพ
(.)
ในกรณีที่มันเป็นสิ่งสำคัญที่จะตรวจสอบจริงๆทฤษฎีพื้นฐาน โชคดีที่มีคนทำสิ่งนี้ให้เราแล้ว แต่กลับกลายเป็นว่าการรวมกันของ(.)
และid
รูปแบบโครงสร้างทางคณิตศาสตร์ที่รู้จักกันเป็นหมวดหมู่ แต่มีวิธีอื่นในการจัดหมวดหมู่ ยกตัวอย่างเช่นหมวดหมู่ Kleisli ช่วยให้วัตถุที่ประกอบขึ้นจะเพิ่มขึ้นเล็กน้อย หมวดหมู่สำหรับ Kleisli Maybe
จะประกอบด้วยและ(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
id :: a -> Maybe a
นั่นคือวัตถุในหมวดหมู่เพิ่ม(->)
ด้วยMaybe
ดังนั้นจึง(a -> b)
กลายเป็น(a -> Maybe b)
กลายเป็น
และทันใดนั้นเราได้ขยายพลังของการแต่งเพลงไปสู่สิ่งที่การ(.)
ดำเนินงานดั้งเดิมไม่สามารถทำได้ นี่คือแหล่งที่มาของพลังงานที่เป็นนามธรรมใหม่ หมวดหมู่ Kleisli สามารถใช้งานได้มากกว่าประเภทMaybe
อื่น พวกเขาทำงานกับทุกประเภทที่สามารถรวบรวมหมวดหมู่ที่เหมาะสมตามกฎหมายหมวดหมู่
id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
ตราบใดที่คุณสามารถพิสูจน์ได้ว่าประเภทของคุณเป็นไปตามกฎหมายทั้งสามนี้คุณสามารถเปลี่ยนเป็นหมวดหมู่ Kleisli ได้ และเรื่องใหญ่เกี่ยวกับเรื่องนี้คืออะไร? มันกลับกลายเป็นว่าพระสงฆ์เป็นสิ่งเดียวกันกับหมวดหมู่ Kleisli Monad
's return
เป็นเช่นเดียวกับ id
Kleisli Monad
's (>>=)
ไม่เหมือนกับ Kleisli (.)
แต่มันจะออกมาเป็นเรื่องง่ายมากที่จะเขียนในแต่ละแง่ของคนอื่น ๆ และหมวดหมู่ของกฎหมายก็เหมือนกับกฎของ monad เมื่อคุณแปลความแตกต่างระหว่าง(>>=)
และ(.)
และ
แล้วทำไมต้องผ่านสิ่งเหล่านี้ไป? ทำไมต้องมีMonad
นามธรรม? ดังที่ฉันได้กล่าวถึงข้างต้นจะช่วยให้สามารถใช้รหัสซ้ำได้ มันยังช่วยให้ใช้ซ้ำรหัสตามสองมิติที่แตกต่างกัน
มิติแรกของการใช้รหัสซ้ำมาจากการปรากฏตัวของสิ่งที่เป็นนามธรรม คุณสามารถเขียนโค้ดที่ใช้ได้กับทุก ๆ สิ่งที่เป็นนามธรรม มีทั้งเป็นmonad ห่วงMonad
แพคเกจประกอบด้วยลูปที่ทำงานร่วมกับอินสแตนซ์ใด ๆ
มิติที่สองเป็นทางอ้อม แต่มันเกิดขึ้นจากการมีอยู่ขององค์ประกอบ เมื่อการเรียบเรียงเป็นเรื่องง่ายการเขียนโค้ดเป็นชิ้นเล็ก ๆ นี่เป็นวิธีเดียวกับที่(.)
ผู้ปฏิบัติงานใช้สำหรับฟังก์ชั่นสนับสนุนการเขียนฟังก์ชั่นขนาดเล็กและใช้ซ้ำได้
เหตุใดจึงมีนามธรรม เพราะมันได้รับการพิสูจน์แล้วว่าเป็นเครื่องมือที่ช่วยให้สามารถเขียนโค้ดได้มากขึ้นส่งผลให้สามารถสร้างรหัสที่สามารถใช้ซ้ำได้และกระตุ้นให้มีการสร้างโค้ดที่นำกลับมาใช้ใหม่ได้มากขึ้น การใช้รหัสซ้ำเป็นหนึ่งในโปรแกรมที่ศักดิ์สิทธิ์ สิ่งที่เป็นนามธรรมอยู่เพราะมันเคลื่อนเราไปสู่จอกศักดิ์สิทธิ์นั้นเล็กน้อย
newtype Kleisli m a b = Kleisli (a -> m b)
. ประเภท Kleisli มีฟังก์ชั่นที่เด็ดขาดชนิดกลับ ( b
ในกรณีนี้) m
เป็นอาร์กิวเมนต์เพื่อนวกรรมิกประเภท Iff Kleisli m
เป็นหมวดหมู่m
Monad
Kleisli m
ดูเหมือนว่าจะในรูปแบบหมวดหมู่นี้มีวัตถุประเภท Haskell และเช่นที่ลูกศรจากa
ที่จะb
มีฟังก์ชั่นจากa
ไปm b
ด้วยและid = return
(.) = (<=<)
มันเกี่ยวกับความถูกต้องหรือฉันกำลังผสมสิ่งต่าง ๆ หรืออะไรบางอย่างเข้าด้วยกัน?
a
และb
แต่ไม่ใช่ฟังก์ชันที่ง่าย พวกเขากำลังตกแต่งด้วยm
ค่าตอบแทนพิเศษของฟังก์ชั่น
Benjamin Pierce กล่าวในTAPL
ระบบประเภทสามารถถือเป็นการคำนวณแบบคงที่ประมาณกับพฤติกรรมเวลาทำงานของคำในโปรแกรม
นั่นเป็นสาเหตุที่ภาษาที่มีระบบพิมพ์อันทรงพลังนั้นมีความหมายที่ชัดเจนกว่าภาษาที่พิมพ์ไม่ดี คุณสามารถคิดเกี่ยวกับพระในลักษณะเดียวกัน
ในฐานะที่เป็น @Carl และsigfpe point คุณสามารถจัดประเภทข้อมูลด้วยการดำเนินการทั้งหมดที่คุณต้องการโดยไม่ต้องหันไปหาพระมหากษัตริย์พิมพ์ดีดหรืออะไรก็ตามที่เป็นนามธรรม อย่างไรก็ตามพระช่วยให้คุณไม่เพียง แต่จะเขียนรหัสที่นำมาใช้ซ้ำได้ แต่ยังให้รายละเอียดที่ซ้ำซ้อนทั้งหมดอีกด้วย
ตัวอย่างเช่นสมมติว่าเราต้องการกรองรายการ วิธีที่ง่ายที่สุดคือการใช้filter
ฟังก์ชั่นซึ่งเท่ากับfilter (> 3) [1..10]
[4,5,6,7,8,9,10]
รุ่นที่ซับซ้อนกว่าเล็กน้อยfilter
ซึ่งผ่านตัวสะสมจากซ้ายไปขวาคือ
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
ในการรับทั้งหมดi
เช่นนั้นi <= 10, sum [1..i] > 4, sum [1..i] < 25
เราสามารถเขียน
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
[3,4,5,6]
ซึ่งเท่ากับ
หรือเราสามารถกำหนดnub
ฟังก์ชันที่ลบองค์ประกอบที่ซ้ำกันออกจากรายการในแง่ของfilterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
[1,2,4,5,3,8,9]
เท่ากับ รายการจะถูกส่งเป็นตัวสะสมที่นี่ รหัสใช้งานได้เพราะเป็นไปได้ที่จะออกจากรายการ monad ดังนั้นการคำนวณทั้งหมดจึงบริสุทธิ์ ( notElem
ไม่ได้ใช้>>=
จริง แต่ทำได้) อย่างไรก็ตามไม่สามารถออกจาก IO monad ได้อย่างปลอดภัย (เช่นคุณไม่สามารถเรียกใช้การกระทำของ IO และส่งกลับค่าบริสุทธิ์ - ค่าจะถูกห่อใน IO monad เสมอ) อีกตัวอย่างหนึ่งคืออาร์เรย์ที่ไม่แน่นอน: หลังจากคุณปล่อยให้ ST monad ซึ่งอาร์เรย์ที่ไม่แน่นอนอยู่ได้คุณจะไม่สามารถอัปเดตอาร์เรย์ในเวลาคงที่อีกต่อไป ดังนั้นเราจึงต้องการการกรองแบบ monadic จากControl.Monad
โมดูล:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
True
ดำเนินการการกระทำเอกสำหรับองค์ประกอบทั้งหมดจากรายการองค์ประกอบผลผลิตซึ่งเอกผลตอบแทนการกระทำ
ตัวอย่างการกรองด้วยอาเรย์:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
พิมพ์[1,2,4,5,3,8,9]
ตามที่คาดไว้
และรุ่นที่มี IO monad ซึ่งถามว่าองค์ประกอบใดที่จะส่งคืน:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
เช่น
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
และเป็นภาพประกอบขั้นสุดท้ายfilterAccum
สามารถกำหนดได้ในแง่ของfilterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
กับStateT
monad ที่ใช้ภายใต้ประทุนเป็นเพียงประเภทข้อมูลธรรมดา
ตัวอย่างนี้แสดงให้เห็นว่า monads ไม่เพียง แต่จะอนุญาตให้คุณบริบทการคำนวณเชิงนามธรรมและเขียนโค้ดที่นำมาใช้ใหม่ได้อย่างสะอาดตา (เนื่องจากความสามารถในการคอมโพสิตของ monads ตามที่ @Carl อธิบาย) แต่ยังรักษาประเภทข้อมูลที่ผู้ใช้กำหนดเอง
ฉันไม่คิดว่าIO
ควรถูกมองว่าเป็น Monad ที่โดดเด่นเป็นพิเศษ แต่เป็นหนึ่งในสิ่งที่น่าประหลาดใจสำหรับผู้เริ่มต้นดังนั้นฉันจะใช้มันเพื่ออธิบาย
ระบบ IO ที่เป็นไปได้ง่ายที่สุดสำหรับภาษาที่ใช้งานได้จริง (และอันที่จริง Haskell เริ่มต้นด้วย) คือ:
main₀ :: String -> String
main₀ _ = "Hello World"
ด้วยความขี้เกียจลายเซ็นง่าย ๆ นั้นก็เพียงพอที่จะสร้างโปรแกรมเทอร์มินัลเชิงโต้ตอบซึ่งมีข้อ จำกัดอย่างมาก ที่น่าผิดหวังที่สุดคือเราสามารถส่งออกข้อความเท่านั้น ถ้าเราเพิ่มความเป็นไปได้ของผลลัพธ์ที่น่าตื่นเต้นออกมา
data Output = TxtOutput String
| Beep Frequency
main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
น่ารัก แต่แน่นอนมากจริงมากขึ้น“เอาท์พุท Alterative” จะถูกเขียนไปยังแฟ้ม แต่คุณก็ต้องการวิธีอ่านจากไฟล์ มีโอกาสไหม?
เมื่อเราใช้main₁
โปรแกรมของเราและส่งไฟล์ไปยังกระบวนการ (โดยใช้ระบบปฏิบัติการ) เราได้ทำการอ่านไฟล์เป็นหลัก หากเราสามารถทริกเกอร์การอ่านไฟล์จากภายในภาษา Haskell ...
readFile :: Filepath -> (String -> [Output]) -> [Output]
สิ่งนี้จะใช้ "โปรแกรมแบบโต้ตอบ" String->[Output]
ป้อนสตริงที่ได้รับจากไฟล์และให้โปรแกรมที่ไม่ทำงานแบบโต้ตอบที่เรียกใช้งานไฟล์ที่กำหนด
มีปัญหาหนึ่งที่นี่: เราไม่ได้มีความคิดเมื่ออ่านไฟล์ [Output]
รายการแน่ใจว่าได้มีคำสั่งที่ดีกับผลแต่เราไม่ได้รับการสั่งซื้อเมื่อปัจจัยการผลิตที่จะทำ
การแก้ไข: ทำให้เหตุการณ์อินพุทเป็นรายการในรายการสิ่งที่ต้องทำ
data IO₀ = TxtOut String
| TxtIn (String -> [Output])
| FileWrite FilePath String
| FileRead FilePath (String -> [Output])
| Beep Double
main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
[TxtOutput "Hello World"]
]
ตกลงตอนนี้คุณอาจพบความไม่สมดุล: คุณสามารถอ่านไฟล์และสร้างผลลัพธ์ขึ้นอยู่กับมัน แต่คุณไม่สามารถใช้เนื้อหาไฟล์เพื่อตัดสินใจเช่นอ่านไฟล์อื่น ทางออกที่ชัดเจน: ทำให้ผลของการป้อนข้อมูลเหตุการณ์ที่เกิดขึ้นนอกจากนี้ยังมีบางสิ่งบางอย่างชนิดไม่เพียงIO
Output
แน่นอนว่ารวมถึงการส่งออกข้อความที่เรียบง่าย แต่ยังช่วยให้การอ่านไฟล์เพิ่มเติม ฯลฯ
data IO₁ = TxtOut String
| TxtIn (String -> [IO₁])
| FileWrite FilePath String
| FileRead FilePath (String -> [IO₁])
| Beep Double
main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
[TxtOut "Hello World"]
]
ตอนนี้จะช่วยให้คุณสามารถแสดงการดำเนินการไฟล์ใด ๆ ที่คุณอาจต้องการในโปรแกรม (แม้ว่าอาจจะไม่ได้ประสิทธิภาพที่ดี) แต่มันค่อนข้างซับซ้อน:
main₃
ให้รายชื่อของการกระทำทั้งหมด ทำไมเราไม่ใช้ลายเซ็น:: IO₁
ซึ่งเป็นกรณีพิเศษ?
รายการไม่ได้ให้ภาพรวมที่น่าเชื่อถือของโฟลว์ของโปรแกรมอีกต่อไป: การคำนวณที่ตามมาส่วนใหญ่จะเป็น "ประกาศ" เท่านั้นอันเป็นผลมาจากการดำเนินการอินพุตบางส่วน ดังนั้นเราอาจวางโครงสร้างรายการและเพียงแค่“ และทำ” กับการดำเนินการส่งออกแต่ละครั้ง
data IO₂ = TxtOut String IO₂
| TxtIn (String -> IO₂)
| Terminate
main₄ :: IO₂
main₄ = TxtIn $ \_ ->
TxtOut "Hello World"
Terminate
ก็ไม่เลวนะ!
ในทางปฏิบัติคุณไม่ต้องการใช้ตัวสร้างธรรมดาเพื่อกำหนดโปรแกรมทั้งหมดของคุณ จะต้องมีคู่ที่ดีของตัวสร้างพื้นฐานดังกล่าว แต่สำหรับสิ่งที่สูงกว่าส่วนใหญ่เราต้องการที่จะเขียนฟังก์ชั่นที่มีลายเซ็นระดับสูงที่ดี ปรากฎว่าสิ่งเหล่านี้ส่วนใหญ่จะมีลักษณะค่อนข้างคล้ายกัน: ยอมรับค่าที่พิมพ์อย่างมีความหมายบางอย่างและให้ผลการดำเนินการ IO ตามผลลัพธ์
getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂
มีรูปแบบที่เห็นได้ชัดที่นี่และเราควรเขียนว่า
type IO₃ a = (a -> IO₂) -> IO₂ -- If this reminds you of continuation-passing
-- style, you're right.
getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)
ตอนนี้เริ่มที่จะดูคุ้นเคย แต่เราก็ยังคงทำงานเฉพาะกับฟังก์ชั่นธรรมดาบางเบาภายใต้ประทุนและที่มีความเสี่ยง: "การกระทำที่คุ้มค่า" แต่ละคนมีความรับผิดชอบในการส่งผ่านการกระทำจริงของฟังก์ชั่นที่มีอยู่ โฟลว์การควบคุมของโปรแกรมทั้งหมดนั้นถูกหยุดชะงักโดยการกระทำที่ไม่ดีอย่างใดอย่างหนึ่งที่ตรงกลาง) เราควรทำให้ข้อกำหนดนั้นชัดเจนยิ่งขึ้น มันกลับกลายเป็นว่าเป็นกฎของ monadแต่ฉันไม่แน่ใจว่าเราสามารถกำหนดมันได้โดยไม่ต้องมีผู้ประกอบการมาตรฐาน / ผูกมัด
ไม่ว่าจะในอัตราใดก็ตามเรามาถึงการกำหนด IO ที่มีอินสแตนซ์ monad ที่เหมาะสม:
data IO₄ a = TxtOut String (IO₄ a)
| TxtIn (String -> IO₄ a)
| TerminateWith a
txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()
txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith
instance Functor IO₄ where
fmap f (TerminateWith a) = TerminateWith $ f a
fmap f (TxtIn g) = TxtIn $ fmap f . g
fmap f (TxtOut s c) = TxtOut s $ fmap f c
instance Applicative IO₄ where
pure = TerminateWith
(<*>) = ap
instance Monad IO₄ where
TerminateWith x >>= f = f x
TxtOut s c >>= f = TxtOut s $ c >>= f
TxtIn g >>= f = TxtIn $ (>>=f) . g
เห็นได้ชัดว่านี่ไม่ใช่การใช้งาน IO อย่างมีประสิทธิภาพ แต่เป็นหลักการที่ใช้งานได้จริง
IO3 a ≡ Cont IO2 a
@jdlugosz: แต่ฉันหมายถึงการแสดงความคิดเห็นเพิ่มเติมว่าเป็นพยักหน้าให้กับผู้ที่รู้อยู่แล้วว่า monad ต่อเนื่องเพราะมันไม่ได้มีชื่อเสียงว่าเป็นมิตรกับผู้เริ่มต้น
Monadsเป็นเพียงกรอบงานที่สะดวกในการแก้ปัญหาที่เกิดซ้ำ ก่อนอื่นพระต้องเป็นfunctors (เช่นต้องสนับสนุนการทำแผนที่โดยไม่ดูที่องค์ประกอบ (หรือประเภทของพวกเขา)) พวกเขายังต้องนำการดำเนินการที่มีผลผูกพัน (หรือการผูกมัด) และวิธีการสร้างค่า monadic จากประเภทองค์ประกอบ ( return
) ในที่สุดbind
และreturn
ต้องทำให้สมการสองสมการ (ตัวตนด้านซ้ายและขวา) หรือที่เรียกว่ากฎหมาย monad (อีกวิธีหนึ่งสามารถกำหนด monads ให้มีflattening operation
ผลผูกพันแทน)
monad รายการเป็นที่นิยมใช้ในการจัดการกับไม่ใช่ชะตา การดำเนินการผูกเลือกองค์ประกอบหนึ่งของรายการ (โดยสังเขปพวกมันทั้งหมดในโลกคู่ขนาน ) ทำให้โปรแกรมเมอร์ทำการคำนวณบางอย่างกับพวกเขาแล้วรวมผลลัพธ์ในโลกทั้งหมดไปยังรายการเดียว (โดยการต่อกันหรือแบนราบรายการซ้อน ) นี่คือวิธีที่เราจะนิยามฟังก์ชันการเปลี่ยนแปลงในกรอบ monadic ของ Haskell:
perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
let shortened = take index l ++ drop (index + 1) l
trailer <- perm shortened
return (leader : trailer)
นี่คือตัวอย่างการจำลองเซสชัน:
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
ควรสังเกตว่ารายการ monad นั้นไม่มีผลกระทบต่อการคำนวณ โครงสร้างทางคณิตศาสตร์ที่เป็น monad (เช่นการสอดคล้องกับอินเทอร์เฟซและกฎหมายที่กล่าวถึงข้างต้น) ไม่ได้บ่งบอกถึงผลข้างเคียงแม้ว่าปรากฏการณ์ที่เกิดผลข้างเคียงมักจะพอดีกับกรอบ monadic
Monads ทำหน้าที่เป็นพื้นฐานในการเขียนฟังก์ชั่นร่วมกันในห่วงโซ่ ระยะเวลา
ตอนนี้วิธีที่พวกเขาเขียนแตกต่างกันไปทั่วพระที่มีอยู่แล้วจึงส่งผลให้พฤติกรรมที่แตกต่างกัน (เช่นเพื่อจำลองสถานการณ์ที่ไม่แน่นอนในรัฐ monad)
ความสับสนเกี่ยวกับ monads นั้นเป็นเรื่องทั่วไปนั่นคือกลไกในการเขียนฟังก์ชั่นพวกเขาสามารถนำมาใช้หลายสิ่งหลายอย่างทำให้ผู้คนเชื่อว่า monads เกี่ยวกับรัฐเกี่ยวกับ IO ฯลฯ เมื่อพวกเขาเพียงเกี่ยวกับ "
ตอนนี้สิ่งหนึ่งที่น่าสนใจเกี่ยวกับพระคือผลลัพธ์ของการแต่งเพลงนั้นเป็นประเภท "M a" เสมอนั่นคือค่าภายในซองจดหมายที่ติดแท็กด้วย "M" คุณลักษณะนี้เป็นสิ่งที่ดีที่จะนำมาใช้จริงตัวอย่างเช่นการแยกที่ชัดเจนระหว่างบริสุทธิ์จากรหัสไม่บริสุทธิ์: ประกาศการกระทำที่ไม่บริสุทธิ์ทั้งหมดในรูปแบบฟังก์ชั่นประเภท "IO a" และไม่มีฟังก์ชั่นเมื่อกำหนด IO monad ค่า "จากภายใน" IO a " ผลที่ได้คือฟังก์ชั่นไม่สามารถบริสุทธิ์และในเวลาเดียวกันเอาค่าจาก "IO a" เพราะไม่มีวิธีที่จะใช้ค่าดังกล่าวในขณะที่อยู่ที่บริสุทธิ์ (ฟังก์ชั่นจะต้องอยู่ภายใน "IO" monad ที่จะใช้ ค่าดังกล่าว) (หมายเหตุ: ดีไม่มีอะไรสมบูรณ์แบบดังนั้น "IO พระที่นั่ง" สามารถถูกทำลายโดยใช้ "unsafePerformIO: IO a -> a"
คุณจำเป็นต้องมีพระถ้าคุณมีตัวสร้างประเภทและฟังก์ชั่นที่ส่งกลับค่าของตระกูลประเภทนั้น ในที่สุดคุณต้องการที่จะรวมชนิดของฟังก์ชั่นเหล่านี้ร่วมกัน เหล่านี้เป็นสามองค์ประกอบที่สำคัญที่จะตอบว่าทำไม
ให้ฉันทำอย่างละเอียด คุณมีInt
, String
และReal
และหน้าที่ของชนิดInt -> String
, String -> Real
และอื่น ๆ Int -> Real
คุณสามารถรวมฟังก์ชั่นเหล่านี้ได้อย่างง่ายดายที่ลงท้ายด้วย ชีวิตเป็นสิ่งที่ดี.
แล้ววันหนึ่งคุณจะต้องสร้างใหม่ครอบครัวประเภท อาจเป็นเพราะคุณต้องพิจารณาถึงความเป็นไปได้ที่จะไม่คืนค่า ( Maybe
) ส่งคืนข้อผิดพลาด ( Either
) ผลการค้นหาหลายรายการ ( List
) และอื่น ๆ
ขอให้สังเกตว่าMaybe
เป็นตัวสร้างประเภท มันต้องใช้เวลาชนิดเช่นและผลตอบแทนรูปแบบใหม่Int
Maybe Int
สิ่งแรกที่ต้องจดจำไม่มีตัวสร้างประเภทไม่มี monad
แน่นอนว่าคุณต้องการใช้ตัวสร้างประเภทของคุณในรหัสของคุณและในไม่ช้าคุณจะจบลงด้วยการทำงานเช่นการและInt -> Maybe String
String -> Maybe Float
ตอนนี้คุณไม่สามารถรวมฟังก์ชั่นของคุณได้อย่างง่ายดาย ชีวิตไม่ดีอีกต่อไป
และนี่คือตอนที่พระมาช่วย ช่วยให้คุณสามารถรวมฟังก์ชั่นประเภทนั้นอีกครั้ง คุณเพียงแค่ต้องเปลี่ยนองค์ประกอบ สำหรับ> ==