将IO和状态计算混合使用

4

作为一项练习,我正在用Haskell编写一个命令行逆波兰计算器。它的思路是提示输入(数字或运算符),并打印出新的堆栈。我的计划是将数字列表存储在状态单子上,并针对该列表执行运算。例如:

> 4
[4]
> 3
[3,2]
> 5
[5,3,2]
> +
[8, 2]

等等。

我首先尝试使用状态单子来在每个条目上进行输入和输出,以构建列表。由于同一函数中包含IO和State的组合,我已经陷入了困境。我的问题是,我还需要在输入上进行递归,以保持提示在输入第一个数字后继续。

到目前为止,这是我的代码:

module Main where

import Control.Monad.State

addEntry :: Int -> State [Int] Int
addEntry entry = do
  entries <- get
  put (entry : entries)
  return entry

mainLoop :: [Int] -> IO ()
mainLoop entries = do
  entry <- readLn
  newEntries <- execState (addEntry entry) entries
  print newEntries
  mainLoop newEntries

main :: IO ()
main = do
    print $ mainLoop []

这是我当前遇到的编译错误:

src/Main.hs@14:28-14:42 Couldn't match type [Int] with ‘IO [Int]’
Expected type: State (IO [Int]) Int
  Actual type: State [Int] Int
src/Main.hs@14:44-14:51 Couldn't match expected typeIO [Int]’ with actual type [Int] …

有没有关于如何构建这些函数的建议,使我不需要结合IO和State?

1
你是否考虑过使用 StateT [Int] IO Int 替代 StateT [Int] Identity Int(它与 State [Int] Int 相同)?这样,你就可以使用 liftIO 将 IO 操作提升到 StateT [Int] IO 单子中,在同一个 do-block 中执行有状态的操作和 IO 操作。 - bheklilr
1
虽然,如果您在文件顶部添加 {-# LANGUAGE FlexibleContexts #-},然后将 addEntry 的类型签名更改为 MonadState [Int] m => Int -> m Int,然后将 execState 更改为 execStateT,则可以使其编译通过,但是您还会遇到一个编译器错误,即 print $ mainLoop [] 应该只是 mainLoop [],因为它本身就会打印并且不返回任何内容。像这样 - bheklilr
在实际应用中,将IO包装的值传递到状态中是否很常见?除非实际状态计算由于某种原因依赖于IO,否则对我来说感觉很不好。 - Joe Fiorini
单子变换器的整个目的就是能够将不同种类的单子效应组合成一个更大的“堆栈”。你不是将IO包装的值传递作为状态,而是同时执行IO操作和StateT操作。因为你想要编写类似于RPN计算器的REPL,这意味着你想在执行IO操作的同时跟踪状态。你可以将所有实际的RPN操作移动到纯函数中,只要需要获取值和输出值时就使用StateT MyState IO - bheklilr
通常的做法看起来会像这样。这将分离出哪些部分可以进行IO操作,哪些部分只能进行有状态的操作。使用MonadState上下文使这些函数更容易组合,并且它将所有IO操作都保留在rpnREPL内部。 - bheklilr
非常感谢您的解释和示例!我认为在完成这个练习时,我会经常使用它作为参考。 - Joe Fiorini
1个回答

4

我不确定您是否使用状态是因为想尝试它,但是您可以在不必使用状态单子的情况下实现状态本身。

module Main where

addEntry :: Int -> [Int] -> [Int]
addEntry = (:)

mainLoop :: [Int] -> IO ()
mainLoop entries = do
  entry <- readLn
  let newEntries = addEntry entry entries
  print newEntries
  mainLoop newEntries

main :: IO ()
main = mainLoop []

我确实想尝试一下,我正在努力更加熟悉单子。但更重要的是,我希望尽可能保持简单。所以这非常有帮助! - Joe Fiorini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接