阅读器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答案太短了,现在想想更长的时间已经太晚了。如果没有其他人做,我会睡觉后。 –