34

Haskellに問題があります。次のようなテキストファイルがあります。

5.
7. 
[(1,2,3),(4,5,6),(7,8,9),(10,11,12)].

最初の2つの数字(上記の2と7)と最後の行のリストを取得するにはどうすればよいかわかりません。各行の終わりにドットがあります。

パーサーを作成しようとしましたが、「readFile」という関数がIOStringというモナドを返します。そのタイプの文字列から情報を取得する方法がわかりません。

私は文字の配列で作業することを好みます。「IO文字列」から[文字]に変換できる関数があるのではないでしょうか。

4

4 に答える 4

93

Haskell の IO について根本的な誤解があると思います。特に、あなたはこう言います。

'IO String' を [Char] に変換できる関数はないでしょうか?

いいえ、1は​​ありません。そのような関数がないという事実は、Haskell について最も重要なことの 1 つです。

Haskell は非常に原理主義的な言語です。「純粋な」関数 (副作用がなく、同じ入力を与えると常に同じ結果を返す) と「不純な」関数 (ファイルからの読み取り、印刷などの副作用がある) の区別を維持しようとします。画面への書き込み、ディスクへの書き込みなど)。ルールは次のとおりです。

  1. 純粋関数はどこでも使用できます (他の純粋関数、または非純粋関数内)。
  2. 不純な関数は、他の不純な関数内でのみ使用できます。

コードがピュアまたはインピュアとしてマークされる方法は、型システムを使用することです。次のような関数シグネチャが表示された場合

digitToInt :: String -> Int

この関数が純粋であることがわかります。a を与えるとStringan が返され、Intさらに同じを与えると常に同じものが返されIntStringます。一方、次のような関数シグネチャ

getLine :: IO String

の戻り値の型がでマークされているため、は不純です。明らかに(ユーザー入力の行を読み取る) は、常に同じ を返すとは限りません。これは、ユーザーが入力した内容に依存するためです。この関数を純粋なコードで使用することはできません。コード。一度行ったら二度と戻れません。StringIOgetLineStringIO

IOラッパーと考えることができます。たとえば のような特定の型が表示された場合、 "は、実行時に任意の I/O を実行し、型の何かを返すアクションであるx :: IO Stringことを意味すると解釈する必要があります" (Haskell では、とはまったく同じであることに注意してください)もの)。xStringString[Char]

IOでは、アクションから値にアクセスするにはどうすればよいでしょうか? 幸いなことに、関数の型mainIO ()(何らかの I/O を実行して を返すアクションであり()、何も返さないのと同じです) です。そのため、常にIO内部で関数を使用できますmain。Haskell プログラムを実行すると、main関数が実行されます。これにより、プログラム定義のすべての I/O が実際に実行されます。たとえば、ファイルから読み書きしたり、ユーザーに入力を求めたり、 stdout などへ

次のような Haskell プログラムの構造を考えることができます。

  • I/O を行うすべてのコードがタグを取得します (基本的に、ブロックIOに入れます)。do
  • I/O を実行する必要のないコードは、doブロック内にある必要はありません。これらは「純粋な」関数です。
  • 関数mainは、定義した I/O アクションを、プログラムが実行したいことを実行させる順序で並べます (任意の場所に純粋な関数を散在させます)。
  • を実行するmainと、これらすべての I/O アクションが実行されます。

では、これらすべてを考慮して、どのようにプログラムを作成しますか? さて、機能

readFile :: FilePath -> IO String

ファイルを として読み取りますString。したがって、それを使用してファイルの内容を取得できます。関数

lines:: String -> [String]

Stringaを改行で分割するのでString、それぞれがファイルの 1 行に対応する s のリストが得られます。関数

init :: [a] -> [a]

.リストから最後の要素を削除します (これにより、各行の最後の要素が削除されます) 。関数

read :: (Read a) => String -> a

は aを取り、それをorStringなどの任意の Haskell データ型に変換します。これらの関数を適切に組み合わせることで、プログラムが完成します。IntBool

実際に I/O を実行する必要があるのは、ファイルを読み取るときだけであることに注意してください。したがって、IOタグを使用する必要があるのはプログラムの唯一の部分です。プログラムの残りの部分は「純粋に」書くことができます。

あなたが必要としているのは記事The IO Monad For People Who Simply Don't Careで、あなたの質問の多くを説明しているようです。「モナド」という言葉を恐れないでください - Haskell プログラムを書くのにモナドとは何かを理解する必要はありません (私の回答で「モナド」という言葉を使っているのはこの段落だけであることに注意してください。今までに4回使用しました...)


これがあなたが書きたい(と思う)プログラムです

run :: IO (Int, Int, [(Int,Int,Int)])
run = do
  contents <- readFile "text.txt"   -- use '<-' here so that 'contents' is a String
  let [a,b,c] = lines contents      -- split on newlines
  let firstLine  = read (init a)    -- 'init' drops the trailing period
  let secondLine = read (init b)    
  let thirdLine  = read (init c)    -- this reads a list of Int-tuples
  return (firstLine, secondLine, thirdLine)

の出力へのnpfedwards適用に関するコメントに答えるには、それが を与えることを理解する必要があります。それを変数にバインドする ( を使用) 場合にのみ、基になる にアクセスできるため、それに適用できます。linesreadFile text.txtreadFile text.txtIO Stringcontents <-Stringlines

覚えておいてください:一度行くIOと二度と戻れません。


1unsafePerformIO名前が暗示するように、非常に安全ではないため、意図的に無視しています! 自分が何をしているのか本当にわからない限り、絶対に使用しないでください。

于 2012-06-27T15:49:03.883 に答える
13

プログラミング初心者として、私も s に戸惑いましたIOIO行ったら出てこないことを覚えておいてください。Chris はその理由について素晴らしい説明を書きました。IO Stringモナドでの使用方法の例をいくつか挙げると役立つかもしれないと思っただけです。ユーザー入力を読み取って を返すgetLineを使用しますIO String

line <- getLine 

getLineこれが行うのは、 からのユーザー入力をという名前の値にバインドすることだけlineです。これを ghci に入力:type lineすると、次のように返されます。

:type line
line :: String

ちょっと待って!getLineを返しますIO String

:type getLine
getLine :: IO String

では、 のIOネスはどうなったのgetLineでしょうか? <-起こったことです。<-あなたのIO友達です。IOこれにより、モナド内のによって汚染された値を引き出し、通常の関数で使用することができます。モナドは で始まるため、簡単に識別できますdo。そのようです:

main = do
    putStrLn "How much do you love Haskell?"
    amount <- getLine
    putStrln ("You love Haskell this much: " ++ amount) 

あなたが私のような人なら、すぐにそれliftIOがあなたの次善のモナド友人であり$、書く必要のある括弧の数を減らすのに役立つことに気付くでしょう。

では、どのように から情報を取得しますreadFileか? の出力readFileIO String次のようになります。

:type readFile
readFile :: FilePath -> IO String

次に、必要なのはあなたのフレンドリー<-です:

 yourdata <- readFile "samplefile.txt"

これを ghci に入力して のタイプを確認するとyourdata、単純なString.

:type yourdata
text :: String
于 2012-06-28T02:39:06.883 に答える
9

人々がすでに言っているように、1 つはreadStringFromFile :: FilePath -> IO Stringで、もう1 つは であるという 2 つの関数がある場合、この 2 つの関数をさまざまな方法で組み合わせることができるためdoTheRightThingWithString :: String -> Something、実際には から文字列をエスケープする必要はありません。IO

fmapfor ( is IO) :IOFunctor

fmap doTheRightThingWithString readStringFromFile

(<$>)for ( is and IO) :IOApplicative(<$>) == fmap

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

( )liftMの場合:IOliftM == fmap

import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile

(>>=)for IO( IOis , Monad) fmap == (<$>) == liftM == \f m -> m >>= return . f:

readStringFromFile >>= \string -> return (doTheRightThingWithString string)
readStringFromFile >>= \string -> return $ doTheRightThingWithString string
readStringFromFile >>= return . doTheRightThingWithString
return . doTheRightThingWithString =<< readStringFromFile

表記doあり:

do
  ...
  string <- readStringFromFile
  -- ^ you escape String from IO but only inside this do-block
  let result = doTheRightThingWithString string
  ...
  return result

あなたが得るたびにIO Something

なぜそのようにしたいのですか?さて、これであなたの言語に純粋参照透過的なプログラム (関数) ができます。これは、型が IO-free であるすべての関数が純粋であり、参照透過的であることを意味します。そのため、同じ引数に対して同じ値が返されます。たとえば、同じに対して同じをdoTheRightThingWithString返します。ただし、これは IO フリーではないため (ファイルが変更される可能性があるため) 毎回異なる文字列を返す可能性があるため、そのような純粋でない値を からエスケープすることはできません。SomethingStringreadStringFromFileIO

于 2012-06-28T07:50:24.100 に答える
5

このタイプのパーサーがある場合:

myParser :: String -> Foo

を使用してファイルを読み取ります

readFile "thisfile.txt"

次に、次を使用してファイルを読み取って解析できます

fmap myParser (readFile "thisfile.txt")

その結果は type になりIO Fooます。

このfmap手段myParserは、IO の「内部」で実行されます。

それを考える別の方法は、それに対してmyParser :: String -> Foofmap myParser :: IO String -> IO Foo.

于 2012-06-27T15:36:51.410 に答える