まいどのごとく

工学システム学類の授業の「プログラミング序論」をhaskellで解いてみた。
こういう入門系なプログラミングの課題ってhaskellの勉強にちょうどよいかも。


ということで今回はファイル操作関係。

課題21

引数で指定されたファイルをすべて開いて表示する。


要はcat


というか先生側の正解/不正解判定はcatの結果(あるいはそれと同様なもの)との比較らしい。
いや、ちゃんと手動で一個一個みてるのかな?
知らんけど。


すくなくとも提出されたファイル同士のdiffはとってると思われ。
完コピだと呼ばれるらしい。


で、話を戻して。
haskellの話。


今回は過程も含めつつ。


まずは引数処理

gatArgs :: IO [String]

で引数取得。全部開くわけですがhaskellだしmapで。
で、ファイルを開くのはというとreadFileってのがあるらしい。

readFile :: FilePath -> IO String
type FilePath = String

まんまStringを返すらしい。楽だ。
ってことで

getArgs >>= map readFile

とかやるとエラー。
モナドまわりがアレなんでmapMを使う。

map :: (a -> b) -> [a] -> [b]
mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
(getArgs >>=) :: ([String] -> IO b) -> IO b

このあたりを睨みつつ
これでおっけ

getArgs >>= mapM readFile :: IO [String]

あとはcatしろってことなんでconcatかまして、putStrすればおっけー♪
ということで完成版↓

import System

main = getArgs >>= mapM readFile >>= putStr . concat

課題22

引数のファイルのアルファベットの出現頻度を出力する

こんな感じか

$ cat file1
aaabbc
$ cat file2
AbbcCc
$ ./a.out file1 file2
a:0.333
b:0.333
c:0.333

大文字/小文字の区別はいらないらしい。
あと表示は小数点以下三桁


むぅ。前回の課題13あたりの流用できそう
できそうなんだけど、頻度か。
全体数で割らないといけないのか...。
むぅ。


とりあえずファイル展開はさっきと同様

getArgs >>= mapM readFile

例のごとくタプルで数えよう。
ノリとしては
"ababac" => [('a',3), ('b',2), ('c',1)]
って、感じで。
...この時点ですでに頻度にしてしまうか。
"ababac" => [('a',0.5), ('b',0.33..), ('c',0.166..)]
大まかな流れはsortしてgroupしてそれぞれlengthとるといい感じかな。
"ababac" => "aaabbc" => ["aaa", "bb", "c"] => [('a',3),('b',2),('c',1)]


あ、流れが二つある。
かたっぽはheadして、もうかたほうはlength
letとかwhereのお世話になりそう...。


ということで数える部分

accFunc n xs = (n, (head xs, (fromIntegral $ length xs) / (fromIntegral n)))
snd . (uncurry $ mapAccumL accFunc) . (\str -> (length str, group str)) . sort

uncurryを使ってみたのだが別に使わずに

(\str -> snd $ (mapAccumL accFunc) (length str) (group $ sort str))

でもいい気がする。
もちっと綺麗にならんかなぁ...。
とりあえず詰まりまくってたのは、整数→実数はfromIntegralでやんないとだめっぽい。ってこと


ん?これ個別にmapしてzipしたほうがよかったのか?
まぁ、いいや。
というか

(a -> b) -> (a -> c) -> a -> (b, c)
(a -> b) -> (a -> c) -> [a] -> ([b], [c])

みたいなのPreludeとかにありそう...。ないか。
いや、別に自分で作ればいいだけなんだけどね。


mapAccumLはアキュームレータ付きなマップ。
...つかわなくてもいける気がしてきた...。まぁいいや。

mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])

こんかいはaccは一切いじってないのでmapAccumLであるひつようはないのかも
mapAccumLがためにかなり混乱してました。


まぁ、いいや。とりあえずこれで頻度がでたのであとは表示だ。
今回もprintTapple
芸が無いが気にしない。
むしろこれは自分で方作ってShowクラス云々なあれをやるべきなのか。
まぁ、気にしない。
今回はやや汎用的に拡張

printTapple :: (Show a, Show b) => (a -> ShowS) -> (b -> ShowS) -> [(a,b)] -> IO()
printTapple _ _ [] = return ()
printTapple sx sy ((x,y):xys) = (putStrLn $ (sx x) . (':':) . (sy y) $ []) >> 
                                printTapple sx sy xys

実はかなり悪戦苦闘しつつ書いたというね。
まぁ、おかげでshowあたりを多少理解した。
これfold*1とかで書けるんだろうなぁ...とかおもいつつラムダはもういいや、ってことでこれでFA


ShowSを渡せるのでどんなものでも表示可能だぜ。


と、いうかね。
最初小数点以下3桁表示、っていう条件がめっちゃきついって思ってたんですよ。
思いあまって書いたコードはこんなの

(round $ (fromIntegral $ length xs) / (fromIntegral n) * 1000) / 1000

で、いざprintしてみると"3.33e-1"
あら。そっちデフォなの...?
とかずっと悩んでいたのですが、showFFloatなるものをNumericにて発見

showFFloat :: (RealFloat a) => Maybe Int -> a -> String -> String

String->StringはたしかShowSとかいうもの。
ん?で、Maybe Int?
なんだこのIntって!!とかおもってたら精度でしたYo!!


一発解決


Nothingで丸め無しで、Just NでN桁まで!
ちなみにshowFFloatは少数表示、showEFloatが指数表示、showGFloatはそのコンパチ。
printはshowGFloatらしいな。


ということで完成版

import System
import List
import Char
import Numeric

main :: IO ()
main = let accFunc n xs = (n, (head xs, (fromIntegral $ length xs) / (fromIntegral n)))
           count = snd . (uncurry $ mapAccumL accFunc) . (\str -> (length str, group str)) . sort
       in
       getArgs >>= mapM readFile >>=
       printTapple (shows) (showFFloat $ Just 3) . count . filter isAlpha . map toLower . concat

まぁ、こんなものでしょう。
map toLowerとfilter isAlphaは多分説明イランとおもうが仕様を満たすために挿入。
letがちとあれだがよしとしよう。

課題23

は課題22を低レベル関数で実装しろというもの。
えと、haskellでいうと?
まぁ、パス。

*1:mapでは無いはず。いや、mapしてからsequenceすればいいのか