2008年10月25日土曜日

Haskell で簡単なテキスト処理 (2)

Haskell で簡単なテキスト処理 - データの抽出」の続き。もう一度、前回と同じ処理を書いてみる。

対象のデータは、「Python で簡単なテキスト処理 (2) - データの抽出」で使った以下の 気象データ 。ここから、気圧 (1列目) と気温 (6列目) のデータのみを抽出したい。

1
998.7 1003.1 -- -- -- 6.0 10.9 1.8 36 16 2.6 4.8 北西 9.4 北西 6.9 -- -- 晴後一時曇 快晴
2
1007.2 1011.7 -- -- -- 6.2 11.2 1.1 35 19 3.1 6.6 北西 13.3 北西 9.1 -- -- 快晴 快晴
3
1011.6 1016.1 -- -- -- 5.9 10.4 1.5 45 29 1.8 3.6 北北西 6.4 南南東 9.1 -- -- 晴 快晴
4
1014.6 1019.1 -- -- -- 7.0 12.1 2.7 43 24 2.5 5.0 北西 9.4 北北西 9.0 -- -- 快晴 晴後曇
5
1014.0 1018.6 0.0 0.0 0.0 6.0 8.7 4.0 53 39 2.0 3.8 北西 6.1 西北西 3.7 --

※データファイルは UTF-8 で保存。

 

例1

まずは、なるべくシンプルに書くにはどうしたらいいかという視点で考えると、

import qualified System.IO.UTF8 as U
import Text.Regex.Posix

main = U.getContents >>= U.putStr . unlines . extract . lines
extract = map col . filter (\x -> not $ x =~ "^[[:digit:]]{1,2}$|^$")
col line = let ds = words line 
           in ds !! 0 ++ "\t" ++ ds !! 5

さすがに Ruby のようなシンプル書き方はできないかなぁ…。

追記(2008.11.7) : filter の第1引数を関数合成に変更するなら、

extract = map col . filter (not . (=~ "^[[:digit:]]{1,2}$|^$"))

 

IO モナド

前回と違うところは、main において do 式を使わずに (>>=) で置き換えたこと。 (>>=) は「左の箱の中身を、右の箱の中に流し込むための装置」と理解している。(これが正しいのかよくわからないのだけれど…。)  do 式では渡された中身を変数に束縛していたが、(>>=) を使うと省略できる。

※ モナドについてはまだよくわからない。(+_+) ただし、Meet the Monads の以下の部分を読んで、イメージとして何か「入れ物」と「中身」の扱いについてのメタ的な記述だというくらいの認識にはなった。 ^^;

多相型は多くの異る型の値を保持できるコンテナのようなものです。… Haskell ではこのコンテナの型も多相にすることができます。それゆえ、「m a」と書いて、ある型の値を保持するある型のコンテナを表現することができます。

 

Meet the Monads によると (>>=) の型は、

(>>=) :: m a -> (a -> m b) -> m b

関数の合成は、(cf. Haskell の関数合成)

(.) :: (b –> c) –> (a –> b) –> (a –>c)

getContens の型は、Prelude によると、

getContents :: IO String

これは、`IO’ という入れ物に String を入れて運び、 (>>=) で右側に中身だけ「よっこらせ」と降ろすということ。(多分)

lines は String が引数で、`IO’ の中身はここに投入される。

lines :: String -> [String]

そして、必要な処理がされると、putStr によってまた`IO’ に入れられて出力。これが main 関数の返り値になる。

putStr :: String -> IO ()

 

ところで、(.) により putStr から lines までが、Stirng –> IO() という型の一つの関数に合成される。

IO モナド によると、

Haskell では、トップレベルの main 関数の型は IO () でなければなりません。

(>>=) の関数の型を今回の場合に当てはめると、

IO String –> (String –> IO ()) –> IO ()

これで String が投入されると IO () が返され、main 関数の型に沿うようになる。

モナドついてわからないけれど、外見は上記のような理解でいいのだろうか… ^^;

 

正規表現

Haskell で正規表現 (2) =~ , Python・Ruby と比較しながら を参照。

数字を表わすには `\d’ ではなく、Rx - Posix Basic Regular Expressions によると、

[[:digit:]]

正規表現に一致しないものを抽出したい場合、not 関数 で Bool 値を反転させる。

not :: Bool -> Bool

 

リスト内包表記

map と filter を合成している関数をリスト内包表記に置き換えると、 

extract css = [col cs| cs <- [cs| cs <- css, not $ cs =~ "^[[:digit:]]{1,2}$|^$"]]   

しかし、あまり見やすくならないので map, filter の合成を使うことに。

map f . filter match

一つのパターンとして覚えておくといいかも。

 

例2

main 関数において、行を抽出した後に、列を抽出するという意図を明確にしたいなら、

import qualified System.IO.UTF8 as U
import Text.Regex.Posix

main = U.getContents >>= U.putStr . col . row
row = filter (\x -> not $ x =~ "^[[:digit:]]{1,2}$|^$") . lines
col = unlines . map col'
    where col' line = let ds = words line
                      in ds !! 0 ++ "\t" ++ ds !! 5 

line – unlines の組み合わせを別々の関数の入口と出口に配置し、filter ・ map をそれぞれ行・列の抽出処理に対応させるようにしてみた。例1.と比べて、どちらが理解しやすく書きやすいだろう?

filter に適用する無名関数が長いと感じたら、filter match として、match 関数を作成。

match cs = not $ cs =~ "^[[:digit:]]{1,2}$|^$"

 

例3

なるべくシンプルにしたいと思って、列の抽出をまとめていったら、逆に長くなってしまった。(+_+) 抽出する列数が少なければあまり意味がなさそう。

import qualified System.IO.UTF8 as U
import Text.Regex.Posix
import Data.List

main = U.getContents >>= U.putStr . unlines . row . lines
row = map col . filter match
match cs = not $ cs =~ "^[[:digit:]]{1,2}$|^$"
col cs = intercalate "\t" $ col' [0,5] cs
col' [] _ = [""]
col' (x:xs) cs = (col'' x) : (col' xs cs)
    where
      col'' i = (words cs) !! i

(cf. Haskell でリストの要素を join - List.Data の intersperse, intercalate)

 

例4

データを「表」とみなし、表は「列のリスト」からなり、列は「文字列のリスト」からなると考えると、

import qualified System.IO.UTF8 as U
import Data.List

data Table = Table [Row] 
instance Show Table where
    show (Table xs) = concatMap show xs

data Row = Row [String]
instance Show Row where
    show (Row xs) = intercalate "\t" xs ++ "\n"

main = U.getContents >>= U.print . filterCol . filterRow . readTable

filterRow :: Table -> Table
filterRow (Table rows) = Table $ filter match rows
    where
      match :: Row -> Bool
      match (Row cells) = if length cells > 1 then True else False

filterCol :: Table -> Table
filterCol (Table rows) = Table $ map (col [0,5]) rows
    where
      col :: [Int] -> Row ->  Row
      col is (Row cells) = Row $ map (cells !!) is

readTable :: String -> Table
readTable css = Table $ map readRow $ lines css

readRow :: String -> Row
readRow cs = Row $ words cs

データを readTable , readRow 関数によって Table 型の値に一度変換し、filterRow, filterCol によって必要な部分を抽出するようにした。そのため、ここでは正規表現を利用していない。

各関数はシンプルになったけれど、これはちょっと長すぎる気が。うーん… (+_+)

それにしても、こういう文字列から値の変換は、8.3 Read クラスと Show クラス のように Read を使うのかな?

 

感想

実際に関数を定義してみて実感したことがあった。それは、パターンマッチの書き方、つまり、フィールドの値を変数に束縛する仕方が、定義でどのように値を使いたいかを暗示しているということ。例えば、上記の filterRow 関数において (Row cells) としているところを (Row (x:xs)) と束縛していたなら、パターンマッチを見ただけで定義において再帰的に処理がされているのでは (?) と予想できる。普通のリスト処理のときはそんなの当り前と思っていたけれど、フィールドにリストがある型に対してはそういう見方になってなかった。 ^^; 多分、読むだけでアップアップしてた。


関連記事