2013-03-26 68 views
3

我有一個Haskell程序,它將文件作爲輸入並將其轉換爲二進制搜索樹。Haskell IO:將IO字符串轉換爲「其他類型」

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 

請沒有人能幫助我嗎?

感謝

回答

4

你真的無法逃脫IO單子(除了通過不安全的功能),但有沒有實際需要做的是,在你的情況:

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

(現場演示:here

+0

謝謝,但我希望用戶輸入他想添加到列表中的任何元素。我不希望元素被修復 – modafarhan 2013-03-26 17:00:24

+0

然後修改上面的內容:通過'getLine'讀取元素並使用它,添加某種循環來添加從標準輸入讀取的許多元素,無論您喜歡什麼。 – us2012 2013-03-26 17:02:59

8

如果您使用類型簽名提供fileRead,您將能夠立即看到問題。讓我們來找出類型註釋,GHC將在內部分配給fileRead

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

lstToTree :: Ord a => [a] -> Tree aread總是返回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) 

所以fileReadTree包裹在一個IO,因爲它希望你不能直接將它傳遞到ins一個Tree自己。您不能將Tree移出IO,但您可以將'擡起'功能ins轉換爲IO monad。

Control.Monad exports liftM :: Monad m => (a -> r) -> (m a -> m r)。它接受一個常規功能,並將它變成一個像IO這樣的單子。它實際上是fmap(在標準Prelude中)的同義詞,因爲所有單子都是仿函數。因此,這個代碼大致相當於@ us202,代碼結果爲fileRead,插入5,並返回包含在IO中的結果。我想推薦fmap版本。此代碼僅使用IO是仿函數的事實,因此使用liftM意味着讀者可能需要它成爲monad。

「升降」是在monad或函數中包裝的值上使用純函數的一般技術。如果你不熟悉提升(或者如果你對單子和函子感到困惑),我衷心推薦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

嗯。事情是,我真正希望能夠做的是產生一個可以調用的函數並返回一個Tree a類型的元素。即它讀取文本文件中的列表,通過lstToTree或類似方法生成樹,然後返回該樹,以便用戶可以運行類似於ins的其他函數。這可以直接完成嗎? – modafarhan 2013-03-27 12:13:44

+0

@ MohammedAl-Farhan - 這是一個非常好的問題,要做到這一點,正義將需要600多個字符。我已經更新了我的答案。 – 2013-03-28 02:58:56