3

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

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

x = input;
y = x + x;

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

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

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 时,我看不出有什么办法……?

4

4 回答 4

9

请记住,这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 st的是-machine 是对-machineIO的轻微修改。IOst

现在让我们检查一下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 变压器。给定一个名称和一个IO产生 a 的 -machine Stateget将产生一个IO返回一个数字的 -machine。再次注意,get name st固定始终使用 (fixed, input) 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

在这里,我们在同一台机器上调用get l和,从而产生两个(完全独立的)机器和。然后我们一个接一个地评估它们的效果,并返回它们结果的组合。get rIOstateIOget l stateget r stateIOop

让我们检查一下IO内置的机器类型main。在第一行中,我们产生了一个普通IO的机器,叫做state, writtenreturn Map.empty。这IO台机器在每次运行时都不会执行任何副作用以返回一个新鲜的空白Map.Map

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

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

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

最后,我们eval使用do符号来运行这台机器((<-)箭头表示正在运行这台机器)。显然它应该收集两条单独的行。


那么我们真正想做的是什么?好吧,我们需要在IOmonad 中模拟环境状态,而不仅仅是传递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
于 2014-03-21T21:07:21.283 回答
4

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

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以使其不与getfrom冲突Control.Monad.State,但它的作用与以前基本相同,它只是获取我们的地图并在其中查找该变量。

-- 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

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

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

于 2014-03-21T20:54:13.003 回答
3

IO State您可以传递State然后使用更高级别的函数来处理IO ,而不是传递一个。您可以走得更远,get并且eval没有副作用:

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
于 2014-03-21T20:35:07.243 回答
0

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

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

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 中执行此操作。

于 2014-03-21T21:21:40.583 回答