「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 によると、
これは、`IO’ という入れ物に String を入れて運び、 (>>=) で右側に中身だけ「よっこらせ」と降ろすということ。(多分)
lines は String が引数で、`IO’ の中身はここに投入される。
そして、必要な処理がされると、putStr によってまた`IO’ に入れられて出力。これが main 関数の返り値になる。
ところで、(.) により 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 値を反転させる。
リスト内包表記
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)) と束縛していたなら、パターンマッチを見ただけで定義において再帰的に処理がされているのでは (?) と予想できる。普通のリスト処理のときはそんなの当り前と思っていたけれど、フィールドにリストがある型に対してはそういう見方になってなかった。 ^^; 多分、読むだけでアップアップしてた。
0コメント:
コメントを投稿