閱讀器Monad非常複雜,似乎沒用。在像Java或C++這樣的命令式語言中,對於讀者monad來說沒有等價的術語(如果我是對的)。閱讀器Monad的目的是什麼?
你能給我一個簡單的例子,讓我清楚一點嗎?
閱讀器Monad非常複雜,似乎沒用。在像Java或C++這樣的命令式語言中,對於讀者monad來說沒有等價的術語(如果我是對的)。閱讀器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。
讀者單子也被用來做什麼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
只是id
和local
只是在其他順序的功能組成!
非常有趣的答案。說實話,我多次閱讀它,當我想審查monad。順便說一下,關於nagamax算法,「values < - mapM(negate。我知道,你提供的代碼只是爲了展示讀者單子如何工作,但是如果你有時間,你能糾正negamax算法的代碼嗎?因爲有趣的是,當你使用reader monad來解決negamax – chipbk10
所以'Reader'是一個具有monad類型類的特定實現的函數嗎?以前說過會讓我感到困惑一點,首先我沒有得到它。我認爲:「哦,它允許你返回一些能夠在你提供缺失值時給你想要的結果的東西。」我認爲這很有用,但是突然意識到一個函數完全可以做到這一點 – ziggystar
在閱讀了這篇文章之後,我瞭解了大部分內容。'local'函數確實需要更多的解釋.. –
我記得當時很困惑,直到我自己發現讀卡器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 m => (a -> m b) -> (b -> m c) -> a -> m c
可以最直觀地理解monad實例。類型爲a -> History t b
的函數是將a
映射到b
值的歷史的函數;例如,您可以有getSupervisor :: Person -> History Day Supervisor
和getVP :: Supervisor -> History Day VP
。因此,History
的Monad實例是關於組合這些功能的;例如,getSupervisor >=> getVP :: Person -> History Day VP
是獲取(Person
)所具有的歷史記錄的功能。
那麼,這個History
monad其實就是,就是,就跟Reader
一樣。 History t a
與Reader 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
。
所以Functor
,Applicative
和Reader
Monad
情況是因爲你所排序的造型任何情況下,一個非常有用的泛化「的a
是不可少的r
」,並允許您將這些「不完整」的對象,就好像他們完成了。
說同樣的事情的另一種方法:一個Reader r a
是一件消耗r
併產生a
和Functor
,Applicative
和Monad
情況下是基本模式與Reader
s工作。 Functor
=使Reader
修改另一個Reader
的輸出; Applicative
=將兩個Reader
連接到相同的輸入並組合它們的輸出; Monad
=檢查Reader
的結果,並用它來構造另一個Reader
。 local
和withReader
函數=使Reader
將輸入修改爲另一個Reader
。
很棒的回答。您還可以使用'GeneralizedNewtypeDeriving'擴展名來根據基礎類型爲newty派生'Functor','Applicative','Monad'等。 –
在Java或C++中,您可以從任何位置訪問任何變量,而不會有任何問題。您的代碼變爲多線程時出現問題。
在Haskell你只有兩種方式的值傳遞從一個功能到另一個:
fn1 -> fn2 -> fn3
,函數fn2
可能不需要您從fn1
到fn3
傳遞的參數。Reader monad只是傳遞你想在功能之間共享的數據。函數可能會讀取該數據,但不能更改它。這就是Reader monad的全部功能。好吧,幾乎所有。還有一些功能,如local
,但第一次只能堅持asks
。
使用monads隱式傳遞數據的另一個缺點是,很容易發現自己在'do'-notation中編寫了大量的'命令式'代碼,這會更好地被重構爲純函數。 –
@BenjaminHodgson在do -notation中寫入'單向性'代碼並不意味着編寫副作用(不純)代碼。實際上,Haskell中的副作用代碼可能只能在IO monad中使用。 –
如果您想 - 有時從(不可修改的)環境讀取一些值,但不希望顯式傳遞該環境,則可以使用讀者monad。在Java或C++中,您可以使用全局變量(雖然它不完全相同)。 –
@丹尼爾:聽起來非常像一個*答案* – SingleNegationElimination
@TokenMacGuy答案太短了,現在想想更長的時間已經太晚了。如果沒有其他人做,我會睡覺後。 –