读者单子的目的是什么?

2023-12-13

reader monad 太复杂了,而且似乎没什么用处。如果我没记错的话,在 Java 或 C++ 这样的命令式语言中,读者 monad 没有等效的概念。

你能给我一个简单的例子并稍微澄清一下吗?


别害怕! reader monad 实际上并没有那么复杂,并且具有真正易于使用的实用性。

有两种处理单子的方法:我们可以问

  1. 单子有什么作用do?它配备了哪些操作?到底有什么好处呢?
  2. 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。

Reader monad 也用于执行 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

以获得现货价格。然后你可以在代码中调用这个字典......但是等等!那是不行的!货币字典是不可变的,因此不仅在程序的生命周期内必须相同,而且从它获得的时间起也必须相同compiled!所以你会怎么做?好吧,一种选择是使用 Reader monad:

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

也许最经典的用例是实现解释器。但是,在我们看这个之前,我们需要介绍另一个函数

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

好的,Haskell 和其他函数式语言都是基于拉姆达演算。 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 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 演算的全功能解释器。


思考这个问题的另一种方式是问:它是如何实现的?答案是,reader 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

while local还不错:

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

好的,所以 reader monad 只是一个函数。为什么要有 Reader?好问题。事实上,你不需要它!

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

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

这些就更简单了。更重要的是,ask只是id and local只是函数组合,并且函数的顺序交换了!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

读者单子的目的是什么? 的相关文章

  • 带有 RankNTypes 扩展的奇怪类型推断

    我正在尝试在 Haskell 中尝试 System F 类型 并通过以下方式实现了自然数的 Church 编码type 当加载这段代码时 OPTIONS GHC Wall LANGUAGE RankNTypes type CNat fora
  • 管道:多个流消费者

    我编写了一个程序来计算语料库中 NGram 的频率 我已经有一个函数 它消耗一串令牌并生成一个订单的 NGram ngram Monad m gt Int gt Conduit t m t trigrams ngram 3 countFre
  • Haskell Data.Decimal 作为 Aeson 类型

    是否可以解析一个数据 十进制 https hackage haskell org package Decimal 0 4 2 docs Data Decimal html使用 Aeson 包从 JSON 获取 假设我有以下 JSON foo
  • 如何打乱列表?

    如何从一组数字 1 2 3 直到我击中x 我的计划是重新调整列表 1 2 3 并把它砍在x chopAt 3 2 3 1 2 3 chopAt 3 2 1 3 2 1 3 chopAt 3 3 1 2 3 chopAt chopAt x y
  • 类型级别集结合律的证明

    我试图证明类型级函数Union https hackage haskell org package type level sets 0 8 5 0 docs Data Type Set html t Union是关联的 但我不确定应该如何完
  • 关于“没有绑定的类型签名”的错误

    我在 Haskell 中遇到 ASCII 问题 fromEnum Char gt Int toEnum Int gt Char offset Int offset fromEnum A fromEnum a toUpper Char gt
  • 纯 Haskell 代码需要线程池吗?

    In 现实世界 Haskell 第 28 章 软件事务内存 http book realworldhaskell org read software transactional memory html 开发了一个并发网络链接检查器 它获取网
  • RankN多态性和令人发指的克莱斯利之箭

    我不明白为什么 demobind1 的定义会产生一些编译器错误 它看起来像一个愚蠢的翻转 但不知何故 LANGUAGE GADTs LANGUAGE RankNTypes ScopedTypeVariables TypeOperators
  • Haskell 项目可以使用 cmake 吗?

    我正在计划一个用 Haskell 编写的项目 也许也有一些部分是用 C 编写的 对于构建系统 我决定不选择 Haskell 程序 cabal 的常见选择 主要是因为我想了解其他语言的构建程序是如何工作的 我听说过 CMake 我认为这是一个
  • 如何在 Haskell Pipes 中将两个 Consumer 合并为一个?

    我使用Haskell流处理库pipes https hackage haskell org package pipes编写一个命令行工具 每个命令行操作都可以将结果输出到stdout并记录到stderr with pipes API I n
  • Haskell 点运算符

    我尝试在 Haskell 中开发一个简单的平均函数 这似乎有效 lst 1 3 x fromIntegral sum lst y fromIntegral length lst z x y 但是为什么下面的版本不行呢 lst 1 3 x f
  • 为什么 mod 在表达式中给出的结果与在函数调用中给出的结果不同?

    假设有人想要计算函数 f x y x mod 3 y mod 3 mod 2 那么 如果再展开f 1 0 手动 可以得到 1 mod 3 0 mod 3 mod 2 1 然而 如果使用内联函数 结果是 let f x y x mod 3 y
  • 如何只修改记录的一个字段而不完全重写它? [复制]

    这个问题在这里已经有答案了 It s the second time I m tackling this problem And for the second time this is while working with the Stat
  • 为什么我不能声明推断类型?

    我有以下内容 runcount Eq a Num b gt a gt b runcount runcountacc 0 runcountacc Eq a Num b gt b gt a gt b runcountacc n runcount
  • 如何让 esqueleto 为我生成 SQL 字符串?

    我怎样才能让esqueleto从a生成一个SQL字符串from陈述 的文档toRawSql说 你可以打开持久的查询日志记录 我尝试了所有可能的形式MonadLogger我可以理解 但它从未打印任何 SQL 同一文档还说 手动使用此功能 是可
  • 检索 Haskell 项目中所有导入的列表

    因此 我的最终目标是通过确保项目导入的所有实体都存在于其声称可以使用的版本中 来评估 cabal 文件中依赖项的准确性 一个好的开始是找到单个源文件使用的所有导入实体的列表 可选地包含有关它们来自何处的信息 我愿意暂时忽略类实例的情况 因为
  • 优化 Haskell 内循环

    仍在 Haskell 中进行 SHA1 实现 我现在已经有了一个有效的实现 这是内部循环 iterateBlock Int gt Word32 gt Word32 gt Word32 gt Word32 gt Word32 gt Word3
  • Haskell 中的内部爆炸模式是否总是强制使用外部构造函数?

    在 Haskell 中 是否存在对于数据类型 LANGUAGE BangPatterns import Control DeepSeq data D D Int 实例 instance NFData D where rnf D 与具有另一个
  • 不同类型的列表?

    data Plane Plane point Point normal Vector Double data Sphere Sphere center Point radius Double class Shape s where inte
  • 承诺的反面是什么?

    承诺代表将来可能可用 或无法实现 的值 我正在寻找的是一种数据类型 它表示将来可能变得不可用的可用值 可能是由于错误 Promise a b TransitionFromTo

随机推荐