2013-01-06 47 views
89

閱讀器Monad非常複雜,似乎沒用。在像Java或C++這樣的命令式語言中,對於讀者monad來說沒有等價的術語(如果我是對的)。閱讀器Monad的目的是什麼?

你能給我一個簡單的例子,讓我清楚一點嗎?

+19

如果您想 - 有時從(不可修改的)環境讀取一些值,但不希望顯式傳遞該環境,則可以使用讀者monad。在Java或C++中,您可以使用全局變量(雖然它不完全相同)。 –

+5

@丹尼爾:聽起來非常像一個*答案* – SingleNegationElimination

+0

@TokenMacGuy答案太短了,現在想想更長的時間已經太晚了。如果沒有其他人做,我會睡覺後。 –

回答

117

不要害怕!閱讀器monad實際上並不複雜,並且具有真正簡單易用的實用程序。

有接近一個單子的方式有兩種:我們可以問

  1. 是什麼單子?它配備了什麼操作?到底有什麼好處呢?
  2. 如何單子執行?它從哪裏出現?

從第一種方法,讀者單子是一些抽象類型

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。

讀者單子也被用來做什麼OO人稱之爲dependency injection。例如,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和其他功能的語言都是基於lambda calculus。 Lambda微積分的語法看起來像

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 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, than 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 []) 

就是這樣。 lambda演算的全功能解釋器。


所以,另一種思考這個問題的方法是問:它是如何實現的?那麼答案是,讀者monad實際上是所有monads中最簡單最優雅的一種。

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

閱讀器只是一個功能的花哨名稱!我們已經定義了runReader那麼API的其他部分呢?那麼每一個Monad也是Functor

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

現在,得到一個單子:

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) 

好了,所以讀者單子只是一個功能。爲什麼有讀者?好問題。其實,你不需要它!

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

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

這是更簡單。更何況,ask只是idlocal只是在其他順序的功能組成!

+4

非常有趣的答案。說實話,我多次閱讀它,當我想審查monad。順便說一下,關於nagamax算法,「values < - mapM(negate。我知道,你提供的代碼只是爲了展示讀者單子如何工作,但是如果你有時間,你能糾正negamax算法的代碼嗎?因爲有趣的是,當你使用reader monad來解決negamax – chipbk10

+4

所以'Reader'是一個具有monad類型類的特定實現的函數嗎?以前說過會讓我感到困惑一點,首先我沒有得到它。我認爲:「哦,它允許你返回一些能夠在你提供缺失值時給你想要的結果的東西。」我認爲這很有用,但是突然意識到一個函數完全可以做到這一點 – ziggystar

+1

在閱讀了這篇文章之後,我瞭解了大部分內容。'local'函數確實需要更多的解釋.. –

38

我記得當時很困惑,直到我自己發現讀卡器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 

也就是說,FunctorApplicative允許我們適應規律的,非歷史的功能來處理歷史。

通過考慮函數(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c可以最直觀地理解monad實例。類型爲a -> History t b的函數是將a映射到b值的歷史的函數;例如,您可以有getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP。因此,History的Monad實例是關於組合這些功能的;例如,getSupervisor >=> getVP :: Person -> History Day VP是獲取(Person)所具有的歷史記錄的功能。

那麼,這個History monad其實就是,就是,就跟Reader一樣。 History t aReader t a(與t -> a相同)確實相同。

另一個例子:我最近在Haskell設計了原型OLAP。這裏的一個想法是「超立方體」,它是從一組維度的交集到值的映射。在這裏,我們又來了:

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

一個常見的立方體操作是應用多地標功能,以超立方體的對應點。這一點我們可以通過定義一個Hypercube例如Applicative得到:

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 @[email protected] hypercube to its corresponding point 
    -- in @[email protected] 
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x) 

我只是copypasted以上History代碼改了個名字。如您所知,Hypercube也只是Reader

它繼續下去。例如,語翻譯也歸結爲Reader,在應用此模型:

  • 表達式= ask
  • 評價環境= Reader執行環境的Reader
  • 自由變量=用途。
  • 結合構建= local

一個很好的比喻是,Reader r a代表與它「洞」,即防止你知道哪些a我們談論的a。一旦你提供一個r來填補洞,你只能得到一個實際的a。有很多這樣的事情。在上面的例子中,「歷史」是一個值,只有指定時間後才能計算,超立方體是指定交集之前無法計算的值,而語言表達式是可以計算的值直到你提供變量的值才被計算。它也給你一個直覺,因爲Reader r a爲什麼與r -> a相同,因爲這樣的功能也是直觀的a缺少r

所以FunctorApplicativeReaderMonad情況是因爲你所排序的造型任何情況下,一個非常有用的泛化「的a是不可少的r」,並允許您將這些「不完整」的對象,就好像他們完成了。

說同樣的事情的另一種方法:一個Reader r a是一件消耗r併產生aFunctorApplicativeMonad情況下是基本模式與Reader s工作。 Functor =使Reader修改另一個Reader的輸出; Applicative =將兩個Reader連接到相同的輸入並組合它們的輸出; Monad =檢查Reader的結果,並用它來構造另一個ReaderlocalwithReader函數=使Reader將輸入修改爲另一個Reader

+4

很棒的回答。您還可以使用'GeneralizedNewtypeDeriving'擴展名來根據基礎類型爲newty派生'Functor','Applicative','Monad'等。 –

13

在Java或C++中,您可以從任何位置訪問任何變量,而不會有任何問題。您的代碼變爲多線程時出現問題。

在Haskell你只有兩種方式的值傳遞從一個功能到另一個:

  • 您通過的可調用函數的輸入參數一個傳遞價值。缺點是:1)你不能以這種方式傳遞所有的變量 - 輸入參數列表只是讓你頭腦發熱。 2)按函數調用順序:fn1 -> fn2 -> fn3,函數fn2可能不需要您從fn1fn3傳遞的參數。
  • 你傳遞一些monad的值。缺點是:你必須牢固理解Monad的概念是什麼。將值傳遞給您可能會使用Monads的應用程序只是其中的一個。其實Monad的構想是非常強大的。如果你一次沒有得到洞察力,不要感到不安。只要繼續嘗試,並閱讀不同的教程。你將獲得的知識將會得到回報。

Reader monad只是傳遞你想在功能之間共享的數據。函數可能會讀取該數據,但不能更改它。這就是Reader monad的全部功能。好吧,幾乎所有。還有一些功能,如local,但第一次只能堅持asks

+2

使用monads隱式傳遞數據的另一個缺點是,很容易發現自己在'do'-notation中編寫了大量的'命令式'代碼,這會更好地被重構爲純函數。 –

+3

@BenjaminHodgson在do -notation中寫入'單向性'代碼並不意味着編寫副作用(不純)代碼。實際上,Haskell中的副作用代碼可能只能在IO monad中使用。 –