Haskell IO:将IO字符串转换为“其他类型”

3
我有一个 Haskell 程序,它以文件作为输入并将其转换为二叉搜索树。
import System.IO    

data Tree a = EmptyBST | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

ins :: Ord a => a -> (Tree a) -> (Tree a)
ins a EmptyBST                  = Node a EmptyBST EmptyBST
ins a (Node p left right)
    | a < p                             = Node p (ins a left) right
    | a > p                             = Node p left (ins a right)
    | otherwise                             = Node p left right



lstToTree :: Ord a => [a] -> (Tree a)
lstToTree                   = foldr ins EmptyBST

fileRead                    = do    file    <- readFile "tree.txt"
                            let a = lstToTree (conv (words file))
                            return a

conv :: [String] -> [Int]
conv                        = map read

然而,当我运行以下命令时:

ins 5 fileRead 

我遇到了以下错误:

<interactive>:2:7:
    Couldn't match expected type `Tree a0'
                with actual type `IO (Tree Int)'
    In the second argument of `ins', namely `fileRead'
    In the expression: ins 5 fileRead
    In an equation for `it': it = ins 5 fileRead

请问有人能帮助我吗?

谢谢

2个回答

7
如果您为fileRead提供了类型标记,那么问题立刻就会显现出来。让我们来看一下 GHC 将会内部分配给fileRead的类型注释:
fileRead = do file <- readFile "tree.txt"
              let t = lstToTree $ map read $ words file
              return t

lstToTree :: Ord a => [a] -> Tree a 是一个将列表转换为二叉树的函数,而read总是返回Read类型类的成员。因此,t :: (Read a, Ord a) => Tree a。具体的类型取决于文件内容。

return用单子包装其参数,因此return t的类型为Ord a, Read a => IO (Tree a)。由于return tdo块中的最终语句,它成为fileRead的返回类型。

fileRead :: (Read a, Ord a) => IO (Tree a)

所以,fileRead 是一个被 IO 包装的 Tree,你不能直接将它传递给 ins,因为它期望一个独立的 Tree。你无法从 IO 中取出 Tree,但你可以将函数 ins 提升到 IO monad 中。

Control.Monad 导出 liftM :: Monad m => (a -> r) -> (m a -> m r)。 它接受一个常规函数,并将其转换为在像 IO 这样的 monad 上执行的函数。实际上,它是标准 Prelude 中 fmap 的同义词,因为所有的 monads 都是 functors。 因此,这段代码与 @us202 的代码大致相等,它获取 fileRead 的结果,插入 5,然后将结果包装在 IO 中。

liftM (ins 5) fileRead
-- or --
fmap (ins 5) fileRead

我建议使用fmap版本。这段代码只利用了IO是一个functor的事实,因此使用liftM意味着读者可能需要它成为一个monad。
"lifting"是一种在monads或functors中使用纯函数的通用技术。如果您不熟悉lifting(或者如果您对monads和functors感到困惑),我强烈推荐Learn You A Haskell第11-13章。
注意fileRead的最后两行应该合并,因为return实际上没有任何作用。
fileRead :: (Read a, Ord a) => IO (Tree a)
fileRead = do file <- readFile "tree.txt"
           return $ lstToTree $ map read $ words file

或者,由于这是一个足够简短的函数,您可以完全放弃do符号,并再次使用fmap

fileRead :: (Read a, Ord a) => IO (Tree a)
fileRead = fmap (lstToTree . map read . words) (readFile "tree.txt")

针对您的评论进行编辑:

Haskell被有意设计为将执行IO的代码与常规代码分开。这背后有一个非常好的哲学原因:大多数Haskell函数都是“纯”的——也就是说,它们的输出仅取决于输入,就像数学中的函数一样。你可以运行一个纯函数一百万次,你总会得到相同的结果。我们喜欢纯函数,因为它们不会意外地破坏程序的其他部分,它们允许懒惰计算,并且它们允许编译器为你积极地优化代码。

当然,在现实世界中,我们需要一点点不纯洁。像getLine这样的IO代码不可能是纯的(而没有执行IO的程序是无用的!)。getLine的结果取决于用户键入了什么:你可以运行getLine一百万次,每次都得到不同的字符串。Haskell利用类型系统来使用类型IO标记不纯洁的代码。

这是问题的关键:如果你在从不纯的数据中获取的数据上使用纯函数,则结果仍然是不纯的,因为结果取决于用户的操作。因此,整个计算都属于IO单子。当你想将一个纯函数带入IO时,你必须提升它,可以明确地(使用fmap)或隐式地(使用do表示法)。

这是Haskell中非常常见的模式——看看我上面的fileRead版本。我使用fmap来使用纯函数操作不纯的IO数据。


1
嗯。问题是,我真正想做的是编写一个函数,可以调用并返回类型为 Tree a 的元素。也就是说,它通过 lstToTree 或类似方法读取文本文件中的列表,生成一棵树,然后返回该树,以便用户可以在其上运行其他类似于 ins 的函数。这可以直接完成吗? - user950356
@MohammedAl-Farhan - 这是一个非常好的问题,要回答得恰当需要超过600个字符。我已经更新了我的答案。 - Benjamin Hodgson

3
你不太可能真正地避免使用IO单子(除非使用不安全的函数),但在你的情况下实际上没有必要这样做。
main = do f <- fileRead
          let newtree = ins 5 f
          putStr $ show newtree

(现场演示:在这里

谢谢,但我希望用户输入他想要添加到列表中的任何元素。我不希望元素被固定。 - user950356
然后修改上面的代码:通过 getLine 读取元素并使用它,添加一些循环以从stdin中添加多个元素,你可以随意选择。 - us2012

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