读者模式看起来很复杂,而且似乎没有什么用处。在像Java或C++这样的命令式语言中,如果我没有弄错的话,没有与读者模式相当的概念。
你能给我一个简单的例子,并解释一下吗?
读者模式看起来很复杂,而且似乎没有什么用处。在像Java或C++这样的命令式语言中,如果我没有弄错的话,没有与读者模式相当的概念。
你能给我一个简单的例子,并解释一下吗?
不要害怕!读者单子其实并不那么复杂,而且有真正易于使用的实用功能。
有两种方法来理解单子:我们可以询问
从第一种方法来看,读者单子是某种抽象类型。
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
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
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 [])
就是这样。一个完全功能的lambda演算解释器。
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
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)
好的,所以阅读器单子只是一个函数。为什么需要Reader呢?很好的问题。实际上,你不需要它!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
ask
只是id
,local
只是函数组合,函数的顺序被调换了!(Reader f) >>= g = (g (f x))
吗? - zerononex
在哪里? - Ashish Negiimport 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
,我们可以最直观地理解单子实例。类型为 a -> History t b
的函数将 a
映射到 b
值的历史记录;例如,你可以有 getSupervisor :: Person -> History Day Supervisor
和 getVP :: Supervisor -> History Day VP
。因此,History
的单子实例是关于组合这些函数的;例如,getSupervisor >=> getVP :: Person -> History Day VP
是一个函数,对于任何 Person
,它获取了他们拥有的所有 VP
的历史记录。
嗯,这个 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 @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Reader r a
表示一个带有“空洞”的 a
,防止你知道我们正在谈论哪个 a
。只有在提供一个 r
来填补这些空洞后,才能获得实际的 a
。还有很多类似的东西。在上面的例子中,“history”是一个值,在指定时间之前无法计算;超立方体是一个值,在指定交点之前无法计算;语言表达式是一个值,在提供变量的值之前无法计算。这也让你直观地理解为什么 Reader r a
和 r -> a
是相同的,因为这样的函数本质上也是一个缺少了 r
的 a
。Reader
的 Functor
、Applicative
和 Monad
实例非常适用于模拟任何“缺少 r
的 a
”类型的情况,并允许您将这些“不完整”的对象视为完整。Reader r a
是消耗r
并产生a
的东西,而Functor
、Applicative
和Monad
实例是处理Reader
的基本模式。Functor
=创建一个修改另一个Reader
输出的Reader
;Applicative
=将两个Reader
连接到同一输入并组合它们的输出;Monad
=检查Reader
的结果并使用它来构建另一个Reader
。local
和withReader
函数=创建一个修改另一个Reader
输入的Reader
。在Java或C++中,您可以从任何地方访问任何变量而不会出现任何问题。当代码变为多线程时,问题出现了。
在Haskell中,只有两种方法可以将值从一个函数传递到另一个函数:
fn1 -> fn2 -> fn3
中,函数fn2
可能不需要从fn1
传递到fn3
的参数。Reader monad只是传递您想要在函数之间共享的数据。函数可以读取该数据,但不能更改它。那就是Reader monad所做的全部。好吧,几乎全部。还有一些像local
这样的函数,但是您可以先只使用asks
。
do
符号语法中编写了许多“命令式风格”的代码,最好将其重构为纯函数。 - Benjamin Hodgsonwhere
子句连接到该函数,那么它是否被接受为传递变量的第三种方式? - Elmex80s我从以上答案中学到了很多,并感谢它们的回答。虽然我对 Haskell 还很陌生,但我想谈一下关于 Reader
Monad 的一些事情。
首先,将 Reader 视为一个 Computation
是很重要的。它不是一个状态,而是一个计算。
例如,Reader monad 的第一个官方示例 中提到的函数 calc_isCountCorrect
返回一个 Reader Bindings Bool
,这意味着当调用 runReader
时,它接收一个 Bindings
并返回一个 Bool。它是一个计算。
calc_isCountCorrect :: Reader Bindings Bool
calc_isCountCorrect = do
count <- asks (lookupVar "count")
bindings <- ask
return (count == (Map.size bindings))
Bindings
,当您的代码非常简单时,没有什么显著的区别。calcIsCountCorrectWithoutReader :: Bindings -> Bool
calcIsCountCorrectWithoutReader bindings = do
let num = lookupVar "count" bindings
let count = Map.size bindings
num == count
然而,它确实有一个区别,那就是价值来自何处
。它提供了一种通过隐式来源而不是参数来检索它的方式。
当涉及类比问题时,我认为C++
中的lambda
是一个很好的解释。
在像Java或C++这样的命令式语言中,没有与读者单子等效的概念。
读者使您能够从外部(非全局变量,而是上层作用域)检索值。这很像C++ lambda
中的捕获子句。
例如,您有一个Haskell代码,将number
变为三倍,并将其与隐式值相加。以下代码输出10
。
import Control.Monad.Reader
trebleNumberAndAddImplicitly :: Int -> Reader Int Int
trebleNumberAndAddImplicitly number = do
implicitNumber <- ask
return $ 3*number + implicitNumber
main :: IO ()
main = do
print $ runReader (trebleNumberAndAddImplicitly 3) 1
Reader
访问。捕获子句
。输出是:output is: 10
。然而,它比Haskell有更多的限制。但在我看来它们相似。#include<iostream>
int main(){
int implicitNumber = 1;
auto trebleNumberAndAddImplicitly = [implicitNumber](int number) -> int {
return (3*number + implicitNumber);
};
std::cout << "output is: " << trebleNumberAndAddImplicitly(3);
}