我有一个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
请有人可以帮助我吗?
由于
答案 0 :(得分:8)
如果您为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
将其参数包装在monad中,因此return t
的类型为Ord a, Read a => IO (Tree a)
。由于return t
是do
块中的最终语句,因此它将成为fileRead
的返回类型,因此
fileRead :: (Read a, Ord a) => IO (Tree a)
所以fileRead
是Tree
包裹在IO
中,您无法将其直接传递到ins
,因为它需要Tree
拥有。您无法从Tree
中取出IO
,但可以将函数ins
“提升到IO
monad中。< / p>
Control.Monad导出liftM :: Monad m => (a -> r) -> (m a -> m r)
。它接受一个常规函数,并将其转换为一个作用于IO
等monad的函数。它实际上是fmap
的同义词(在标准前奏中),因为所有monad都是仿函数。因此,此代码大致相当于@ us202,取fileRead
的结果,插入5
,并返回包含在IO
中的结果。
liftM (ins 5) fileRead
-- or --
fmap (ins 5) fileRead
我推荐fmap
版本。此代码仅使用IO
是一个仿函数的事实,因此使用liftM
向读者暗示您可能还需要它作为monad。
'Lifting'是在monad或functor包含的值上使用纯函数的一般技巧。如果你不熟悉解除(或者如果你对monad和functor感到困惑),我衷心推荐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
数据。
答案 1 :(得分:4)
你无法真正逃脱IO monad(除了通过不安全的函数),但在你的情况下没有实际需要这样做:
main = do f <- fileRead
let newtree = ins 5 f
putStr $ show newtree
(现场演示:here)