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

Hal*_*ast 2 monads interpreter haskell functional-programming io-monad

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

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

x = input;
y = x + x;
Run Code Online (Sandbox Code Playgroud)

将导致解释器调用输入过程3次,而不是一个.

在评估器模块的内部,我使用a Map来存储变量的值.因为我需要处理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
Run Code Online (Sandbox Code Playgroud)

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

我的问题是:我如何解决这个问题,以便上面的代码只输入一次?我怀疑它是IO导致问题的 - 包装 - 但是当我们处理IO时,我认为没有办法解决这个问题.?

J. *_*son 8

请记住,这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
Run Code Online (Sandbox Code Playgroud)

在这里,提供了一个用于生成状态的机器,我们创建了一个更大的机器,它采用了通过状态并read从输入线添加了一个.同样,为了清楚,input name st是一个IO-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
Run Code Online (Sandbox Code Playgroud)

这里我们有另一台机器IO变压器.给定一个名称和一个IO产生a的机器State,get会生成一个IO返回数字的机器.再次注意的是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
Run Code Online (Sandbox Code Playgroud)

在此,我们呼吁get lget r每个在同一IO-machine state,从而产生两个(完全独立的)IO-machines get l stateget r state.然后我们IO一个接一个地评估它们的效果并返回op它们结果的组合.

让我们来看看IO内置的机器种类main.在第一行我们生产的琐碎IO-machine,叫state,写return Map.empty.这台机器IO每次运行时都不会产生任何副作用,以便返回一个新的,空白的Map.Map.

在第二行,我们生产了一种IO叫做的新型机器state'.这台机器IO基于state IO-machine,但它也要求输入线.因此,为了清楚起见,每次state'运行时,Map.Map都会生成一个新的,然后读取输入行以读取一些Int,存储在"x".

应该清楚这是怎么回事,但是现在当我们检查第三行时,我们看到我们通过state'IO- 机器,进入eval.之前我们说过,eval运行它的输入IO-machine两次,每个名称一次,然后结合结果.到目前为止,应该清楚发生了什么.

总之,我们构建了一种特定类型的机器,它绘制输入并将其作为整数读取,并将其分配给空白中的名称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
Run Code Online (Sandbox Code Playgroud)