【问题标题】:Get value from IO rather than the computation itself从 IO 而非计算本身获取价值
【发布时间】:2014-03-21 20:20:39
【问题描述】:

作为 Haskell 的新手,我目前正在尝试通过为一种简单的命令式玩具语言编写解释器来提高我的技能。

这种语言中的一个表达式是input,它从标准输入中读取一个整数。但是,当我将此表达式的值分配给一个变量,然后稍后再使用此变量时,似乎我实际上存储了读取值的计算,而不是读取的值本身。这意味着例如声明

x = input;
y = x + x;

将导致解释器调用输入过程三次次而不是一次。

在评估器模块内部,我使用Map 来存储变量的值。因为我需要处理 IO,所以它被包裹在一个 IO monad 中,如下面的最小示例所示:

import qualified Data.Map as Map

type State = Map.Map String Int
type Op = Int -> Int -> Int

input :: String -> IO State -> IO State
input x state = do line <- getLine
                   st <- state
                   return $ Map.insert x (read line) st

get :: String -> IO State -> IO Int
get x state = do st <- state
                 return $ case Map.lookup x st of
                            Just i -> i

eval :: String -> Op -> String -> IO State -> IO Int
eval l op r state = do i <- get l state
                       j <- get r state
                       return $ op i j

main :: IO ()
main = do let state = return Map.empty
          let state' = input "x" state
          val <- eval "x" (+) "x" state'
          putStrLn . show $ val

main 函数中的第二行模拟x 的赋值,而第三行模拟二元+ 运算符的求值。

我的问题是:如何解决这个问题,以使上面的代码只输入一次?我怀疑是 IO-wrapping 导致了问题,但是当我们处理 IO 时,我看不出有什么办法......?

【问题讨论】:

    标签: haskell functional-programming monads interpreter io-monad


    【解决方案1】:

    请记住,IO State 不是实际状态,而是IO 机器的规范,最终生成State。让我们将 input 视为 IO-machine 变压器

    input :: String -> IO State -> IO State
    input x state = do line <- getLine
                       st <- state
                       return $ Map.insert x (read line) st
    

    在这里,我们提供了一个生成状态的机器,我们创建了一个更大的机器,它接收传递的状态并从输入行添加read。同样,需要明确的是,input name stIO-machine,它是对 IO-machine st 的轻微修改。

    现在让我们看看get

    get :: String -> IO State -> IO Int
    get x state = do st <- state
                     return $ case Map.lookup x st of
                                Just i -> i
    

    这里我们有另一个IO-machine 变压器。给定一个名称和一个产生StateIO-machine,get 将产生一个返回一个数字的IO-machine。再次注意get name st 被固定为始终使用(固定,输入)IO-machine st 产生的状态。

    让我们将这些片段合并到eval

    eval :: String -> Op -> String -> IO State -> IO Int
    eval l op r state = do i <- get l state
                           j <- get r state
                           return $ op i j
    

    这里我们在同一个IO-machine state 上分别调用get lget r,从而产生两个(完全独立的)IO-machines get l stateget r state。然后我们依次评估他们的IO 效果,并返回他们结果的op-组合。

    让我们检查一下IO 内置的main 机器的种类。在第一行中,我们生成了一个琐碎的IO-machine,称为state,写作return Map.empty。这个IO-machine,每次运行时,为了返回一个新鲜,空白Map.Map,不会产生副作用。

    在第二行中,我们生成了一种新的IO-machine,称为state'。这个IO-machine 基于stateIO-machine,但它也请求输入行。因此,需要明确的是,每次state' 运行时,都会生成一个新的Map.Map,然后读取输入行以读取一些Int,存储在"x"

    应该很清楚这是怎么回事,但是现在当我们检查第三行时,我们看到我们将state'IO-machine)传递到eval。之前我们说过eval 运行其输入IO-machine 两次,每个名称一次,然后合并结果。至此,应该清楚发生了什么。

    总而言之,我们构建了一种特定类型的机器,它提取输入并将其读取为整数,并将其分配给空白 Map.Map 中的名称。然后,我们将这个IO-machine 构建成一个更大的,在两个单独的调用中使用第一个IO-machine 两次,以便收集数据并将其与Op 结合。

    最后,我们使用do 表示法运行这台eval 机器((&lt;-) 箭头表示正在运行机器)。显然它应该收集两个单独的行。


    那么我们真正想做的是什么?好吧,我们需要在IO monad 中模拟环境状态,而不仅仅是传递Map.Maps。使用IORef 很容易做到这一点。

    import Data.IORef
    
    input :: IORef State -> String -> IO ()
    input ref name = do
      line <- getLine
      modifyIORef ref (Map.insert name (read line))
    
    eval :: IORef State -> Op -> String -> String -> IO Int
    eval ref op l r = do
      stateSnapshot <- readIORef ref
      let Just i = Map.lookup l stateSnapshot
          Just j = Map.lookup l stateSnapshot
      return (op i j)
    
    main = do
      st <- newIORef Map.empty   -- create a blank state, embedded into IO, not a value
      input st "x"               -- request input *once*
      val <- eval st (+) "x" "x" -- compute the op
      putStrLn . show $ val
    

    【讨论】:

    • 您的 modifyIORef 调用缺少其 ref 参数,并且引用了不存在的变量 x。你想在这里做什么?
    • 啊,我的意思是复制粘贴更好。现已修复,谢谢。
    【解决方案2】:

    可以将getLine 之类的操作包装在IO 中,但在我看来,您的问题是您试图在IO monad 中传递您的状态。相反,我认为这可能是您了解 monad 转换器的时候了,以及它们如何让您将 IOState monad 分层以将两者的功能合二为一。

    Monad 转换器是一个非常复杂的主题,需要一段时间才能让您熟悉它们(我一直在学习关于它们的新东西),但它们是一个非常有用的工具当您需要分层多个单子时。您需要 mtl 库来执行此示例。

    首先,进口

    import qualified Data.Map as Map
    import Control.Monad.State
    

    然后输入

    type Op = Int -> Int -> Int
    -- Renamed to not conflict with Control.Monad.State.State
    type AppState = Map.Map String Int
    type Interpreter a = StateT AppState IO a
    

    这里的InterpreterMonad,我们将在其中构建我们的解释器。我们还需要一种方法来运行解释器

    -- A utility function for kicking off an interpreter
    runInterpreter :: Interpreter a -> IO a
    runInterpreter interp = evalStateT interp Map.empty
    

    我认为默认为 Map.empty 就足够了。

    现在,我们可以在新的 monad 中构建解释器操作。首先我们从input 开始。我们只是修改地图中的当前状态,而不是返回我们的新状态:

    input :: String -> Interpreter ()
    input x = do
        -- IO actions have to be passed to liftIO
        line <- liftIO getLine
        -- modify is a member of the MonadState typeclass, which StateT implements
        modify (Map.insert x (read line))
    

    我不得不重命名get,这样它就不会与Control.Monad.State 中的get 冲突,但它的作用与以前基本相同,它只是获取我们的地图并在其中查找该变量。

    -- Had to rename to not conflict with Control.Monad.State.get
    -- Also returns Maybe Int because it's safer
    getVar :: String -> Interpreter (Maybe Int)
    getVar x = do
        -- get is a member of MonadState
        vars <- get
        return $ Map.lookup x vars
    -- or
    -- get x = fmap (Map.lookup x) get
    

    接下来,eval 现在只查找我们映射中的每个变量,然后使用liftM2 将返回值保持为Maybe Int。我更喜欢Maybe的安全性,但如果你愿意,你可以重写它

    eval :: String -> Op -> String -> Interpreter (Maybe Int)
    eval l op r = do
        i <- getVar l
        j <- getVar r
        -- liftM2 op :: Maybe Int -> Maybe Int -> Maybe Int
        return $ liftM2 op i j
    

    最后,我们编写示例程序。它将用户输入存储到变量"x",将其添加到自身,然后打印出结果。

    -- Now we can write our actions in our own monad
    program :: Interpreter ()
    program = do
        input "x"
        y <- eval "x" (+) "x"
        case y of
            Just y' -> liftIO $ putStrLn $ "y = " ++ show y'
            Nothing -> liftIO $ putStrLn "Error!"
    
    -- main is kept very simple
    main :: IO ()
    main = runInterpreter program
    

    基本思想是有一个“基础”monad,这里是IO,这些动作被“提升”到“父”monad,这里是StateT AppState。在MonadState 类型类中,StateT 实现了不同状态操作getputmodify 的类型类实现,为了解除IO 动作,有一个预制的@ 987654354@ 函数将 IO 动作“提升”到父 monad。现在我们不必担心显式传递我们的状态,我们仍然可以执行 IO,它甚至简化了代码!

    我建议阅读有关 monad 转换器的 Real World Haskell 章节,以便更好地了解它们。还有其他有用的,例如用于处理错误的ErrorT,用于静态配置的ReaderT,用于聚合结果(通常用于日志记录)的WriterT,等等。这些可以分层到所谓的变压器堆栈中,而且自己制作也不是太难。

    【讨论】:

      【解决方案3】:

      您可以传递State,而不是传递IO State,然后使用更高级别的函数来处理IO。你可以更进一步,让geteval 没有副作用:

      input :: String -> State -> IO State
      input x state = do
          line <- getLine
          return $ Map.insert x (read line) state
      
      get :: String -> State -> Int
      get x state = case Map.lookup x state of
                      Just i -> i
      
      eval :: String -> Op -> String -> State -> Int
      eval l op r state = let i = get l state
                              j = get r state
                          in  op i j
      
      main :: IO ()
      main = do
          let state = Map.empty
          state' <- input "x" state
          let val = eval "x" (+) "x" state'
          putStrLn . show $ val
      

      【讨论】:

        【解决方案4】:

        如果您实际上是在构建一个解释器,那么您可能会在某个时候拥有一个要执行的指令列表。

        这是我对你的代码的粗略翻译(虽然我自己只是一个初学者)

        import Data.Map (Map, empty, insert, (!))
        import Control.Monad (foldM)                                                        
        
        type ValMap = Map String Int                                                        
        
        instrRead :: String -> ValMap -> IO ValMap                                          
        instrRead varname mem = do                                                          
            putStr "Enter an int: "                                                         
            line <- getLine                                                                 
            let intval = (read line)::Int                                                   
            return $ insert varname intval mem                                              
        
        instrAdd :: String -> String -> String -> ValMap -> IO ValMap                       
        instrAdd varname l r mem = do                                                       
            return $ insert varname result mem                                              
            where result = (mem ! l) + (mem ! r)                                            
        
        apply :: ValMap -> (ValMap -> IO ValMap) -> IO ValMap                               
        apply mem instr = instr mem                                                         
        
        main = do                                                                           
            let mem0 = empty                                                                
            let instructions = [ instrRead "x", instrAdd "y" "x" "x" ]                      
            final <- foldM apply mem0 instructions                                          
            print (final ! "y")                                                             
            putStrLn "done"
        

        foldM 将函数 (apply) 应用于起始值 (mem0) 和列表 (instructions),但在 monad 中这样做。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-12-29
          • 2021-04-23
          • 1970-01-01
          • 2019-02-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多