祝! Haskellバージンスクリプトを書いた

in

テキストを読んでいるだけだとどうしてもダレてくるので思い切ってコードを書いてみました。お題は以下のようにデータベースから抽出したセミコロンで区切られた数値データを

30,773;50,838,846;7,230,463
1,637;1,410,874;288,999
30,218;39,266,091;13,714,126
23,434;53,026,153;6,621,278
6,181;2,781,656;850,041

桁数を揃えたレポートとして出力するスクリプトです。

      30,773  50,838,846   7,230,463
       1,637   1,410,874     288,999
      30,218  39,266,091  13,714,126
      23,434  53,026,153   6,621,278
       6,181   2,781,656     850,041

同じことをperlで作成したツールで毎日やっているのですが、これをHaskellで書き直してみるという試みです。仕様としては、

  • 数値項目は各行3つ必ず存在する
  • 各項目はセミコロンで区切られる
  • 数値はカンマ編集を含めて最大11桁まである

といったところ。はじめてのHaskellには手頃な素材です。以下、はじめてのHaskellスクリプトを作成していく過程です。

最初に取りかかったのはセミコロンによる、要素の区切り。これにはText.Regexを使いました。本当は正規表現なんか使う場面ではないのですが、他にに区切る方法が見つけらず妥協。とにかく動くモノを...

-- 文字列を指定の正規表現で区切る
splitreg :: SReg -> String -> [String]
splitreg = \r -> (\l -> splitRegex (mkRegex r) l) 

こんなカンジ。

*Main> splitreg ";" "123;456;789"
["123","456","789"]

12桁右寄せ左空白詰めは、"△△△△..△" ++ "n,nnn"して、後ろから12個取り出して編集。空白12個は練習の意味も含めて、文字と繰り返す個数を与えて文字列を生成する関数として作成しました。

-- cをn回繰り返した文字列を作成する
csply :: Char -> Int -> String
csply = \c -> (\n -> take n $ repeat c)

まっ、これで他の用途でも使えます。

*Main> csply ' ' 12
"            "
*Main> csply 'a' 10
"aaaaaaaaaa"

csplyで作成した12個の空白と連結した文字列の後ろ12個の文字を取り出すのは、C/C++やperlから抜け出せない思考回路では少し苦しかったです。とりあえず凡夫が思いついたのは回数分tailを繰り返すやり方。

-- リストlの後方n個の要素のリストを作成する
lastn :: Int -> [a] -> [a]
lastn n l = if (length l) > n then lastn n (tail l) else l

一応、再帰を使ってそれらしいです。

*Main> lastn 12 "            123"
"         123"

想定としてはcsplyで作った12個のスペースと連結した文字列を使うので、

*Main> lastn 12 (csply ' ' 12 ++ "123")
"         123"

という風に使う予定。一応、これで各数値を幅12の右寄せで揃えることができるはず。各要素を同じ幅に揃えたら今度はそれを1行分の文字列として連結します。

["         123","         456","         789"]
"         123         456         789"

最初は、concat関数の存在を知らずに、

catitem :: [String] -> String
catitem [] = ""
catitem (x:xs) = x ++ (catitem xs)

を作りましたが、もちろんconcatを使います。さらに、行の分割と元へ戻すのはlines/unlinesを使います。これで、素材が揃ったのであとは組み合わせて完成!

main = do cs <- getContents
          putStr $ unlines [concat $ fline l ";" | l <- lines cs]
          where
            fline :: String -> SReg -> [String]
            fline inp sep = 
              map (\y -> lastn 12 $ (csply ' ' 12) ++ y) [x|x<-splitreg sep inp]

とまあ、これで目的のレポートはめでたく出力することができました。とりあえず僕のHaskellバージンアプリ。とりあえずHello World以外に動けば素直にうれしい!。

初体験の感想としては、やっぱり標準ライブラリの良いリファレンスが欲しいです。「こんなことしたいならアノ関数」みたいなカンが働かないうちはとにかくリファレンスを繰るしかないないんですがこれが初学者には易しくないです。そして一番重要なのはやはり思考の転換がまだ"手続き型"で自由自在に切替がいかないこと。これをなんとかしないと。例えば、最後のmain関数は最初

main = do cs <- getContents
          putStr $ concat $ fline cs ";"
          where
            :

でした。まさにperlやruby感覚で思考が完全一行単位なってます。実行して結果をみるまでは全然気がつきませんでした。結局lines/unlinesを使ったのですが、そもそも”一行単位に処理”するなんていう思考に実装を合わせてしまっていいのかな?という気持ちを拭い去れません。もっと違った角度からアプローチしないとわざわざHaskellで実装する意味がないように思います。

今はまだモナドに関しては触れないでいます。テキストProgramming in HaskellもまだChap8で足踏み中だし。まだまだ先は長いですが、とりあえず第一歩目。世のHaskellerの方々には突っ込みどころ満載だと思いますのでソースを以下に晒しておきます。

添付サイズ
frm.hs1.02 KB
test_data.txt1.15 KB

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/333

Comments

なにがショックって。

> format = printf "%12s"

ですねー。必死で考えたのにぃ。

それにしても、簡潔なコードというものはいいですね、ありがとうございます。

精進します。

import Text.Regex
import Text.Printf

main = getContents >>= mapM_ (putStrLn . concat . map format . split ";") . lines

format = printf "%12s"
split sep = splitRegex (mkRegex sep)