从IO获取价值而不是计算本身

时间:2014-03-21 20:20:39

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

对于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 - 包装,但是当我们处理IO时,我认为没有办法解决问题......?

4 个答案:

答案 0 :(得分:8)

请记住,IO State不是实际状态,而是最终生成IO的{​​{1}}计算机的规范。我们将State视为 input - 机器转换器

IO

这里,提供了一个用于生成状态的机器,我们创建了一个更大的机器,它接受了传递状态并从输入行添加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 - 机器,它只是IO - 机器IO的略微修改。

现在让我们来看看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 - 生成IO的机器,State将生成一个get - 机器,它返回一个数字。请再次注意,IO已修复为始终使用由(固定,输入)get name st - 机器IO生成的状态。

让我们在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 rIO,从而生成两个(完全独立的)state - 机器{{ 1}}和IO。然后,我们依次评估他们的get l state效果并返回get r state - 结果的组合。

让我们检查IO中构建的op种机器的种类。在第一行中,我们生成了一个名为IO普通main - 机器,写为IO。这台state - 机器,每次运行时都不会产生任何副作用,以便返回新鲜,空白return Map.empty

在第二行中,我们生成了一种名为IO的新Map.Map机器。此IO - 计算机基于state' IO - 计算机,但它也请求输入行。因此,要清楚,每次state运行时,都会生成一个新的IO,然后读取输入行以读取存储在state'的某些Map.Map

应该清楚这是怎么回事,但现在当我们检查第三行时,我们看到我们将Int - "x" - 机器传递到state'。之前我们声明IO运行其输入eval - 机器两次,每个名称一次,然后合并结果。到目前为止,应该清楚发生了什么。

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

最后,我们使用IO表示法运行此Op计算机(eval箭头表示正在运行计算机)。显然它应该收集两个单独的行。


那么我们真正想做什么?好吧,我们需要模拟do monad中的环境状态,而不仅仅是传递(<-) s。使用IO即可轻松完成此操作。

Map.Map

答案 1 :(得分:4)

getLine中的IO行动包裹在IO中是很好的,但对我而言,您的问题似乎是您试图在IO monad中传递您的状态。相反,我认为这可能是你介绍monad变换器的时间,以及它们如何让你对Statemtl monad进行分层以获得两者的功能。

Monad变形金刚是一个非常复杂的话题,它需要一段时间才能到达你对它们感到满意的地方(我仍在不断地学习新东西),但它们是一个非常有用的工具当你需要分层多个monad时。您需要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

然后输入

Interpreter

此处Monad-- A utility function for kicking off an interpreter runInterpreter :: Interpreter a -> IO a runInterpreter interp = evalStateT interp Map.empty ,我们将在其中构建我们的解释器。我们还需要一种运行解释器的方法

Map.empty

我认为默认为input就足够了。

现在,我们可以在新monad中构建我们的解释器操作。首先,我们从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

我必须重命名get,以免与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

基本的想法是有一个“基础”monad,这里StateT AppState,这些行为被“提升”到“父”monad,这里getput类型类中的不同状态操作modifyMonadStateStateT有一个类型类实现,IO实现,并且为了提升liftIO行动有一个预先制作的IO功能,可以将ErrorT动作“提升”给父母monad。现在我们不必担心明确地传递我们的状态,我们仍然可以执行IO,它甚至简化了代码!

我建议阅读关于monad变换器的真实世界Haskell章节,以便更好地感受它们。还有其他有用的,例如ReaderT用于处理错误,WriterT用于静态配置,{{1}}用于聚合结果(通常用于记录),以及许多其他。这些可以分层到所谓的变换器堆栈中,并且制作自己的也不是很难。

答案 2 :(得分:3)

您可以传递IO State,然后使用更高级别的函数来处理IO,而不是传递State。您可以进一步让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

答案 3 :(得分: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中执行此操作。