จุดประสงค์ของผู้อ่าน monad คืออะไร?


122

monad ผู้อ่านมีความซับซ้อนและดูเหมือนจะไม่มีประโยชน์ ในภาษาที่จำเป็นเช่น Java หรือ C ++ ไม่มีแนวคิดที่เทียบเท่าสำหรับผู้อ่าน monad ถ้าฉันไม่เข้าใจผิด

คุณช่วยยกตัวอย่างง่ายๆและทำให้ชัดเจนขึ้นหน่อยได้ไหม


21
คุณใช้โปรแกรมอ่าน monad หากคุณต้องการ - ในบางครั้ง - อ่านค่าบางอย่างจากสภาพแวดล้อม (ไม่สามารถแก้ไขได้) แต่ไม่ต้องการส่งผ่านสภาพแวดล้อมนั้นอย่างชัดเจน ใน Java หรือ C ++ คุณจะใช้ตัวแปรส่วนกลาง (แม้ว่าจะไม่เหมือนกันทุกประการ)
Daniel Fischer

5
@Daniel: ฟังดูแย่มากเหมือนคำตอบ
SingleNegationElimination

@TokenMacGuy สั้นเกินไปสำหรับคำตอบและตอนนี้มันสายไปแล้วสำหรับฉันที่จะคิดอะไรอีกต่อไป ถ้าไม่มีใครทำฉันจะหลังจากที่ฉันนอนแล้ว
Daniel Fischer

8
ใน Java หรือ C ++ โมนาดของ Reader จะคล้ายคลึงกับพารามิเตอร์คอนฟิกูเรชันที่ส่งผ่านไปยังอ็อบเจ็กต์ในคอนสตรัคเตอร์ซึ่งจะไม่มีการเปลี่ยนแปลงตลอดอายุการใช้งานของอ็อบเจ็กต์ ใน Clojure มันจะเหมือนกับตัวแปรที่กำหนดขอบเขตแบบไดนามิกที่ใช้เพื่อกำหนดลักษณะการทำงานของฟังก์ชันโดยไม่จำเป็นต้องส่งผ่านไปอย่างชัดเจนเป็นพารามิเตอร์
danidiaz

คำตอบ:


169

ไม่ต้องกลัว! ตัวอ่าน monad นั้นไม่ซับซ้อนและมียูทิลิตี้ที่ใช้งานง่ายอย่างแท้จริง

มีสองวิธีในการเข้าหาโมนาด: เราถามได้

  1. Monad ทำอะไร? มีระบบปฏิบัติการอะไรบ้าง? มันดีสำหรับอะไร?
  2. monad ใช้งานอย่างไร? มันเกิดขึ้นจากไหน?

จากแนวทางแรกผู้อ่าน monad เป็นนามธรรมบางประเภท

data Reader env a

ดังนั้น

-- Reader is a monad
instance Monad (Reader env)

-- and we have a function to get its environment
ask :: Reader env env

-- finally, we can run a Reader
runReader :: Reader env a -> env -> a

แล้วเราจะใช้สิ่งนี้อย่างไร? ผู้อ่าน monad นั้นดีสำหรับการส่งผ่านข้อมูลการกำหนดค่า (โดยนัย) ผ่านการคำนวณ

เมื่อใดก็ตามที่คุณมี "ค่าคงที่" ในการคำนวณที่คุณต้องการในหลาย ๆ จุด แต่จริงๆแล้วคุณต้องการที่จะสามารถทำการคำนวณเดียวกันกับค่าที่แตกต่างกันคุณควรใช้ตัวอ่าน monad

monads อ่านยังจะใช้ในการทำในสิ่งที่คน OO เรียกฉีดพึ่งพา ตัวอย่างเช่นอัลกอริทึมNegamaxถูกใช้บ่อย (ในรูปแบบที่ได้รับการปรับให้เหมาะสมที่สุด) เพื่อคำนวณมูลค่าของตำแหน่งในเกมที่มีผู้เล่นสองคน แม้ว่าอัลกอริทึมจะไม่สนใจว่าคุณกำลังเล่นเกมอะไรอยู่ยกเว้นว่าคุณต้องสามารถกำหนดตำแหน่ง "ถัดไป" ในเกมได้และคุณต้องสามารถบอกได้ว่าตำแหน่งปัจจุบันเป็นตำแหน่งแห่งชัยชนะหรือไม่

 import Control.Monad.Reader

 data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie

 data Game position
   = Game {
           getNext :: position -> [position],
           getState :: position -> GameState
          }

 getNext' :: position -> Reader (Game position) [position]
 getNext' position
   = do game <- ask
        return $ getNext game position

 getState' :: position -> Reader (Game position) GameState
 getState' position
   = do game <- ask
        return $ getState game position


 negamax :: Double -> position -> Reader (Game position) Double
 negamax color position
     = do state <- getState' position 
          case state of
             FirstPlayerWin -> return color
             SecondPlayerWin -> return $ negate color
             Tie -> return 0
             NotOver -> do possible <- getNext' position
                           values <- mapM ((liftM negate) . negamax (negate color)) possible
                           return $ maximum values

จากนั้นสิ่งนี้จะใช้ได้กับเกมที่มีผู้เล่นสองคน

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

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

เพื่อรับราคาสปอต จากนั้นคุณสามารถเรียกพจนานุกรมนี้ในรหัสของคุณได้ .... แต่เดี๋ยวก่อน! ไม่ได้ผล! พจนานุกรมสกุลเงินนั้นไม่เปลี่ยนรูปและต้องเหมือนกันไม่เพียง แต่ตลอดอายุการใช้งานของโปรแกรมของคุณเท่านั้น แต่ยังรวมถึงเวลาที่รวบรวมด้วย ! แล้วคุณจะทำอย่างไร? ทางเลือกหนึ่งคือใช้ Reader monad:

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here

บางทีการใช้งานที่คลาสสิกที่สุดคือการใช้ล่าม แต่ก่อนที่เราจะดูนั้นเราต้องแนะนำฟังก์ชันอื่น

 local :: (env -> env) -> Reader env a -> Reader env a

เอาล่ะเพื่อ Haskell และภาษาอื่น ๆ ที่ทำงานอยู่บนพื้นฐานของแคลคูลัสแลมบ์ดา แลมบ์ดาแคลคูลัสมีไวยากรณ์ที่ดูเหมือน

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)

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

 newtype Env = Env ([(String, Closure)])
 type Closure = (Term, Env)

เมื่อเสร็จแล้วเราควรหาค่า (หรือข้อผิดพลาด):

 data Value = Lam String Closure | Failure String

ลองเขียนล่าม:

interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
   = do env <- ask
        return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
   = do (Env env) <- ask
        case lookup (show v) env of
          -- if it is not in the environment we have a problem
          Nothing -> return . Failure $ "unbound variable: " ++ (show v)
          -- if it is in the environment, then we should interpret it
          Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
   = do v1 <- interp' t1
        case v1 of
           Failure s -> return (Failure s)
           Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!

ในที่สุดเราสามารถใช้มันได้โดยผ่านสภาพแวดล้อมที่ไม่สำคัญ:

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])

และนั่นก็คือ ล่ามที่ทำงานได้อย่างสมบูรณ์สำหรับแคลคูลัสแลมบ์ดา


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

newtype Reader env a = Reader {runReader :: env -> a}

Reader เป็นเพียงชื่อแฟนซีสำหรับฟังก์ชัน! เราได้กำหนดไว้แล้วแล้วrunReaderส่วนอื่น ๆ ของ API ล่ะ? ทุกคนMonadยังเป็นFunctor:

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

ตอนนี้เพื่อรับ monad:

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x

ซึ่งไม่น่ากลัวเท่าไหร่ askง่ายมาก:

ask = Reader $ \x -> x

ในขณะที่localไม่เลวร้ายนัก:

local f (Reader g) = Reader $ \x -> runReader g (f x)

โอเคดังนั้นผู้อ่าน monad เป็นเพียงหน้าที่ ทำไมมี Reader เลย? คำถามที่ดี. ที่จริงคุณไม่ต้องการมัน!

instance Functor ((->) env) where
  fmap = (.)

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x

สิ่งเหล่านี้ง่ายกว่าด้วยซ้ำ มีอะไรมากกว่าที่askเป็นเพียงidและlocalเป็นเพียงองค์ประกอบของฟังก์ชั่นคำสั่งของฟังก์ชั่นเปลี่ยน!


6
คำตอบที่น่าสนใจมาก สุจริตฉันอ่านซ้ำหลายครั้งเมื่อฉันต้องการทบทวน monad อย่างไรก็ตามเกี่ยวกับอัลกอริทึม nagamax "ค่า <- mapM (ทำให้เป็นไปได้. negamax (สีลบ))" ดูเหมือนจะไม่ถูกต้อง ฉันรู้ว่ารหัสที่คุณระบุมีไว้เพื่อแสดงให้เห็นว่าเครื่องอ่าน monad ทำงานอย่างไร แต่ถ้าคุณมีเวลาคุณสามารถแก้ไขรหัสของอัลกอริทึม Negamax ได้หรือไม่? เพราะมันน่าสนใจเมื่อคุณใช้ reader monad เพื่อแก้ปัญหา negamax
chipbk10

4
ดังนั้นReaderฟังก์ชันที่มีการใช้งานคลาสประเภท monad โดยเฉพาะหรือไม่? การพูดก่อนหน้านี้จะช่วยให้ฉันงงงวยน้อยลง ก่อนอื่นฉันไม่เข้าใจ ผ่านไปครึ่งทางฉันคิดว่า "โอ้มันช่วยให้คุณสามารถคืนของที่จะให้ผลลัพธ์ที่ต้องการเมื่อคุณระบุมูลค่าที่ขาดหายไป" ฉันคิดว่ามีประโยชน์ แต่ทันใดนั้นก็รู้ว่าฟังก์ชันทำสิ่งนี้ได้
ziggystar

1
หลังจากอ่านสิ่งนี้ฉันเข้าใจมากที่สุด localฟังก์ชั่นที่ไม่ต้องการคำอธิบายเพิ่มเติมบางส่วน แต่ ..
Christophe De Troyer

@ ฟิลิปฉันมีคำถามเกี่ยวกับอินสแตนซ์ Monad เราเขียนฟังก์ชันการผูกเป็น(Reader f) >>= g = (g (f x))ไม่ได้หรือไม่?
zeronone

@zeronone อยู่ที่ไหนx?
Ashish Negi

56

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

ยกตัวอย่างเช่นที่จุดหนึ่งที่ผมเขียนโค้ดบางส่วนที่จะจัดการกับประวัติศาสตร์ค่า; ค่าที่เปลี่ยนแปลงตลอดเวลา แบบจำลองที่ง่ายมากของสิ่งนี้คือฟังก์ชันจากจุดเวลาไปจนถึงค่า ณ เวลานั้น:

import Control.Applicative

-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }

instance Functor (History t) where
    -- Apply a function to the contents of a historical value
    fmap f hist = History (f . observe hist)

instance Applicative (History t) where
    -- A "pure" History is one that has the same value at all points in time
    pure = History . const

    -- This applies a function that changes over time to a value that also 
    -- changes, by observing both at the same point in time.
    ff <*> fx = History $ \t -> (observe ff t) (observe fx t)

instance Monad (History t) where
    return = pure
    ma >>= f = History $ \t -> observe (f (observe ma t)) t

Applicativeเช่นหมายความว่าถ้าคุณมีemployees :: History Day [Person]และcustomers :: History Day [Person]คุณสามารถทำสิ่งนี้:

-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers

กล่าวคือFunctorและApplicativeอนุญาตให้เราปรับฟังก์ชั่นปกติที่ไม่ใช่ในอดีตเพื่อทำงานร่วมกับประวัติศาสตร์

อินสแตนซ์ monad (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m cเป็นที่เข้าใจมากที่สุดสังหรณ์ใจโดยพิจารณาฟังก์ชัน ฟังก์ชันประเภทa -> History t bคือฟังก์ชันที่จับคู่aกับประวัติของbค่า ตัวอย่างเช่นคุณอาจมีและgetSupervisor :: Person -> History Day Supervisor getVP :: Supervisor -> History Day VPดังนั้นตัวอย่าง Monad Historyจึงเกี่ยวกับการเขียนฟังก์ชันเช่นนี้ ตัวอย่างเช่นgetSupervisor >=> getVP :: Person -> History Day VPเป็นฟังก์ชันที่ได้รับPersonประวัติของVPs ที่เคยมี

ดีนี้Historymonad เป็นจริงว่าReaderเป็นเช่นเดียวกับ History t aเหมือนกับReader t a(ซึ่งเหมือนกับt -> a) จริงๆ

อีกตัวอย่างหนึ่ง: ฉันได้สร้างต้นแบบการออกแบบOLAPใน Haskell เมื่อเร็ว ๆ นี้ แนวคิดหนึ่งที่นี่คือ "ไฮเปอร์คิวบ์" ซึ่งเป็นการจับคู่จากจุดตัดของชุดมิติข้อมูลไปยังค่า ที่นี่เราไปอีกครั้ง:

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }

การดำเนินการทั่วไปอย่างหนึ่งบนไฮเปอร์คิวบ์คือการใช้ฟังก์ชันสเกลาร์แบบหลายตำแหน่งกับจุดที่สอดคล้องกันของไฮเปอร์คิวบ์ สิ่งนี้เราจะได้รับโดยการกำหนดApplicativeอินสแตนซ์สำหรับHypercube:

instance Functor (Hypercube intersection) where
    fmap f cube = Hypercube (f . get cube)


instance Applicative (Hypercube intersection) where
    -- A "pure" Hypercube is one that has the same value at all intersections
    pure = Hypercube . const

    -- Apply each function in the @ff@ hypercube to its corresponding point 
    -- in @fx@.
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

ฉันแค่คัดลอกHistoryโค้ดด้านบนและเปลี่ยนชื่อ ในขณะที่คุณสามารถบอกได้นอกจากนี้ยังเป็นเพียงแค่HypercubeReader

มันดำเนินไปเรื่อย ๆ ตัวอย่างเช่นล่ามภาษาก็มีปัญหาReaderเช่นกันเมื่อคุณใช้โมเดลนี้:

  • Expression = ก Reader
  • ตัวแปรฟรี = การใช้ ask
  • สภาพแวดล้อมการประเมินผล = Readerสภาพแวดล้อมการดำเนินการ
  • โครงสร้างการผูก = local

การเปรียบเทียบที่ดีก็คือเครื่องหมายReader r aแสดงถึงa"รู" ในนั้นซึ่งทำให้คุณไม่รู้ว่าaเรากำลังพูดถึงอะไร คุณจะได้รับจริงก็ต่อaเมื่อคุณจัดหาrเพื่อเติมเต็มในหลุม มีหลายอย่างเช่นนั้น ในตัวอย่างด้านบน "ประวัติ" คือค่าที่คำนวณไม่ได้จนกว่าคุณจะระบุเวลาไฮเปอร์คิวบ์คือค่าที่คำนวณไม่ได้จนกว่าคุณจะระบุจุดตัดและนิพจน์ภาษาเป็นค่าที่สามารถ ไม่สามารถคำนวณได้จนกว่าคุณจะระบุค่าของตัวแปร นอกจากนี้ยังช่วยให้คุณมีสัญชาตญาณว่าทำไมReader r aเป็นเช่นเดียวกับr -> aเพราะฟังก์ชั่นดังกล่าวยังเป็นสังหรณ์ใจหายไปar

ดังนั้นFunctor, ApplicativeและMonadกรณีของการReaderเป็นลักษณะทั่วไปมีประโยชน์มากสำหรับกรณีที่คุณกำลังสร้างแบบจำลองอะไรของการจัดเรียงที่ " aที่หายไปr" และช่วยให้คุณในการรักษาเหล่านี้ "ไม่สมบูรณ์" วัตถุราวกับว่าพวกเขาสมบูรณ์

แต่วิธีการพูดในสิ่งเดียวกันอีกกReader r aเป็นสิ่งที่กินrและผลิตaและFunctor, ApplicativeและMonadกรณีที่มีรูปแบบพื้นฐานสำหรับการทำงานกับReaders Functor= สร้างReaderที่ปรับเปลี่ยนผลลัพธ์ของอีกReader; Applicative= เชื่อมต่อสองReaderวินาทีกับอินพุตเดียวกันและรวมเอาท์พุท Monad= ตรวจสอบผลมาจากการที่และใช้มันเพื่อสร้างอีกReader และฟังก์ชั่น = ทำให้ที่ปรับเปลี่ยนการป้อนข้อมูลไปยังอีกReaderlocalwithReaderReaderReader


5
คำตอบที่ดี นอกจากนี้คุณยังสามารถใช้GeneralizedNewtypeDerivingส่วนขยายให้ได้มาซึ่งFunctor, Applicative, Monadฯลฯ สำหรับ Newtypes ขึ้นอยู่กับประเภทพื้นฐานของพวกเขา
Rein Henrichs

20

ใน Java หรือ C ++ คุณสามารถเข้าถึงตัวแปรได้จากทุกที่โดยไม่มีปัญหา ปัญหาจะปรากฏขึ้นเมื่อรหัสของคุณกลายเป็นแบบมัลติเธรด

ใน Haskell คุณมีเพียงสองวิธีในการส่งผ่านค่าจากฟังก์ชันหนึ่งไปยังอีกฟังก์ชันหนึ่ง:

  • คุณส่งค่าผ่านพารามิเตอร์อินพุตตัวใดตัวหนึ่งของฟังก์ชันที่เรียกได้ ข้อเสียคือ 1) คุณไม่สามารถส่งผ่านตัวแปรทั้งหมดด้วยวิธีนั้น - รายการพารามิเตอร์อินพุตทำให้คุณประทับใจ 2) ในลำดับการเรียกฟังก์ชั่นfn1 -> fn2 -> fn3การทำงานfn2อาจจะไม่ต้องใช้พารามิเตอร์ที่คุณผ่านจากไปfn1fn3
  • คุณส่งผ่านค่าในขอบเขตของ monad บางอย่าง ข้อเสียคือคุณต้องเข้าใจอย่างถ่องแท้ว่าความคิดของโมนาดคืออะไร การส่งผ่านค่าต่างๆเป็นเพียงหนึ่งในแอปพลิเคชันจำนวนมากที่คุณสามารถใช้ Monads ได้ ความคิดของโมนาดนั้นมีพลังมากอย่างไม่น่าเชื่อ อย่าอารมณ์เสียถ้าคุณไม่เข้าใจในทันที พยายามต่อไปและอ่านบทแนะนำต่างๆ ความรู้ที่คุณจะได้รับจะคุ้มค่า

Reader monad เพียงแค่ส่งข้อมูลที่คุณต้องการแบ่งปันระหว่างฟังก์ชันต่างๆ ฟังก์ชันอาจอ่านข้อมูลนั้น แต่ไม่สามารถเปลี่ยนแปลงได้ นั่นคือทั้งหมดที่ทำ Reader monad ดีเกือบทั้งหมด นอกจากนี้ยังมีฟังก์ชั่นต่างๆเช่นlocalแต่เป็นครั้งแรกที่คุณสามารถใช้งานได้asksเท่านั้น


3
ข้อเสียเปรียบเพิ่มเติมของการใช้ monads เพื่อส่งผ่านข้อมูลโดยปริยายก็คือมันง่ายมากที่จะพบว่าตัวเองกำลังเขียนโค้ด 'imperative-style' จำนวนมากในdo-notation ซึ่งจะดีกว่าหากถูก refactored เป็นฟังก์ชันที่บริสุทธิ์
Benjamin Hodgson

4
@BenjaminHodgson การเขียนโค้ด 'จำเป็น - ดูดี' ด้วย monads ใน do -notation ไม่จำเป็นหมายถึงการเขียนโค้ดที่มีประสิทธิภาพด้านข้าง (ไม่บริสุทธิ์) จริงๆแล้วโค้ดด้านประสิทธิภาพใน Haskell อาจเป็นไปได้ภายใน IO monad เท่านั้น
Dmitry Bespalov

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