【问题标题】:Haskell IO: convert IO String to "Other type"Haskell IO:将 IO 字符串转换为“其他类型”
【发布时间】:2013-03-16 12:21:26
【问题描述】:

我有一个 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

请问有人可以帮我吗?

谢谢

【问题讨论】:

    标签: haskell binary-search-tree


    【解决方案1】:

    如果您为fileRead 提供类型签名,您将能够立即看到问题。让我们弄清楚 GHC 将在内部分配给fileRead 的类型注释:

    fileRead = do file <- readFile "tree.txt"
                  let t = lstToTree $ map read $ words file
                  return t
    

    lstToTree :: Ord a =&gt; [a] -&gt; Tree aread 始终返回 Read 类型类的成员。所以t :: (Read a, Ord a) =&gt; Tree a。具体类型取决于文件的内容。

    return 将其参数包装在一个单子中,因此return t 的类型为Ord a, Read a =&gt; IO (Tree a)。由于return tdo块中的最后一条语句,所以它变成了fileRead的返回类型,所以

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

    所以fileRead 是一个包裹在IO 中的Tree,您不能将它直接传递给ins,因为它本身需要一个Tree。您不能将 TreeIO 中取出,但您可以将函数 ins“提升”到 IO monad。

    Control.Monad 导出 liftM :: Monad m =&gt; (a -&gt; r) -&gt; (m a -&gt; m r)。它接受一个常规函数,并将其变成一个作用于像IO 这样的单子的函数。它实际上是 fmap 的同义词(在标准 Prelude 中),因为所有 monad 都是函子。所以这段代码,大致相当于@us202,获取fileRead的结果,插入5,然后返回包裹在IO中的结果。

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

    我推荐fmap 版本。这段代码只利用了IO 是一个函子这一事实,因此使用liftM 向读者暗示您可能也需要它是一个monad。

    'Lifting' 是对包裹在 monad 或 functor 中的值使用纯函数的通用技术。如果你不熟悉提升(或者如果你对一般的 monad 和 functors 感到困惑),我衷心推荐Learn You A Haskell 的第 11-13 章。


    PS。注意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 monad。当你想将一个纯函数引入IO 时,你必须显式(使用fmap)或隐式(使用do 表示法)解除它。

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

    【讨论】:

    • 嗯。问题是,我真正想做的是生成一个可以调用的函数并返回 Tree a 类型的元素。 IE。它读取文本文件中的列表,通过 lstToTree 或类似方法生成一棵树,然后返回该树,以便用户可以在其上运行类似于 ins 的其他函数。这可以直接完成吗?
    • @MohammedAl-Farhan - 这是一个非常好的问题,要做到公正需要 600 多个字符。我已经更新了我的答案。
    【解决方案2】:

    您无法真正逃避 IO monad(通过不安全函数除外),但在您的情况下实际上不需要这样做:

    main = do f <- fileRead
              let newtree = ins 5 f
              putStr $ show newtree
    

    (现场演示:here

    【讨论】:

    • 谢谢,但我希望用户输入他想添加到列表中的任何元素。我不希望元素被固定
    • 然后修改上面的内容:通过getLine读取元素并使用它,添加某种循环来添加从stdin读取的许多元素,无论你喜欢什么。
    猜你喜欢
    • 2011-09-17
    • 1970-01-01
    • 2011-08-22
    • 1970-01-01
    • 2023-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多