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

誰でも私を助けることができますか?

ありがとう

4

2 に答える 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 aread常に型クラスのメンバーを返しますRead。だからt :: (Read a, Ord a) => Tree a。具体的な型は、ファイルの内容によって異なります。

returnはその引数をモナドにラップするのでreturn t、型はOrd a, Read a => IO (Tree a)です。はブロックreturn t内の最後のステートメントなので、 の戻り値の型になるので、dofileRead

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

afileReadTreean にラップされており、それ自体が a を想定しているため、IO直接渡すことはできません。をから取り出すことはできませんが、関数をモナドに「持ち上げる」ことはできます。insTreeTreeIOinsIO

Control.Monad をエクスポートしliftM :: Monad m => (a -> r) -> (m a -> m r)ます。通常の関数を受け入れ、 のようなモナドに作用する関数に変換しIOます。すべてのモナドは関手であるため、実際にはfmap(標準の Prelude では) の同義語です。したがって、このコードは @us202 のコードとほぼ同じで、 の結果をfileRead挿入5し、結果を にラップして返しますIO

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

バージョンをお勧めしfmapます。IOこのコードは がファンクターであるという事実を利用するだけなので、 usingliftMは読者にそれがモナドでもある必要があることを暗示しています。

「リフティング」は、モナドまたはファンクターにラップされた値に対して純粋な関数を使用するための一般的な手法です。リフティングに慣れていない場合 (またはモナドやファンクター全般に混乱している場合) には、Learn You A Haskellの 11-13 章を心からお勧めします。


PS。の最後の 2 行は、実際には何もしない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 関数は「純粋」です。つまり、数学の関数と同じように、出力は入力のみに依存します。純粋な関数を 100 万回実行しても、常に同じ結果が得られます。純粋関数は、誤ってプログラムの他の部分を壊すことがなく、遅延を許容し、コンパイラがコードを積極的に最適化できるため、私たちは純粋関数を好みます。

もちろん、現実の世界では少しの不純物が必要です。のような IO コードgetLineはおそらく純粋ではありません (そして IO を実行しないプログラムは役に立ちません!)。の結果はgetLine、ユーザーが何を入力したかによって異なります。100getLine万回実行しても、毎回異なる文字列を取得できます。Haskell は型システムを利用して、純粋でないコードに type というラベルを付けますIO

問題の要点は次のとおりです。純粋に取得されていないデータに対して純粋な関数を使用しても、結果はユーザーの操作に依存するため、結果は依然として不純です。IOしたがって、計算全体がモナドに属します。純粋な関数を導入したい場合は、明示的に ( を使用して) または暗黙的に (表記法を使用して)IOリフトする必要があります。fmapdo

fileReadこれは Haskell では非常に一般的なパターンです。上記の私のバージョンを見てください。私は純粋な関数fmapで不純なIOデータを操作していました。

于 2013-03-26T17:03:46.533 に答える
3

IO モナドを実際にエスケープすることはできません (安全でない関数を除く) が、実際にそれを行う必要はありません。

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

(ライブデモ:こちら)

于 2013-03-26T16:08:42.713 に答える