Haskell勉強会準備室6 (ファイル出力)

前回はラムダ式とwhere節について学びました。
これらは高階関数を活かすためのギミックでした。
今回はファイル入出力と、ガード、高階関数fold, filterについてご説明します。foldはmapと対になる大事な関数です。
(長くなったのでファイル出力だけにしました。次回、ファイル入力についてやります)

ファイル入出力

まずファイルの入出力について覚えていきます。
これを覚えれば、大きなデータを扱うことが出来、
より実践的なプログラミングが出来そうです。
具体的には、Web上の文章とか、PDFの文章とかをテキストファイル形式で保存しておいて、
それをHaskell側で読み込む、読み込んでデータ処理した結果を出力(セーブ)しておいて一旦終了、
後でそのファイルを入力(ロード)することで上記終了時の状態を再開できると言ったことが出来ます。
重要なのでしっかり学んでいきます。

ファイル出力

まずはファイルを出力してみます。出力した結果を後で入力する流れをとります。
具体的にはListの各要素を一行ずつ出力します。
どういうことかというと、

ns = [1, 3, 1, 11, 25, 100, 43]

であれば、

1
3
1
11
25
100
43

上のような文字列データを複数行もつテキストファイルを出力していきます。
それでは、mecabで文章を処理した結果を出力してみます。

mecabed <- getMecabed "今回はファイル入出力と、ガード、高階関数fold, filterについてご説明します。"
mecabedSh = map getTokenTag mecabed
listToFileJP mecabedSh "results.txt"

getTokenTagは前回作成した関数です。
これで、results.txtというファイルが作成されます。中身を見ると、

("今回","名詞")
("は","助詞")
("ファイル","名詞")
("入出力","名詞")
("と","助詞")
("、","記号")
("ガード","名詞")
("、","記号")
("高階","名詞")
("関数","名詞")
("fold","名詞")
(",","名詞")
("filter","名詞")
("について","助詞")
("ご","接頭詞")
("説明","名詞")
("し","動詞")
("ます","助動詞")
("。","記号")
("EOS","")

みたいな結果が文字列で得られているはずです。
listToFileJPは私が用意した手製の関数です。
ただし難しいと思った人は、ここで詰まっても面白くないので今回はこれで終わりでいいと思います。
このlistToFileJPを使っておけば、日本語文字列を含むリストデータをファイルに出力することは出来ます。
他のプログラミング言語を触っていないと、以下の解説はきつい気がします。
これまで棚上げにしていた、cshowUなどといった日本語表示用の関数の説明も併せて行います。

ファイル出力(詳細説明)

この関数が何をやっているかをご説明します。
内容を理解すればご自身で同様の関数を作成できるはずです。

listToFileJP :: Show a => [a] -> String -> IO ()
listToFileJP lis fpath = do
  handle <- openFile fpath WriteMode
  hSetEncoding handle utf8
  mapM_ (hPutStrLn handle . ushow) lis
  hClose handle

IO()の意味

例のごとく型シグナチャをまず見ます。この関数はaの型を持つデータのリストを受け取り、さらにStringを受け取り、IO()を返す関数になっています。
aについてはShowという限定がついています。
これは、aは何でも良いわけではなく「文字列の形で出力できる」ような型ですよ、という制約と見てください。
ghciで内容を確認できるたぐいの型になります。Showでない型としては、関数が挙げられます。
ghciでmapと打ち込んでみてください。

<interactive>:8:1: error:
    • No instance for (Show ((a0 -> b0) -> [a0] -> [b0]))
        arising from a use of ‘print’
        (maybe you haven't applied a function to enough arguments?)
    • In a stmt of an interactive GHCi command: print it

((a0 -> b0) -> [a0] -> [b0])はmapの型になりますが、
これはShowではありません、みたいなことを言っています。
関数の内容を表示する(確認する)ことは、確かに出来ませんね。
型制約(Type Constraint)については、後の回で改めてご説明します。
戻ります。
listToFileJPは第一引数が出力対象のリスト、第二引数がファイル名(正確にはファイルの出力先、パス)になります。
そして最終結果がIO()となっています

IOの意味

注目すべきはまずIOがついています。復習ですが、IOはHaskellがすべてを把握できないような領域、データのことでした。
getMecabedは出力がIO [String]になっていますが、これはMecabというソフトがHaskell外のもので、
そこから結果を得ていたのでIOがつくことになっています。
ファイルの出力もMecabと同様にHaskellの把握しきれない領域での行為(action)です。
WindowsやMac、LinuxといったOSが管理する領域のactionになります。なのでIOがついています。

()の意味

さらにデータを出力した後に何かを戻すわけではないので、()になっています。
C言語が分かる人向けだと()はvoidに相当しています。何もない(void)という意味ですね
何を言っているかというと、下のように打ち込んだ後でexportedを確認してみてください。

mecabed <- getMecabed "今回はファイル入出力と、ガード、高階関数fold, filterについてご説明します。"
mecabedSh = map getTokenTag mecabed
exported <- listToFileJP mecabedSh "results.txt"

()となっているはずです。listToFileJPはなにもデータを返していません。
getMecabedが[String]を返していたのと対照的です。

listToFileJP :: Show a => [a] -> String -> IO ()
listToFileJP lis fpath = do
  handle <- openFile fpath WriteMode
  hSetEncoding handle utf8
  mapM_ (hPutStrLn handle . ushow) lis
  hClose handle

戻ります。一行ずつ見ていきます。

  handle <- openFile fpath WriteMode

これはfpath(ファイル)をopenFileする(開く)、ただしWriteMode(書き込みモード)で、
という意味です。モードとしては他にReadMode(読み込みモード)がありますが、それはファイル入力の方で出てきます。
handleの型を見ると、、Handleと書かれていますね。何もヒントになりませんでした(笑)。
handleというのは、データの出入り口です。この出入り口を通してファイルとHaskell間でデータのやり取りをします。

  hSetEncoding handle utf8

次に、その出入り口(handle)にて、どういう文字コード(言語)でやり取りされるかを設定しています。
日本語は特殊で(Unicode + UTF8)という文字コードを指定してあげる必要があります。
英語を利用するだけならこういう設定は不要です。
UTF16とかUTF32とかもありますが、UTF8がよく使われるのでここではそうしています。

  mapM_ (hPutStrLn handle . ushow) lis

これは少し複雑なので、まずは簡単な例から段階的に説明します。

  hPutStrLn handle "hoge"

これで、handleに"hoge"という文字列をhPutStrLnという関数で書き込んでいます。
hPutStrLnは、handler, Put, String, line,を表しているのでしょう。
それぞれ、handlerにput(書き込む)する、String(文字列)を、lineで(一行で、行末で改行しての意)。
余談ですが、putStrLnという関数もあって、これはhandle向けでなくREPLに出力する際に使われます。
putの逆はgetで、こちらはファイル入力の際に出てきます。

  hPutStrLn handle (show 2020)

これは、Int型である2020を、show関数で文字列型に変換("2020")してから、handleに"2020"を書き込んでいます。
hPutStrLnは第二引数は文字列型である必要があります。Int型のままではダメです。
このあたりの融通の利かなさが最初は鬱陶しいと感じるかも知れません。
ただ、型の意識を持ついい練習です。幸いにもHIEがリアルタイムでエラーチェックしてくれるので、そうは手間ではないはずです。
マウスホバーすればどういうエラーかも表示してくれます。

  hPutStrLn handle (show "私")
  hPutStrLn handle (ushow "私")

次に"私"というのは日本語なので標準のshowではなくUnicode版のushowを使っておきます。
ushowはText.Unicode.Showというライブラリの関数です。

  mapM_ (\str -> hPutStrLn handle (ushow str)) ["私", "は", "Haskell", "を", "勉強しています"]

最後に、"私" :: String という単体ではなく、["私", "は", "Haskell", "を", "勉強しています"] :: [String]という文字列リストを出力します。
前回の復習ですが、単体向けの関数をリストに適用するにはmapを使えばよいです。
ただ、ここではmapではなくmapM_という関数を利用しています。
注目点は2つあります。Mとアンダーバーの意味です。
まず、Mはモナド(Monad)のMです。あとでモナドは詳しく説明します。IOはモナドです。ここではこのMはIOのことだと思ってください。
さらにアンダーバーはmapMの結果を受け取らない、という意味になります。
何を言っているかは、map, mapM, mapM_の型を比べると早いです。

*Main Lib> :t map
map :: (a -> b) -> [a] -> [b]
*Main Lib> :t mapM
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
*Main Lib> :t mapM_
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()

(Traversable t)とか(Foldable t)のtは今の文脈ではリストのことです。
さらに(Monad m)のmはIOのことです。
それを踏まえて上を書き換えると、、

*Main Lib> :t map
map   :: (a ->    b) -> [a] -> [b]
*Main Lib> :t mapM
mapM  :: (a -> IO b) -> [a] -> IO [b]
*Main Lib> :t mapM_
mapM_ :: (a -> IO b) -> [a] -> IO ()

少しスペースも入れてみました。
(a -> m b)は、、(\str -> hPutStrLn handle (ushow str))に相当しています。
後者の型を確認すると、、

(\str -> hPutStrLn handle (ushow str)) :: Show a => a -> IO ()

つまり、bは()ということです。更に書き換えます。

*Main Lib> :t map
map   :: (a ->    b) -> [a] -> [b]
*Main Lib> :t mapM
mapM  :: (a -> IO ()) -> [a] -> IO [()]
*Main Lib> :t mapM_
mapM_ :: (a -> IO ()) -> [a] -> IO ()

こんな感じです。
説明の準備が出来ました。
まずリストlisにmap系高階関数で適用する関数は、

(\str -> hPutStrLn handle (ushow str)) :: Show a => a -> IO ()

となっていて、IOを含んでいます。なので普通のmapは使えず、mapM等のMがついたmap系関数を使うひつようがあります。
次に、mapMとmapM_を比較すると、出力される型がIO [()]かIO ()かの違いが出ています。

listToFileJP :: Show a => [a] -> String -> IO ()
listToFileJP lis fpath = do
  handle <- openFile fpath WriteMode
  hSetEncoding handle utf8
  mapM_ (hPutStrLn handle . ushow) lis
  hClose handle

細かいことなのですが、listToFileJPの出力型はIO()になっていて、
_ <- openFile ..のような値束縛系は問題ないのですが、そういうのではない、副作用のみのアクションである
hSetEncoding, とhCloseは、出力型がIO()になっています。
REPLで確認してみてください。このように、型をIO()に合わせる必要があります。
なので、mapMではなくmapM_を使っています。
IO [()]だと型エラーになります。

  _ <- mapM_ (\str -> hPutStrLn handle (ushow str)) ["私", "は", "Haskell", "を", "勉強しています"]

上のようにopenFileと同様に<- を使っておけばmapMでも問題なしです。HIEがmapM_を使えとサジェッションしてきますが(笑)。
HIEさんは賢いですね。

  mapM_ (hPutStrLn handle . ushow) lis

λ式が冗長なので、上のように書き換えましょうとHIEさんが教えてくれますのでそうしておきます。

  hClose handle

最後に、これでファイルの出力が完了したのでファイルやり取りの出入り口(handle)を閉じておきます。
これでリストデータのファイル出力は完了です。

REPLへの日本語出力

これまでちょいちょい出てきたcshowUの意味がこれで理解できるかと思います。
引数lisを明示すると、

cshowU :: Show a => [a] -> IO ()
cshowU lis = mapM_ (putStrLn . ushow) lis
             mapM_ (hPutStrLn handle . ushow) lis

handleが消えているだけで同じなのがよく分かりますね
出力先がREPLなのかファイルなのかが違うだけです。

次回

今回はリストデータのファイル出力を学びました。次回はファイル入力を扱います。
やはり図がないときつそうなので、勉強会までに図を用意します。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする