Haskell 入門


数値データの文字列変換

数値データをファイルに保存するためには、それを文字列に変換しないといけない。また、文字列で保存されたデータは数値に変換しないと使えない。数値データの文字列化と文字列の数値化は Haskell を使いこなすための必須知識だ。

数値の文字列化

数値を文字列化する関数は show だ。また、文字列を数値化するときは read cs :: Int のように read 関数の戻り値の型を指定する必要がある。

Prelude> show 777
"777"
Prelude> read "777" :: Int
777
Prelude> read "777" :: Double
777.0

また、リストなどの構造的なデータの場合は次のようになる。

Prelude> read "[1,2,3]" :: [Int]
[1,2,3]
Prelude> show (1, 'a')
"(1,'a')"
Prelude> read "(1,'a')" :: (Int, Char)
(1,'a')

したがって、ファイルに数値を保存したり、読み出したりするときには show と read を用いなければならない。

Prelude> writeFile "temp.txt" $ show [1,2,3]
Prelude> readFile "temp.txt"
"[1,2,3]"
Prelude> do {cs <- readFile "temp.txt"; return (read cs 
:: [Int])}
[1,2,3]

Show クラスと Read クラス

自分で定義した代数的データ型に show や read を使うためには、data 宣言するときに deriving ディレクティブでデータ型を Show クラスと Read クラスのインスタンス宣言しておく必要がある。

Prelude> data MyData = MyData Int Double deriving (Show, Read)
Prelude> show $ MyData 1 2
"MyData 1 2.0"
Prelude> show (MyData 1 2)
"MyData 1 2.0"
Prelude> read "MyData 1 2.0" :: MyData
MyData 1 2.0

これで Haskell で処理したデータを自由にファイルに保存したり読み出したりできる。

また、自前のデータ型は data 宣言の時に Show クラスのインスタンス宣言をしておかないと、ghci のコマンドラインで表示できない。それは ghci のコマンドラインでデータを表示する時に内部的に show 関数が使われるからだ。初めて代数的データ型を定義した時によくある落とし穴だ。

Prelude> data NewType = NewType Int
Prelude> NewType 1

:66:1:
No instance for (Show NewType) arising from a use of ‘print’
In a stmt of an interactive GHCi command: print it
Prelude> data NewType = NewType Int deriving Show
Prelude> NewType 1
NewType 1

数値データのファイルとのやりとり

数値データをファイルに保存したり読み出したりするには、データを文字列にしたり、文字列を数値化したりなどの操作の他に、空白や改行で区切られた文字列を切り分ける操作も必要だ。後者については Haskell には words, unwords, lines, unlines などの関数が用意されている。
Prelude> words "apple orange lemon"
["apple","orange","lemon"]
Prelude> unwords ["apple","orange","lemon"]
"apple orange lemon"
Prelude> lines "hello\nworld"
["hello","world"]
Prelude> unlines ["hello","world"]
"hello\nworld\n"

そこで、まずリスト [1,2,3,4,5] を改行区切りのデータにしてファイルに保存することを考えてみる。

Prelude> writeFile "temp.txt" $ unlines $ map show 
[1,2,3,4,5]
Prelude> readFile "temp.txt"
"1\n2\n3\n4\n5\n"

ファイルのデータを取り出してリストにするには次のようにする。

Prelude> do cs <- readFile "temp.txt"; return $ map (\x->read x :: Int) $
 lines cs
[1,2,3,4,5]

リストのリストの場合も簡単にできる。まずはファイルへの保存から。文章で表現するより ghci の実験結果を見てもらったほうが仕組みが分かりやすい。

Prelude> map (map show) mydata
[["1","2","3"],["4","5","6","7"],["8","9"]]
Prelude> map (\x -> unwords $ map show x) mydata
["1 2 3","4 5 6 7","8 9"]
Prelude> unlines $ map (\x -> unwords $ map show x) mydata
"1 2 3\n4 5 6 7\n8 9\n"
Prelude> writeFile "temp.txt" $ unlines $ map (\x -> unwords $ 
map show x) mydata
Prelude> readFile "temp.txt" >>= putStr
1 2 3
4 5 6 7
8 9

次にこれをリストのリストに復元する。

Prelude> do cs <- readFile "temp.txt"; print cs
"1 2 3\n4 5 6 7\n8 9\n"
Prelude> do cs <- readFile "temp.txt"; print $ lines cs
["1 2 3","4 5 6 7","8 9"]
Prelude> do cs <- readFile "temp.txt"; print $ map words $ lines cs
[["1","2","3"],["4","5","6","7"],["8","9"]]
Prelude> do cs <- readFile "temp.txt"; print $ map (map (read :: String 
-> Int)) $ map words $ lines cs
[[1,2,3],[4,5,6,7],[8,9]]

データのファイル保存と読み出しをもっとプログラムらしく整形してみると次のようになる。

module SaveData where

save_data filename datalist = do let datastring = unlines $ map (\x -> unwords $ map show x) datalist writeFile filename datastring

load_data filename = do cs <- readFile filename let datalist = map (map (read :: String -> Int)) $ map words $ lines cs return datalist

ghci でテストしてみた。

Prelude> :l SaveData
[1 of 1] Compiling SaveData ( SaveData.hs, interpreted )
Ok, modules loaded: SaveData.
*SaveData> save_data "temp.txt" [[1,2,3],[4,5]]
*SaveData> load_data "temp.txt"
[[1,2,3],[4,5]]