web-dev-qa-db-ja.com

HaskellでIO Stringを解析するにはどうすればよいですか?

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

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

最初の2つの数値(上記の2と7)と最後の行からリストを取得する方法はわかりません。各行の終わりにドットがあります。

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

私は文字の配列で作業することを好みます。たぶん 'IO String'から[Char]に変換できる関数はありますか?

28
Simon

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

たぶん 'IO String'から[Char]に変換できる関数はありますか?

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

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

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

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

digitToInt :: String -> Int

あなたはこの関数が純粋であることを知っています。 Stringを指定すると、Intが返され、さらに同じIntを指定すると、常に同じStringが返されます。一方、関数シグネチャのような

getLine :: IO String

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

IOはラッパーと考えることができます。 x :: IO Stringなどの特定のタイプが表示された場合、「xは、任意のI/Oを実行し、タイプStringの何かを返すアクションです」と解釈する必要があります(Haskellでは、Stringおよび[Char]はまったく同じものです)。

では、IOアクションから値にアクセスするにはどうすればよいでしょうか。さいわい、関数mainのタイプはIO ()です(これは、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]

Stringを改行で分割します。これで、Stringsのリストができ、それぞれがファイルの1行に対応します。関数

init :: [a] -> [a]

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

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

Stringを受け取り、それをIntBoolなどの任意のHaskellデータ型に変換します。これらの関数を賢く組み合わせると、プログラムが得られます。

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

あなたが必要としているのは記事 The IO単に気にしない人のためのモナド であり、多くの質問を説明するはずです。 「モナド」という用語に怖い-あなたはHaskellプログラムを書くためにモナドが何であるかを理解する必要はありません(この段落は「モナド」という単語を使用する私の答えの中で唯一のものであることに注意してください。今は...)


これがあなたが書きたいプログラムだと思います

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)

readFile text.txtの出力にnpfedwardsを適用することについてのlinesコメントに回答するには、readFile text.txtIO Stringを提供することを理解する必要があります。これは、contents <-を使用して変数にバインドした場合にのみ、基礎となるStringにアクセスできるため、 linesをそれに適用できます。

覚えておいてください。IOにいったん戻ると、決して戻ることはありません。


1 名前からわかるように、unsafePerformIOは意図的に無視しています。あなたが本当にあなたが何をしているのかを知っているのでない限り、決してそれを使わないでください。

69
Chris Taylor

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

line <- getLine 

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

:type line
line :: String

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

:type getLine
getLine :: IO String

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

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

私のような人なら、liftIOがモナドの次の親友であり、$は書く必要のある括弧の数を減らすのに役立ちます。

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

:type readFile
readFile :: FilePath -> IO String

次に、必要なのはフレンドリーな<-だけです。

 yourdata <- readFile "samplefile.txt"

これをghciに入力してyourdataのタイプを確認すると、単純なStringであることがわかります。

:type yourdata
text :: String
9
pooya72

人々がすでに言っているように、2つの関数がある場合、1つは_readStringFromFile :: FilePath -> IO String_でもう1つは_doTheRightThingWithString :: String -> Something_である場合、IOから文字列をエスケープする必要はありません。この2つの機能をさまざまな方法で組み合わせます。

fmapIOの場合(IOFunctor):

_fmap doTheRightThingWithString readStringFromFile
_

_(<$>)_ for IOIO is Applicative and _(<$>) == fmap_)の場合:

_import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile
_

liftM for IO(_liftM == fmap_)の場合:

_import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile
_

_(>>=)_ for IOIO is Monadfmap == (<$>) == 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_を取得するたびに。

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

8
JJJ

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

myParser :: String -> Foo

そしてあなたは使用してファイルを読みます

readFile "thisfile.txt"

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

fmap myParser (readFile "thisfile.txt")

その結果はタイプIO Fooになります。

fmapは、myParserがIOの「内部」で実行されることを意味します。

もう1つの考え方は、myParser :: String -> Foofmap myParser :: IO String -> IO Fooです。

5
dave4420