2011-11-29 (Tue) [長年日記]
_ RubyとHaskellによる簡単なCSVファイルの集計
簡単なCSVファイルの集計をHaskellでやろうと思ったら結構大変だったのでメモ。 IO返す関数をはじめて書いた気がする。
問題
以下のような形式の複数のCSVファイルをコマンドライン引数で受け取り、Applicant Nameごとに得点を集計して、合計得点でソートした結果を出力する。
Applicant Name,Project Title,Productivity and performance,Originality and creativity,Feasibility,Jadges point,Comment Shugo Maeda,Introducing list comprehensions into Ruby,5,1,7,3,面白いけど必要ないと思います。 ...
Ruby版 (30行)
Haskellで書こうと思ったけど手が出なかったので、とりあえずRubyで書いた。 色々やり方はあると思うけど、なるべく副作用を使わないやり方で。 flat_map便利。
class Assessment < Struct.new(:applicant, :title, :productivity, :originality, :feasibility, :judges_point) def +(other) self.class.new(self.applicant, self.title, self.productivity + other.productivity, self.originality + other.originality, self.feasibility + other.feasibility, self.judges_point + other.judges_point) end def total productivity + originality + feasibility + judges_point end def to_s [*to_a, total].join(",") end end assessments = ARGV.flat_map { |path| File.read(path).lines.reject { |line| /^Applicant Name/.match(line) }.map { |line| fields = *line.split(/,/) Assessment.new(*fields[0, 2], *fields[2, 4].map(&:to_i)) } }.group_by { |a| a.applicant }.flat_map { |k, as| as.inject(&:+) } puts "Applicant Name,Project Title,Productivity,Originality,Feasibility,Judges Point,Total" puts assessments.sort_by { |i| -i.total }
Haskell版 (96行)
Ruby版を参考に悩みながら書いたのが以下。
import List import Data.Ord import IO import System.Environment import Text.ParserCombinators.Parsec data Assessment = Assessment { applicant:: String, title :: String, productivity :: Int, originality :: Int, feasibility :: Int, judgesPoint :: Int } instance Show Assessment where show a = case a of (Assessment applicant title productivity originality feasibility judgesPoint) -> applicant ++ "," ++ title ++ "," ++ show productivity ++ "," ++ show originality ++ "," ++ show feasibility ++ "," ++ show judgesPoint ++ "," ++ show (totalPoint a) totalPoint :: Assessment -> Int totalPoint (Assessment applicant title productivity originality feasibility judgesPoint) = productivity + originality + feasibility + judgesPoint aplus :: Assessment -> Assessment -> Assessment (Assessment a1 t1 p1 o1 f1 j1) `aplus` (Assessment a2 t2 p2 o2 f2 j2) = Assessment a1 t1 (p1 + p2) (o1 + o2) (f1 + f2) (j1 + j2) text :: Parser String text = many $ noneOf "," point :: Parser Int point = do s <- char '-' return 0 <|> do s <- many digit return $ read s record :: Parser Assessment record = do a <- text char ',' t <- text char ',' p <- point char ',' o <- point char ',' f <- point char ',' j <- point return $ Assessment a t p o f j parseLine :: String -> [Assessment] parseLine line = case (parse record "" line) of Left err -> [] Right a -> [a] linesToAssessments :: [String] -> [Assessment] linesToAssessments = concatMap parseLine rejectHeaders :: [String] -> [String] rejectHeaders = filter $ (not) . isPrefixOf "Applicant Name" sortByApplicant :: [Assessment] -> [Assessment] sortByApplicant = sortBy $ comparing applicant groupByApplicant :: [Assessment] -> [[Assessment]] groupByApplicant = groupBy $ \x y -> applicant x == applicant y sumAssessments :: [[Assessment]] -> [Assessment] sumAssessments = map $ foldl1 aplus sortByTotalPoint :: [Assessment] -> [Assessment] sortByTotalPoint = sortBy $ comparing (negate . totalPoint) assessmentSummary :: [String] -> [Assessment] assessmentSummary = sortByTotalPoint . sumAssessments . groupByApplicant . sortByApplicant . linesToAssessments . rejectHeaders readLines :: FilePath -> IO [String] readLines path = do h <- openFile path ReadMode cs <- hGetContents h return $ lines cs main = do paths <- getArgs liness <- sequence (map readLines paths) let lines = concat liness let summary = assessmentSummary lines putStrLn "Applicant Name,Project Title,Productivity,Originality,Feasibility,Judges Point,Total" sequence_ (map print summary)
色々ハマったけど、以下のことを学んだ。
- Parsecで/[^,]*/相当のことはmany (noneOf ",")と書ける。
- sequenceでIOのリストからアクションの実行結果のリストを得られる。結果が要らない場合はsequence_でOK。
- sortByはRubyのsort_byではなくsort {|x,y|...}相当。Data.Ord.comparingを使うとsort_byっぽく書ける。
残った疑問。
- comparingの==版はないのか? equalingみたいな。
- Rubyのsort_byみたいにSchwartzian Transformをやってくれる関数はないのだろうか。Haskellだと比較関数が速いからいらない?
- getArgsからlinesを得るまでの流れをもっとスマートに書けないだろうか。
- ファイルのクローズはどのタイミングでやればいいの?
- というか、そもそもHaskellでこういうコードを普通はどう書くの?
liness <- sequence (map readLines paths)<br> let lines = concat liness<br><br>は<br><br> lines <- concat `fmap` sequence (map readLines paths)<br><br>にするとか。ちなみに、importするときはなるべく特定関数のみにするか、qualifiedすると読みやすくなります。
sequence_ (map print summary)<br><br>は<br><br> mapM_ print summary<br><br>とか。
CSV などの表をいじり回すコマンドを以前から作っていたりするのですが、<br>それを使うとこんなですかねぇ。<br><br>% table cat a.csv b.csv |<br>table newfield total '<br>_["Productivity and performance"].to_i +<br>_["Originality and creativity"].to_i +<br>_["Feasibility"].to_i +<br>_["Jadges point"].to_i'|<br>table group 'Applicant Name' -a 'sum(total)'<br><br>https://github.com/akr/table<br>(正直にいえば、newfield は今日加えたものですが)<br><br>表を扱うことについては、もっとよい抽象化層があるんじゃないですかね。
> ujihisaさん<br><br>なるほど、fmapとかmapMを使うとすっきりしますね。たしかにたくさんモジュールをimportするとどのモジュールの関数かわかりにくいですね。<br><br>> akrさん<br><br>おお、そんなコマンドが。<br><br>たしかにリストでやるのはちょっと低レベルな気がします。<br>まだ、リストもロクに使えませんが…。
比較以外の部分を僕なりに書いてみました。質問は Twitter でいつでも聞いてください。<br>https://gist.github.com/1408300<br>質問の「equaling みたいな」と「Schwartzian Transform」は、Haskell で意識して書いたことがないです。comparing で用が足りています。他の人はどうだろう。<br>「getArgs から lines への流れをスマートに」する方法の一つは Applicative を使うことです。が、do-notation の長所(手続きプログラマが読みやすい)が消えるので一長一短です。<br>「ファイルのクローズ」は hGetContents がよろしくやってくれるので気にする必要はありません。 hGetContents のマニュアルにありますが、すべてを読み終えたらハンドルは閉じます(というのが僕の理解)。<br>「Haskell でどう書く」ってのは難しい質問ですが、分割統治して解決してから関数合成という意味で、shugo さんのアプローチは Haskell らしいと思います。<br>Haskell の Show クラスは、僕の認識では Ruby の inspect に近いです。今回のように、 total みたいな値を表示に付け加えたいときは、僕は Text.PrettyPrint を使います。
「Equaling」は (==) : Eq a => a -> a -> Bool のことかなあ、と考えました。comparing :: Ord a => (b -> a) -> b -> b -> Ordering の対比です。<br>それから、shugo さんの Haskell のコードではパターンマッチによってデータ型の値を取り出していますが、ラベルを使う方法もあります(というのを先ほどのコメントで言い忘れました)。totalPoint x = productivity x + ... というふうに書けます。これは好みの問題です。
ありがとうございます。<br><br>僕のHaskell力ではまだ読めませんが、すっきりしたコードですね。<br>Showはたぶんinspectなんだろうなと思いつつ、to_sが何にあたるのかわかりませんでした。<br>hCloseの話はReal World Haskellにも書いてありました。<br><br> https://kindle.amazon.com/post/27B3SNLZCW3QI<br><br>equaling :: Eq a => (b -> a) -> b -> b -> Boolみたいなのはないんですかね。<br>それがあれば、<br><br> groupByApplicant = groupBy $ equaling applicant<br><br>と書けるかな、と。<br><br>ラベルを使う方法は一応知ってましたが、何となくパターンマッチを使ってみました。<br>この例だとあんまり面白くなかったですね。<br>赤黒木のbalanceみたいなのだとかっこいいですけど。