haskell - 读者单子(monad)的目的是什么?

标签 haskell monads reader-monad

阅读器单子(monad)是如此复杂,似乎无用。如果我没记错的话,在像 Java 或 C++ 这样的命令式语言中,对于 reader monad 没有等效的概念。

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

最佳答案

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

有两种接近 monad 的方法:我们可以问

  • monad 是什么 ?它配备了哪些操作?到底有什么好处呢?
  • monad 是如何实现的?它从哪里产生?

  • 从第一种方法来看,reader 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
    

    那么我们如何使用它呢?好吧,reader monad 非常适合通过计算传递(隐式)配置信息。

    任何时候你在计算中都有一个在不同点需要的“常数”,但实际上你希望能够使用不同的值执行相同的计算,那么你应该使用 reader monad。

    Reader monads 也被用来做 OO 人所说的 dependency injection .例如,negamax算法经常(以高度优化的形式)用于计算两人游戏中位置的值(value)。虽然算法本身并不关心你在玩什么游戏,但你需要能够确定游戏中的“下一个”位置是什么,并且你需要能够判断当前位置是否是胜利位置。
     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
    

    这将适用于任何有限的、确定的、两人游戏。

    即使对于不是真正依赖注入(inject)的事情,这种模式也很有用。假设你从事金融工作,你可能会设计一些复杂的逻辑来为 Assets 定价(比如衍生品),这一切都很好,而且你可以在没有任何臭名昭著的单子(monad)的情况下做到这一点。但是,您修改程序以处理多种货币。您需要能够即时在货币之间进行转换。您的第一次尝试是定义一个顶级函数
    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)
    

    我们想为这种语言编写一个评估器。为此,我们需要跟踪一个环境,它是与术语关联的绑定(bind)列表(实际上它将是闭包,因为我们想要进行静态范围界定)。
     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
    

    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只是 idlocal只是功能组合的功能顺序切换!

    关于haskell - 读者单子(monad)的目的是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14178889/

    相关文章:

    列表类型错误的 Haskell 列表

    Haskell concat 在列表理解中复制

    haskell - 小型命令式语言的解释器

    haskell - f, g, h::Kleisli ((->) e) a b <=> f >>> (g &&& h) = (f >>> g) &&& (f >>> h)?

    haskell - 什么是 "a function that you call"和什么是 "a function that call you"?

    haskell - 如何将 Reader Monad 与 (Int -> Int) 一起使用?

    haskell - 访问元组中的字段

    Haskell IO Int 和 Int

    c++ - 是否有人使用带有 Expected<T> 的 monadic 绑定(bind)式编程

    haskell - vim:保存时调用函数