对于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时,我认为没有办法解决问题......?
答案 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 r
和IO
,从而生成两个(完全独立的)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变换器的时间,以及它们如何让你对State
和mtl
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,这里get
。 put
类型类中的不同状态操作modify
,MonadState
和StateT
有一个类型类实现,IO
实现,并且为了提升liftIO
行动有一个预先制作的IO
功能,可以将ErrorT
动作“提升”给父母monad。现在我们不必担心明确地传递我们的状态,我们仍然可以执行IO,它甚至简化了代码!
我建议阅读关于monad变换器的真实世界Haskell章节,以便更好地感受它们。还有其他有用的,例如ReaderT
用于处理错误,WriterT
用于静态配置,{{1}}用于聚合结果(通常用于记录),以及许多其他。这些可以分层到所谓的变换器堆栈中,并且制作自己的也不是很难。
答案 2 :(得分:3)
您可以传递IO State
,然后使用更高级别的函数来处理IO,而不是传递State
。您可以进一步让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
答案 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中执行此操作。