วิธีต่าง ๆ ในการดู monad


29

ในขณะที่เรียนรู้ Haskell ฉันได้พบกับบทเรียนมากมายที่พยายามอธิบายสิ่งที่ monads และทำไม monads มีความสำคัญใน Haskell แต่ละคนใช้การเปรียบเทียบดังนั้นการจับความหมายได้ง่ายขึ้น ในตอนท้ายของวันฉันได้จบลงด้วยมุมมองที่แตกต่างกัน 3 แบบว่า monad คืออะไร:

ดู 1: Monad เป็นป้ายกำกับ

บางครั้งฉันคิดว่า monad เป็นป้ายชื่อประเภทเฉพาะ ตัวอย่างเช่นฟังก์ชั่นของประเภท:

myfunction :: IO Int

myfunction เป็นฟังก์ชันที่เมื่อใดก็ตามที่มีการดำเนินการมันจะให้ค่า Int ชนิดของผลลัพธ์ไม่ใช่ Int แต่เป็น IO Int ดังนั้น IO จึงเป็นเลเบลของค่า Int เตือนผู้ใช้ให้รู้ว่าค่า Int เป็นผลลัพธ์ของกระบวนการที่ได้ทำการกระทำของ IO

ดังนั้นค่า Int นี้จึงถูกทำเครื่องหมายเป็นค่าที่มาจากกระบวนการที่มี IO ดังนั้นค่านี้จึงเป็น "สกปรก" กระบวนการของคุณไม่บริสุทธิ์อีกต่อไป

ดู 2: Monad เป็นพื้นที่ส่วนตัวที่สามารถเกิดสิ่งที่น่ารังเกียจได้

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

ดู 3: Monad ในทฤษฎีหมวดหมู่

นี่คือมุมมองที่ฉันไม่เข้าใจ Monad เป็นเพียงนักแสดงในประเภทเดียวกันหรือหมวดหมู่ย่อย ตัวอย่างเช่นคุณมีค่า Int และเป็นประเภทย่อย IO Int ที่เป็นค่า Int ที่สร้างขึ้นหลังจากกระบวนการ IO

มุมมองเหล่านี้ถูกต้องหรือไม่ ซึ่งมีความแม่นยำมากขึ้น?


5
# 2 ไม่ใช่สิ่งที่ monad ทั่วไป ในความเป็นจริงมันค่อนข้าง จำกัด IO และไม่ใช่มุมมองที่เป็นประโยชน์ (เปรียบเทียบอะไร Monad ไม่ได้ ) นอกจากนี้ "เข้มงวด" มักถูกนำมาใช้เพื่อตั้งชื่อสถานที่ให้บริการ Haskell ไม่ได้ครอบครอง (คือการประเมินผลที่เข้มงวด) โดยวิธีการ Monads ไม่เปลี่ยนแปลงที่ทั้ง (อีกครั้งดูสิ่งที่ Monad ไม่ได้)

3
ในทางเทคนิคมีเพียงหนึ่งในสามที่ถูกต้อง Monad เป็น endofunctor สำหรับการดำเนินการพิเศษซึ่งกำหนดไว้ - การเลื่อนตำแหน่งและการผูกมัด Monads มีมากมาย - รายการ monad เป็นตัวอย่างที่สมบูรณ์แบบที่จะได้รับสัญชาตญาณหลัง monads สิ่งอำนวยความสะดวกในการอ่านจะดียิ่งขึ้น น่าประหลาดใจที่พอพระใช้งานได้เป็นเครื่องมือในการพูดสถานะในภาษาที่ใช้งานได้โดยปริยาย นี่ไม่ใช่คุณสมบัติที่กำหนดของพระ: มันเป็นเรื่องบังเอิญ, การทำเกลียวของรัฐอาจนำมาใช้ในเงื่อนไขของพวกเขา เช่นเดียวกับ IO
permeakra

Common LISP มีคอมไพเลอร์ของตัวเองเป็นส่วนหนึ่งของภาษา Haskell มี Monads
Will Ness

คำตอบ:


33

จำนวนการดู # 1 และ # 2 ไม่ถูกต้อง

  1. ประเภทข้อมูลใด ๆ* -> *สามารถทำงานเป็นป้ายกำกับพระได้มากกว่านั้น
  2. (ยกเว้นของIOmonad) การคำนวณภายใน monad นั้นไม่บริสุทธิ์ มันเป็นเพียงการคำนวณที่เราเห็นว่ามีผลข้างเคียง แต่บริสุทธิ์

ความเข้าใจผิดทั้งสองนี้มาจากการมุ่งเน้นไปที่IOmonad ซึ่งจริงๆแล้วค่อนข้างพิเศษ

ฉันจะพยายามอธิบายอย่างละเอียดในข้อที่ 3 เล็กน้อยโดยไม่ต้องคำนึงถึงทฤษฎีหมวดหมู่หากเป็นไปได้


การคำนวณมาตรฐาน

f :: a -> bการคำนวณทั้งหมดในภาษาเขียนโปรแกรมการทำงานสามารถดูเป็นฟังก์ชั่นที่มีประเภทแหล่งที่มาและประเภทเป้าหมาย: หากฟังก์ชั่นมีมากกว่าหนึ่งอาร์กิวเมนต์เราสามารถแปลงมันเป็นฟังก์ชั่นหนึ่งอาร์กิวเมนต์โดยการปิดกั้น (ดูHaskell wiki ) และถ้าเรามีเพียงค่าx :: a(ฟังก์ชั่นที่มี 0 อาร์กิวเมนต์) เราสามารถแปลงเป็นฟังก์ชันที่รับอาร์กิวเมนต์ของหน่วยประเภท :(\_ -> x) :: () -> a :

เราสามารถสร้างโปรแกรมที่ซับซ้อนมากขึ้นในรูปแบบที่ง่ายขึ้นโดยการเขียนฟังก์ชั่นดังกล่าวโดยใช้.โอเปอเรเตอร์ ตัวอย่างเช่นถ้าเรามีf :: a -> bและเราได้รับg :: b -> c g . f :: a -> cโปรดทราบว่างานนี้ได้ค่าแปลงของเรามากเกินไป: ถ้าเรามีและแปลงเป็นตัวแทนของเราที่เราได้รับx :: af . ((\_ -> x) :: () -> a) :: () -> b

การเป็นตัวแทนนี้มีคุณสมบัติที่สำคัญมาก ได้แก่ :

  • เรามีฟังก์ชั่นพิเศษมาก - ตัวตนของฟังก์ชั่นสำหรับแต่ละประเภทid :: a -> a aมันเป็นเอกลักษณ์องค์ประกอบ ที่เกี่ยวกับ.: fเท่ากับทั้งสองและf . idid . f
  • ผู้ประกอบการองค์ประกอบการทำงาน.เป็นแบบเชื่อมโยง

การคำนวณแบบ monadic

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

ตอนนี้เราอยากจะทำงานกับฟังก์ชั่นในหมวดหมู่ดังกล่าวในลักษณะเดียวกันกับกรณีพื้นฐาน เราต้องการให้สามารถเขียนฟังก์ชั่นเหล่านี้ได้เราต้องการให้องค์ประกอบมีความสัมพันธ์และเราต้องการมีเอกลักษณ์ พวกเราต้องการ:

  • จะมีผู้ประกอบการ (ขอเรียกว่า<=<) ที่ประกอบด้วยฟังก์ชั่นf :: a -> m bและเป็นสิ่งที่เป็นg :: b -> m c g <=< f :: a -> m cและจะต้องมีการเชื่อมโยง
  • จะมีฟังก์ชั่นบางตัวตนแต่ละประเภท, returnขอเรียกว่า นอกจากนี้เรายังต้องการที่f <=< returnเป็นเช่นเดียวกับและเช่นเดียวกับfreturn <=< f

ใด ๆm :: * -> *ที่เรามีฟังก์ชั่นดังกล่าวreturnและ<=<เรียกว่าmonad มันช่วยให้เราสร้างการคำนวณที่ซับซ้อนจากคนที่เรียบง่ายเช่นเดียวกับในกรณีพื้นฐาน แต่ตอนนี้ประเภทของค่าผลตอบแทนจะ tranformed mโดย

(อันที่จริงฉันใช้คำว่าหมวดหมู่เล็กน้อยในทางที่ผิดในความหมายของหมวดหมู่ทฤษฎีเราสามารถเรียกหมวดหมู่การก่อสร้างของเราได้หลังจากที่เรารู้ว่ามันเป็นไปตามกฎหมายเหล่านี้)

Monads ใน Haskell

ใน Haskell (และภาษาการทำงานอื่น ๆ ) () -> aเราส่วนใหญ่ทำงานกับค่าไม่ได้กับฟังก์ชั่นประเภท ดังนั้นแทนที่จะกำหนด<=<สำหรับแต่ละ monad (>>=) :: m a -> (a -> m b) -> m bเรากำหนดฟังก์ชั่น คำจำกัดความทางเลือกดังกล่าวเทียบเท่าเราสามารถแสดงการ>>=ใช้<=<และในทางกลับกัน (ลองออกกำลังกายหรือดูแหล่งที่มา ) หลักการคือตอนนี้ชัดเจนน้อยลง แต่ก็ยังคงเหมือนเดิม: ผลของเราอยู่เสมอประเภทและเราเขียนฟังก์ชั่นประเภทm aa -> m b

สำหรับแต่ละ monad ที่เราสร้างเราต้องไม่ลืมที่จะตรวจสอบreturnและ<=<มีคุณสมบัติที่เราต้องการ: การเชื่อมโยงและตัวตนซ้าย / ขวา ใช้แสดงreturnและ>>=พวกเขาจะเรียกว่ากฎหมาย monad

ตัวอย่าง - รายการ

ถ้าเราเลือกmที่จะเป็นที่เราได้รับในประเภทของการทำงานของประเภท[] a -> [b]ฟังก์ชั่นดังกล่าวเป็นตัวแทนของการคำนวณที่ไม่ได้กำหนดค่าซึ่งผลลัพธ์อาจเป็นหนึ่งหรือหลายค่า แต่ก็ไม่มีค่าเช่นกัน นี้จะทำให้เกิดสิ่งที่เรียกว่าmonad รายการ องค์ประกอบf :: a -> [b]และการg :: b -> [c]ทำงานดังต่อไปนี้g <=< f :: a -> [c]หมายถึงการคำนวณผลลัพธ์ที่เป็นไปได้ทั้งหมด[b]นำgไปใช้กับแต่ละรายการและรวบรวมผลลัพธ์ทั้งหมดในรายการเดียว แสดงใน Haskell

return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f  = concat . map g . f

หรือใช้ >>=

(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f  = concat (map f x)

โปรดทราบว่าในตัวอย่างนี้ประเภทผลตอบแทนได้ดังนั้นมันจึงเป็นไปได้ว่าพวกเขาไม่ได้มีค่าของชนิดใด[a]aแน่นอนไม่มีข้อกำหนดดังกล่าวสำหรับ monad ที่ประเภทการคืนควรมีค่าเช่นนั้น monads บางคนมักจะมี (ชอบIOหรือState) แต่บางคนไม่เหมือนหรือ[]Maybe

IO monad

อย่างที่ฉันได้กล่าวไปแล้วว่าIOMonad นั้นค่อนข้างพิเศษ มูลค่าของประเภทIO aหมายถึงค่าของประเภทที่aสร้างขึ้นโดยการโต้ตอบกับสภาพแวดล้อมของโปรแกรม ดังนั้น (ต่างจากพระอื่น ๆ ทั้งหมด) เราไม่สามารถอธิบายคุณค่าของประเภทIO aโดยใช้การก่อสร้างที่บริสุทธิ์ นี่IOเป็นเพียงแท็กหรือป้ายกำกับที่แยกการคำนวณที่โต้ตอบกับสภาพแวดล้อม นี่คือ (กรณีเดียว) ที่มุมมอง # 1 และ # 2 ถูกต้อง

สำหรับIOmonad:

  • องค์ประกอบf :: a -> IO bและg :: b -> IO cวิธีการ: คำนวณfที่มีปฏิสัมพันธ์กับสภาพแวดล้อมและจากนั้นคำนวณgที่ใช้ค่าและคำนวณผลลัพธ์ที่โต้ตอบกับสภาพแวดล้อม
  • returnเพียงเพิ่มIO"แท็ก" ให้กับค่า (เราเพียง "คำนวณ" ผลลัพธ์โดยการรักษาสภาพแวดล้อมให้เหมือนเดิม)
  • กฎหมาย monad (การเชื่อมโยงตัวตน) ได้รับการรับรองโดยคอมไพเลอร์

หมายเหตุบางส่วน:

  1. เนื่องจากการคำนวณแบบ monadic มีประเภทผลลัพธ์เสมอดังนั้นm aจึงไม่มีวิธี "หลบหนี" จากIOmonad ความหมายคือ: เมื่อการคำนวณโต้ตอบกับสภาพแวดล้อมคุณไม่สามารถสร้างการคำนวณจากสิ่งนั้นได้
  2. เมื่อโปรแกรมเมอร์ที่ใช้งานได้ไม่รู้ว่าจะทำบางสิ่งด้วยวิธีที่บริสุทธิ์ได้อย่างไรเขาสามารถ (เป็นทางเลือกสุดท้าย ) เขียนโปรแกรมโดยการคำนวณที่เป็นรัฐภายในIOMonad นี่คือเหตุผลที่IOมักจะเรียกว่าโปรแกรมเมอร์bin บาป
  3. ขอให้สังเกตว่าในโลกที่ไม่บริสุทธิ์ (ในแง่ของการเขียนโปรแกรมเชิงฟังก์ชัน) การอ่านค่าสามารถเปลี่ยนแปลงสภาพแวดล้อมได้เช่นกัน (เช่นใช้อินพุตของผู้ใช้) นั่นเป็นเหตุผลที่ฟังก์ชั่นเช่นต้องมีประเภทผลมาจากการgetCharIO something

3
คำตอบที่ดี ฉันขอชี้แจงว่าIOไม่มีความหมายพิเศษจากมุมมองภาษา มันไม่ได้เป็นพิเศษมันทำงานเหมือนรหัสอื่น ๆ เฉพาะการใช้งานไลบรารีรันไทม์เท่านั้นที่พิเศษ นอกจากนี้ยังมีวิธีการพิเศษในการหลบหนี ( unsafePerformIO) ฉันคิดว่านี่เป็นสิ่งสำคัญเพราะคนมักจะคิดว่าIOเป็นองค์ประกอบภาษาพิเศษหรือแท็กที่เปิดเผย มันไม่ใช่.
usr

2
@usr จุดดี ฉันเพิ่มว่าunsafePerformIOนั้นไม่ปลอดภัยจริงๆและควรใช้โดยผู้เชี่ยวชาญเท่านั้น มันช่วยให้คุณสามารถทำลายทุกอย่างตัวอย่างเช่นคุณสามารถสร้างฟังก์ชั่นcoerce :: a -> bที่แปลงสองประเภทใด ๆ (และทำให้โปรแกรมของคุณพังในกรณีส่วนใหญ่) ดูตัวอย่างนี้ - คุณสามารถแปลงฟังก์ชั่นให้เป็นฟังก์ชั่นIntอื่น ๆ ได้
Petr Pudlák

"วิเศษพิเศษ" Monad อื่นคือ ST ซึ่งอนุญาตให้คุณประกาศการอ้างอิงไปยังหน่วยความจำที่คุณสามารถอ่านและเขียนได้ตามที่เห็นสมควร (แม้ว่าจะอยู่ใน Monad เท่านั้น) จากนั้นคุณสามารถแยกผลลัพธ์โดยการโทรrunST :: (forall s. GHC.ST.ST s a) -> a
sara

5

ดู 1: Monad เป็นป้ายกำกับ

"ดังนั้นค่า Int นี้จึงถูกทำเครื่องหมายเป็นค่าที่มาจากกระบวนการที่มี IO ดังนั้นค่านี้จึงเป็น" สกปรก "

"IO Int" ไม่ใช่ค่า Int โดยทั่วไป (แม้ว่ามันอาจจะมีในบางกรณีเช่น "return 3") มันเป็นขั้นตอนที่ส่งออกบางค่า Int การดำเนินการที่แตกต่างกันของ "ขั้นตอนนี้" อาจให้ค่า Int ที่แตกต่างกัน

monad m เป็น "ภาษาโปรแกรม" แบบฝัง (จำเป็น): ภายในภาษานี้มีความเป็นไปได้ที่จะกำหนด "ขั้นตอน" บางอย่าง ค่า monadic (ของประเภท ma) เป็นขั้นตอนใน "ภาษาการเขียนโปรแกรม" นี้ซึ่งส่งออกค่าประเภท

ตัวอย่างเช่น:

foo :: IO Int

เป็นโพรซีเดอร์บางตัวที่ให้ผลลัพธ์เป็นค่าชนิด Int

แล้ว:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

เป็นโพรซีเดอร์บางส่วนที่ส่งเอาต์พุตสอง Ints (อาจแตกต่างกัน)

"ภาษา" ดังกล่าวสนับสนุนการทำงานบางอย่าง:

  • สองโพรซีเดอร์ (ma และ mb) อาจเป็น "concatenated": คุณสามารถสร้างโพรซีเดอร์ขนาดใหญ่ (ma >> mb) ที่สร้างจากโพรเซสแรกจากนั้นโพรเซสที่สอง;

  • ยิ่งไปกว่านั้นเอาต์พุต (a) ของอันแรกอาจส่งผลกระทบต่ออันที่สอง (ma >> = \ a -> ... );

  • โพรซีเดอร์ (return x) อาจให้ค่าคงที่ (x)

ภาษาโปรแกรมฝังตัวที่แตกต่างกันนั้นแตกต่างกันไปในสิ่งที่พวกเขาสนับสนุนเช่น:

  • ให้ค่าสุ่ม
  • "การตี" ([] monad);
  • ข้อยกเว้น (การโยน / จับ) (The either e monad);
  • การสนับสนุนอย่างต่อเนื่อง / callcc ชัดเจน;
  • การส่ง / รับข้อความถึง "ตัวแทน" อื่น ๆ ;
  • สร้างตั้งค่าและอ่านตัวแปร (เฉพาะที่กับภาษาการเขียนโปรแกรมนี้) (ST monad)

1

อย่าสับสนประเภท monadic กับคลาส monad

ประเภท monadic (เช่นประเภทที่เป็นตัวอย่างของคลาส monadic) จะแก้ปัญหาเฉพาะ (ในหลักการแต่ละประเภท monadic แก้ปัญหาที่แตกต่างกัน): รัฐสุ่มบางที IO ทั้งหมดเป็นประเภทที่มีบริบท (สิ่งที่คุณเรียกว่า "ป้ายกำกับ" แต่นั่นไม่ใช่สิ่งที่ทำให้พวกเขาเป็น monad)

สำหรับทั้งหมดของพวกเขามีความต้องการของ "การผูกมัดการดำเนินงานที่มีตัวเลือก" (การดำเนินการอย่างใดอย่างหนึ่งขึ้นอยู่กับผลของการก่อนหน้า) มาที่นี่เพื่อเล่นคลาส monad: ให้ประเภทของคุณ (แก้ปัญหาที่ระบุ) เป็นตัวอย่างของคลาส monad และปัญหาการโยงโซ่ได้รับการแก้ไข

ดูคลาส monad แก้ปัญหาอะไร?

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