ทำไมเราต้องการพระ


366

ในความเห็นต่ำต้อยของฉันคำตอบสำหรับคำถามที่มีชื่อเสียง"Monad คืออะไร" โดยเฉพาะคนที่ได้รับการโหวตมากที่สุดพยายามที่จะอธิบายสิ่งที่เป็น monad อย่างชัดเจนโดยไม่ต้องอธิบายว่าทำไม monads มีความจำเป็นจริงๆ พวกเขาสามารถอธิบายได้ว่าเป็นวิธีแก้ปัญหาหรือไม่




4
คุณทำวิจัยอะไรบ้าง คุณมองไปที่ไหน คุณพบทรัพยากรอะไร เราคาดหวังว่าคุณจะทำจำนวนเงินที่สำคัญของการวิจัยก่อนที่จะถามและแสดงให้เราเห็นในคำถามสิ่งที่วิจัยที่คุณได้ทำ มีแหล่งข้อมูลมากมายที่พยายามอธิบายแรงจูงใจสำหรับทรัพยากร - หากคุณไม่พบสิ่งใดเลยคุณอาจต้องทำการวิจัยเพิ่มเติมอีกเล็กน้อย หากคุณพบบางอย่าง แต่พวกเขาไม่ได้ช่วยคุณมันจะทำให้เป็นคำถามที่ดีกว่านี้ถ้าคุณอธิบายสิ่งที่คุณพบและทำไมพวกเขาถึงไม่ได้ผลสำหรับคุณโดยเฉพาะ
DW

8
นี่เป็นแบบที่ดียิ่งขึ้นสำหรับโปรแกรมเมอร์และไม่เหมาะสำหรับ StackOverflow ฉันจะลงคะแนนเพื่อย้ายถ้าทำได้ แต่ฉันทำไม่ได้ = (
jpmc26

3
@ jpmc26 เป็นไปได้มากว่ามันจะถูกปิดเพราะ "โดยพื้นฐานความคิดเห็น" ที่นี่อย่างน้อยก็มีโอกาส (ตามที่แสดงโดย upvotes จำนวนมากเปิดใหม่อย่างรวดเร็วเมื่อวานนี้และยังไม่มีการโหวตอย่างใกล้ชิด)
Izkata

คำตอบ:


580

ทำไมเราต้องการพระ

  1. เราต้องการตั้งโปรแกรมโดยใช้ฟังก์ชั่นเท่านั้น ("ฟังก์ชั่นโปรแกรม (FP)" หลังจากทั้งหมด)
  2. จากนั้นเรามีปัญหาใหญ่ครั้งแรก นี่คือโปรแกรม:

    f(x) = 2 * x

    g(x,y) = x / y

    เราจะบอกได้อย่างไรว่า อะไรที่ต้องถูกประหารชีวิตก่อน ? เราจะสร้างลำดับของฟังก์ชั่นที่สั่ง (เช่นโปรแกรม ) โดยใช้ไม่เกินฟังก์ชั่นได้อย่างไร

    การแก้ไข: ฟังก์ชั่นการเขียน ถ้าคุณต้องการที่แรกgและจากนั้นเพียงแค่เขียนf f(g(x,y))ด้วยวิธีนี้ "โปรแกรม" เป็นฟังก์ชั่นเช่นกัน: main = f(g(x,y)). ได้. แต่ ...

  3. ปัญหาเพิ่มเติม: ฟังก์ชั่นบางอย่างอาจล้มเหลว (เช่นg(2,0)หารด้วย 0) เราไม่มี "ข้อยกเว้น"ใน FP (ข้อยกเว้นไม่ใช่ฟังก์ชัน) เราจะแก้ปัญหาได้อย่างไร

    วิธีแก้ปัญหา: มา อนุญาตให้ฟังก์ชั่นคืนสองสิ่ง : แทนที่จะมีg : Real,Real -> Real(ฟังก์ชั่นจากสอง reals เป็นของจริง), อนุญาตให้g : Real,Real -> Real | Nothing(ฟังก์ชั่นจากสอง reals เป็น (จริงหรือไม่มีอะไร))

  4. แต่ฟังก์ชั่นควร (จะง่าย) ผลตอบแทนเพียงสิ่งหนึ่งที่

    การแก้ไข: เรามาสร้างข้อมูลประเภทใหม่ที่จะส่งคืน "มวยชนิด " ที่ล้อมรอบอาจเป็นของจริงหรือไม่มีอะไรเลย g : Real,Real -> Maybe Realดังนั้นเราสามารถมี ได้. แต่ ...

  5. สิ่งที่เกิดขึ้นในขณะนี้เพื่อf(g(x,y))?fMaybe Realไม่พร้อมที่จะกิน และเราไม่ต้องการที่จะเปลี่ยนฟังก์ชั่นที่เราสามารถเชื่อมต่อกับทุกการกินgMaybe Real

    การแก้ไข: ขอมีฟังก์ชั่นพิเศษเพื่อ "การเชื่อมต่อ" / "เขียน" / "ลิงก์" ฟังก์ชั่น ด้วยวิธีนี้เราสามารถปรับเอาท์พุทของฟังก์ชั่นหนึ่งเพื่อเลี้ยงสิ่งต่อไปนี้

    ในกรณีของเรา: g >>= f(เชื่อมต่อ / เขียนgถึงf) เราต้องการ>>=ที่จะได้รับgของที่ส่งออกตรวจสอบได้และในกรณีที่มันเป็นNothingเพียงแค่ไม่ได้โทรfและผลตอบแทนNothing; หรือในทางกลับกันให้แยกกล่องบรรจุRealแล้วป้อนเข้าfด้วย (อัลกอริทึมนี้เป็นเพียงการดำเนินการ>>=ตามMaybeประเภท) นอกจากนี้โปรดทราบว่า>>=จะต้องเขียนเพียงครั้งเดียวต่อ "ประเภทมวย" (กล่องที่แตกต่างกันขั้นตอนวิธีการปรับตัวที่แตกต่างกัน)

  6. ปัญหาอื่น ๆ อีกมากมายที่เกิดขึ้นซึ่งสามารถแก้ไขได้โดยใช้รูปแบบเดียวกันนี้: 1. ใช้ "กล่อง" เพื่อประมวล / เก็บความหมาย / ค่าต่าง ๆ และมีฟังก์ชั่นเช่นgนั้นคืนค่าเหล่านั้น 2. มีนักแต่งเพลง / ลิงเกอร์g >>= fเพื่อช่วยเชื่อมต่อgเอาท์พุทfของอินพุตดังนั้นเราจึงไม่จำเป็นต้องเปลี่ยนแปลงใด ๆfเลย

  7. ปัญหาที่น่าสังเกตที่สามารถแก้ไขได้โดยใช้เทคนิคนี้คือ:

    • มีสถานะโกลบอลที่ทุกฟังก์ชั่นในลำดับของฟังก์ชั่น ("โปรแกรม") สามารถแชร์: solution StateMonadวิธีการแก้ปัญหา

    • เราไม่ชอบ "ฟังก์ชั่นที่ไม่บริสุทธิ์": ฟังก์ชั่นที่ให้ผลลัพธ์ที่แตกต่างกันสำหรับอินพุตเดียวกัน ดังนั้นให้ทำเครื่องหมายฟังก์ชั่นเหล่านั้นทำให้พวกเขากลับค่าที่ติดแท็ก / กล่อง: IOmonad

ความสุขทั้งหมด!


64
@Carl โปรดเขียนคำตอบที่ดีกว่าเพื่อให้ความกระจ่างแก่เรา
XrXr

15
@Carl ฉันคิดว่ามันชัดเจนในคำตอบว่ามีปัญหามากมายที่ได้รับประโยชน์จากรูปแบบนี้ (จุดที่ 6) และIOmonad นั้นเป็นอีกปัญหาหนึ่งในรายการIO(จุดที่ 7) ในทางกลับกันIOจะปรากฏเพียงครั้งเดียวและท้ายที่สุดดังนั้นอย่าเข้าใจ "เวลาส่วนใหญ่ที่พูดถึง ... เกี่ยวกับ IO"
cibercitizen1

4
ความเข้าใจผิดที่ยิ่งใหญ่เกี่ยวกับพระ: พระเกี่ยวกับรัฐ; monads เกี่ยวกับการจัดการข้อยกเว้น; ไม่มีวิธีที่จะใช้ IO ใน FPL บริสุทธิ์โดยไม่ต้อง monads; monads นั้นไม่คลุมเครือ (contrargument is Either) คำตอบส่วนใหญ่เกี่ยวกับ "เราต้องใช้ functors ทำไม"
vlastachu

4
"6. 2. มีผู้แต่ง / ผู้เชื่อมโยงg >>= fเพื่อช่วยเชื่อมต่อgเอาต์พุตfของอินพุตกับอินพุตดังนั้นเราจึงไม่จำเป็นต้องเปลี่ยนแปลงอะไรfเลย" มันไม่ถูกต้องเลย ก่อนในf(g(x,y)), fสามารถผลิตอะไร f:: Real -> Stringมันอาจจะเป็น ด้วย "การจัดองค์ประกอบแบบ monadic" จะต้องเปลี่ยนเป็นผลิตMaybe Stringมิฉะนั้นประเภทจะไม่พอดี ยิ่งกว่านั้น>>=ตัวมันเองก็ไม่พอดี !! มันคือ>=>องค์ประกอบนี้ใช่>>=ไหม ดูการสนทนากับ dfeuer ภายใต้คำตอบของ Carl
Will Ness

3
คำตอบของคุณถูกต้องในแง่ที่ว่า IMO ของพระมหากษัตริย์นั้นถูกอธิบายไว้อย่างดีที่สุดว่าเป็นเรื่ององค์ประกอบ / ality ของ "ฟังก์ชั่น" (ลูกศรของ Kleisli จริงๆ) แต่รายละเอียดที่แม่นยำของสิ่งที่ทำให้พวกมันเป็น คุณสามารถวางสายในลักษณะต่าง ๆ (เช่น Functor ฯลฯ ) นี้โดยเฉพาะวิธีการเดินสายไฟเข้าด้วยกันเป็นสิ่งที่กำหนดว่า "monad"
Will Ness

219

คำตอบคือแน่นอน"เราทำไม่ได้" เช่นเดียวกับนามธรรมทั้งหมดไม่จำเป็น

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อื่น พวกเขาทำงานกับทุกประเภทที่สามารถรวบรวมหมวดหมู่ที่เหมาะสมตามกฎหมายหมวดหมู่

  1. ตัวตนด้านซ้าย: id . f=f
  2. ตัวตนที่ถูกต้อง: f . id=f
  3. การเชื่อมโยง: f . (g . h)=(f . g) . h

ตราบใดที่คุณสามารถพิสูจน์ได้ว่าประเภทของคุณเป็นไปตามกฎหมายทั้งสามนี้คุณสามารถเปลี่ยนเป็นหมวดหมู่ Kleisli ได้ และเรื่องใหญ่เกี่ยวกับเรื่องนี้คืออะไร? มันกลับกลายเป็นว่าพระสงฆ์เป็นสิ่งเดียวกันกับหมวดหมู่ Kleisli Monad's returnเป็นเช่นเดียวกับ idKleisli Monad's (>>=)ไม่เหมือนกับ Kleisli (.)แต่มันจะออกมาเป็นเรื่องง่ายมากที่จะเขียนในแต่ละแง่ของคนอื่น ๆ และหมวดหมู่ของกฎหมายก็เหมือนกับกฎของ monad เมื่อคุณแปลความแตกต่างระหว่าง(>>=)และ(.)และ

แล้วทำไมต้องผ่านสิ่งเหล่านี้ไป? ทำไมต้องมีMonadนามธรรม? ดังที่ฉันได้กล่าวถึงข้างต้นจะช่วยให้สามารถใช้รหัสซ้ำได้ มันยังช่วยให้ใช้ซ้ำรหัสตามสองมิติที่แตกต่างกัน

มิติแรกของการใช้รหัสซ้ำมาจากการปรากฏตัวของสิ่งที่เป็นนามธรรม คุณสามารถเขียนโค้ดที่ใช้ได้กับทุก ๆ สิ่งที่เป็นนามธรรม มีทั้งเป็นmonad ห่วงMonadแพคเกจประกอบด้วยลูปที่ทำงานร่วมกับอินสแตนซ์ใด ๆ

มิติที่สองเป็นทางอ้อม แต่มันเกิดขึ้นจากการมีอยู่ขององค์ประกอบ เมื่อการเรียบเรียงเป็นเรื่องง่ายการเขียนโค้ดเป็นชิ้นเล็ก ๆ นี่เป็นวิธีเดียวกับที่(.)ผู้ปฏิบัติงานใช้สำหรับฟังก์ชั่นสนับสนุนการเขียนฟังก์ชั่นขนาดเล็กและใช้ซ้ำได้

เหตุใดจึงมีนามธรรม เพราะมันได้รับการพิสูจน์แล้วว่าเป็นเครื่องมือที่ช่วยให้สามารถเขียนโค้ดได้มากขึ้นส่งผลให้สามารถสร้างรหัสที่สามารถใช้ซ้ำได้และกระตุ้นให้มีการสร้างโค้ดที่นำกลับมาใช้ใหม่ได้มากขึ้น การใช้รหัสซ้ำเป็นหนึ่งในโปรแกรมที่ศักดิ์สิทธิ์ สิ่งที่เป็นนามธรรมอยู่เพราะมันเคลื่อนเราไปสู่จอกศักดิ์สิทธิ์นั้นเล็กน้อย


2
คุณสามารถอธิบายความสัมพันธ์ระหว่างหมวดหมู่ทั่วไปกับหมวดหมู่ Kleisli ได้หรือไม่ กฎหมายสามข้อที่คุณอธิบายว่ามีอยู่ในหมวดหมู่ใด ๆ
dfeuer

1
@dfeuer โอ้ ใส่ไว้ในรหัสnewtype Kleisli m a b = Kleisli (a -> m b). ประเภท Kleisli มีฟังก์ชั่นที่เด็ดขาดชนิดกลับ ( bในกรณีนี้) mเป็นอาร์กิวเมนต์เพื่อนวกรรมิกประเภท Iff Kleisli mเป็นหมวดหมู่mMonad
Carl

1
ประเภทการคืนสินค้าอย่างแน่นอนคืออะไร? Kleisli mดูเหมือนว่าจะในรูปแบบหมวดหมู่นี้มีวัตถุประเภท Haskell และเช่นที่ลูกศรจากaที่จะbมีฟังก์ชั่นจากaไปm bด้วยและid = return (.) = (<=<)มันเกี่ยวกับความถูกต้องหรือฉันกำลังผสมสิ่งต่าง ๆ หรืออะไรบางอย่างเข้าด้วยกัน?
dfeuer

1
@dfeuer ถูกต้องแล้ว วัตถุทุกประเภทและ morphisms อยู่ระหว่างประเภทaและbแต่ไม่ใช่ฟังก์ชันที่ง่าย พวกเขากำลังตกแต่งด้วยmค่าตอบแทนพิเศษของฟังก์ชั่น
คาร์ล

1
จำเป็นต้องใช้คำศัพท์ทฤษฎีหมวดหมู่หรือไม่? บางทีแฮสเค็ลล์อาจจะง่ายกว่านี้ถ้าคุณเปลี่ยนประเภทเป็นรูปภาพโดยที่ชนิดนั้นจะเป็น DNA สำหรับการวาดรูป (ขึ้นอยู่กับประเภท *) จากนั้นคุณใช้รูปภาพเพื่อเขียนโปรแกรมของคุณด้วยชื่อที่เป็นตัวอักษรทับทิมขนาดเล็ก เหนือไอคอน
aoeu256

24

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)

filterMTrueดำเนินการการกระทำเอกสำหรับองค์ประกอบทั้งหมดจากรายการองค์ประกอบผลผลิตซึ่งเอกผลตอบแทนการกระทำ

ตัวอย่างการกรองด้วยอาเรย์:

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

กับStateTmonad ที่ใช้ภายใต้ประทุนเป็นเพียงประเภทข้อมูลธรรมดา

ตัวอย่างนี้แสดงให้เห็นว่า monads ไม่เพียง แต่จะอนุญาตให้คุณบริบทการคำนวณเชิงนามธรรมและเขียนโค้ดที่นำมาใช้ใหม่ได้อย่างสะอาดตา (เนื่องจากความสามารถในการคอมโพสิตของ monads ตามที่ @Carl อธิบาย) แต่ยังรักษาประเภทข้อมูลที่ผู้ใช้กำหนดเอง


1
คำตอบนี้อธิบายว่าทำไมเราต้องพิมพ์โมนาล Monad วิธีที่ดีที่สุดที่จะเข้าใจว่าทำไมเราต้อง monads และไม่ใช่สิ่งอื่นใดคือการอ่านเกี่ยวกับความแตกต่างระหว่าง monads และ functors applicative: หนึ่ง , สอง
user3237465

20

ฉันไม่คิดว่าIOควรถูกมองว่าเป็น Monad ที่โดดเด่นเป็นพิเศษ แต่เป็นหนึ่งในสิ่งที่น่าประหลาดใจสำหรับผู้เริ่มต้นดังนั้นฉันจะใช้มันเพื่ออธิบาย

การสร้างระบบ IO สำหรับ Haskell

ระบบ 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

ก็ไม่เลวนะ!

แล้วทั้งหมดนี้เกี่ยวข้องอะไรกับ monads บ้าง?

ในทางปฏิบัติคุณไม่ต้องการใช้ตัวสร้างธรรมดาเพื่อกำหนดโปรแกรมทั้งหมดของคุณ จะต้องมีคู่ที่ดีของตัวสร้างพื้นฐานดังกล่าว แต่สำหรับสิ่งที่สูงกว่าส่วนใหญ่เราต้องการที่จะเขียนฟังก์ชั่นที่มีลายเซ็นระดับสูงที่ดี ปรากฎว่าสิ่งเหล่านี้ส่วนใหญ่จะมีลักษณะค่อนข้างคล้ายกัน: ยอมรับค่าที่พิมพ์อย่างมีความหมายบางอย่างและให้ผลการดำเนินการ 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 ต่อเนื่องเพราะมันไม่ได้มีชื่อเสียงว่าเป็นมิตรกับผู้เริ่มต้น
leftaroundabout

4

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


3

Monads ทำหน้าที่เป็นพื้นฐานในการเขียนฟังก์ชั่นร่วมกันในห่วงโซ่ ระยะเวลา

ตอนนี้วิธีที่พวกเขาเขียนแตกต่างกันไปทั่วพระที่มีอยู่แล้วจึงส่งผลให้พฤติกรรมที่แตกต่างกัน (เช่นเพื่อจำลองสถานการณ์ที่ไม่แน่นอนในรัฐ monad)

ความสับสนเกี่ยวกับ monads นั้นเป็นเรื่องทั่วไปนั่นคือกลไกในการเขียนฟังก์ชั่นพวกเขาสามารถนำมาใช้หลายสิ่งหลายอย่างทำให้ผู้คนเชื่อว่า monads เกี่ยวกับรัฐเกี่ยวกับ IO ฯลฯ เมื่อพวกเขาเพียงเกี่ยวกับ "

ตอนนี้สิ่งหนึ่งที่น่าสนใจเกี่ยวกับพระคือผลลัพธ์ของการแต่งเพลงนั้นเป็นประเภท "M a" เสมอนั่นคือค่าภายในซองจดหมายที่ติดแท็กด้วย "M" คุณลักษณะนี้เป็นสิ่งที่ดีที่จะนำมาใช้จริงตัวอย่างเช่นการแยกที่ชัดเจนระหว่างบริสุทธิ์จากรหัสไม่บริสุทธิ์: ประกาศการกระทำที่ไม่บริสุทธิ์ทั้งหมดในรูปแบบฟังก์ชั่นประเภท "IO a" และไม่มีฟังก์ชั่นเมื่อกำหนด IO monad ค่า "จากภายใน" IO a " ผลที่ได้คือฟังก์ชั่นไม่สามารถบริสุทธิ์และในเวลาเดียวกันเอาค่าจาก "IO a" เพราะไม่มีวิธีที่จะใช้ค่าดังกล่าวในขณะที่อยู่ที่บริสุทธิ์ (ฟังก์ชั่นจะต้องอยู่ภายใน "IO" monad ที่จะใช้ ค่าดังกล่าว) (หมายเหตุ: ดีไม่มีอะไรสมบูรณ์แบบดังนั้น "IO พระที่นั่ง" สามารถถูกทำลายโดยใช้ "unsafePerformIO: IO a -> a"


2

คุณจำเป็นต้องมีพระถ้าคุณมีตัวสร้างประเภทและฟังก์ชั่นที่ส่งกลับค่าของตระกูลประเภทนั้น ในที่สุดคุณต้องการที่จะรวมชนิดของฟังก์ชั่นเหล่านี้ร่วมกัน เหล่านี้เป็นสามองค์ประกอบที่สำคัญที่จะตอบว่าทำไม

ให้ฉันทำอย่างละเอียด คุณมีInt, StringและRealและหน้าที่ของชนิดInt -> String, String -> Realและอื่น ๆ Int -> Realคุณสามารถรวมฟังก์ชั่นเหล่านี้ได้อย่างง่ายดายที่ลงท้ายด้วย ชีวิตเป็นสิ่งที่ดี.

แล้ววันหนึ่งคุณจะต้องสร้างใหม่ครอบครัวประเภท อาจเป็นเพราะคุณต้องพิจารณาถึงความเป็นไปได้ที่จะไม่คืนค่า ( Maybe) ส่งคืนข้อผิดพลาด ( Either) ผลการค้นหาหลายรายการ ( List) และอื่น ๆ

ขอให้สังเกตว่าMaybeเป็นตัวสร้างประเภท มันต้องใช้เวลาชนิดเช่นและผลตอบแทนรูปแบบใหม่Int Maybe Intสิ่งแรกที่ต้องจดจำไม่มีตัวสร้างประเภทไม่มี monad

แน่นอนว่าคุณต้องการใช้ตัวสร้างประเภทของคุณในรหัสของคุณและในไม่ช้าคุณจะจบลงด้วยการทำงานเช่นการและInt -> Maybe String String -> Maybe Floatตอนนี้คุณไม่สามารถรวมฟังก์ชั่นของคุณได้อย่างง่ายดาย ชีวิตไม่ดีอีกต่อไป

และนี่คือตอนที่พระมาช่วย ช่วยให้คุณสามารถรวมฟังก์ชั่นประเภทนั้นอีกครั้ง คุณเพียงแค่ต้องเปลี่ยนองค์ประกอบ สำหรับ> ==


2
สิ่งนี้ไม่เกี่ยวกับตระกูลประเภท คุณกำลังพูดเรื่องอะไร
dfeuer
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.