USP Magazine 2014年12月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?
Mon May 4 12:50:23 JST 2015 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 4620, keywords: この記事は最終更新日が7年以上前のものです。
9. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?¶
産業技術大学院大学・USP研究所・USP友の会 上田隆一
USP友の会のシェル芸勉強会 (脚注:シェルのワンライナー勉強会)は、 日々、他の言語からの他流試合に晒されているのである。 そこで上田は、Haskellで自ら他流試合を行い、 さらにシェル芸勉強会をいじめる自傷行為に手を 染めるのであった。
9.1. はじめに¶
こ [1] 。富山の産んだブラックエンジェル上田です。 最近、次女が重たくなってきて、背負いながら物を書いていると けっこうしんどくなって参りました。 俺はコオイムシ [2] かと、そういう気分になります。 「魚類、モノアラガイ、他の昆虫等を先端に二対の爪がある 鎌状の前肢で積極的に捕らえ、口針から消化液を送り込み 溶けた肉質を吸入する体外消化を行う。」 ことも、 「オスは卵塊保護中は動きを制約されるが、 通常と変わらない程度に給餌もし、 時には他のオスが卵を背負ってる時に、 その卵を襲って捕食してしまう事もある。」 ことも、コオイムシと私に共通した特徴です [3] 。 漢字で書くと「好意無視」となるのも、 なかなか素敵であり、ただならぬシンパシーを感じます。
9.2. 前回のおさらい¶
さて、なんか精神状態を問われそうなスタートになりましたが [4] 、 本題に入りましょう。 現在解いているのは第1回勉強会の4問目です。 こんな問題でした。
次のような ages ファイルを作り、 ans のように集計してください。
ages と ans は図1のようなファイルです。 GitHubにアップしてあります [5] 。 ages はでかいのでブラウザで見ると メモリ圧迫人生圧迫圧迫面接なのでご注意を。
- リスト1: インプットする ages ファイルと解答ファイル ans
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | ###agesは0〜109(年齢)をランダムに書いたもの###
ueda@remote:~/Study1_Q4/data$ head -n 5 ages
91
35
11
100
94
###ansはagesの度数分布表###
ueda@remote:~/Study1_Q4/data$ cat ans
0〜9 9158
10〜19 9142
20〜29 9052
30〜39 9208
40〜49 9081
50〜59 9161
60〜69 8938
70〜79 8959
80〜89 9143
90〜99 9047
100〜109 9111
|
前回はリスト2の q1_4_2.hs までを作りました。 出力と共に示します。 もう既に「何十代なのか」のリストができているので、 これらを集計するだけです。
- リスト2: 前回作成したコードq1_4_2.hsと実行結果
1 2 3 4 5 6 | ueda@remote:~/Study1_Q4$ cat q1_4_2.hs
main = do cs <- getContents
print [ 10 * ( (read c :: Int) `div` 10 ) | c <- lines cs ]
ueda@remote:~/Study1_Q4$ ghc q1_4_2.hs
ueda@remote:~/Study1_Q4$ cat data/ages | ./q1_4_2 | head -c 20
[90,30,10,100,90,0,7
|
9.3. ソートする¶
さて続きですが、6月号でも出てきた sort 関数を使ってリストをソートします。 リスト3にソートまで記述したコードを示します。 リスト2では各年齢の1の位を切り捨てた後、10をかけていますが、 これは不要なので処理を消しました。 また、sort関数を使うには Data.List というモジュールを インポートしますが、これも6月号で説明しました。
- リスト3: リストをソートする処理を加えたq1_4_3.hsとその実行結果
1 2 3 4 5 6 7 8 9 | ueda@remote:~/Study1_Q4$ cat q1_4_3.hs
import Data.List
main = do cs <- getContents
print $ sort [ (read c :: Int) `div` 10 | c <- lines cs ]
ueda@remote:~/Study1_Q4$ ghc q1_4_3.hs
###とりあえずdat/agesの先頭の10行を渡してみる###
ueda@remote:~/Study1_Q4$ head data/ages | ./q1_4_3
[0,1,3,7,9,9,9,10,10,10]
|
リストの要素の個数をカウントするにはソートしない方法もあります。 またソートするのは無駄なようにも思えます。 しかし、ソートしない方法は データや要素の種類の多寡で有利不利が出てしまうので、 あまり凝らずにソートに頼ることにしました。 ここら辺の判断については意見の分かれるところですが、 私は「いつ・誰が困るかどうか」で判断しています。
なんかかっこいい人が「こうでなければならない!」 とか本に書いていてそれを鵜呑みにするのはやめましょう。 世の中に、無条件にこうでなければならないというものは、 ありません。
9.4. カウント処理¶
さて、リストの集計を行います。 count という関数をリスト4のように実装して、 main 関数の処理に挟み込みます。 count の出力は「n十代がm人いる」ことを示すリストです。 10行目の where で定義している h は、リストの先頭の要素で、 その下の len が、 h が何個続いているか、 最後の d が、リストの先頭から h の並びを切り落としたリストです。 d は、9行目で再び count に入力されます。
count 関数が二つ定義されているのは、 7月号で出てきた「パターンマッチ」ですね。 8行目の定義は、引数が [] 、つまり空の場合にマッチし、 この場合はそのまま空のリストを返します。 9行目以降の定義は、8行目以外の場合、 についてマッチします。
Haskellは、普通の言語ではif文を使うような場合でも、 このような仕組みが充実しているのでif文を避けることができます。 これが見やすい・書きやすいかどうかは、 読み手・書き手次第ですが・・・。 ここら辺、読み手書き手にどれくらいのレベルや 慣れを要求するとちょうどいいのかという問題は、 おそらく答えが出ない話です。
- リスト4: count関数を実装したq1_4_4.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ueda@remote:~/Study1_Q4$ cat q1_4_4.hs
import Data.List
main = do cs <- getContents
print $ count $ sort [ (read c :: Int) `div` 10 | c <- lines cs ]
count :: [Int] -> [(Int,Int)]
count [] = []
count ns = (h,len) : count d
where h = head ns
len = length $ takeWhile (== h) ns
d = dropWhile (== h) ns
###出力###
ueda@remote:~/Study1_Q4$ ghc q1_4_4.hs
ueda@remote:~/Study1_Q4$ head data/ages | ./q1_4_4
[(0,1),(1,1),(3,1),(7,1),(9,3),(10,3)]
|
11行目の takeWhile 関数については5月号でも出てきました。 12行目の dropWhile 関数と共に、 リスト5で挙動を見ておきましょう。
- リスト5: takeWhile、dropWhileの挙動
1 2 3 4 5 6 7 8 9 10 11 | ueda@remote:~/Study1_Q4$ ghci
Prelude> let ns = [0,0,1,2]
###takeWhileは関数にマッチするまでの要素を取り出す###
Prelude> takeWhile (== 0) ns
[0,0]
###dropWhileは関数にマッチするまで要素を切り落とす###
Prelude> dropWhile (== 0) ns
[1,2]
###型は次の通り。Boolを返す関数とリストを引数にとる。###
Prelude> :t takeWhile
takeWhile :: (a -> Bool) -> [a] -> [a]
|
9.5. 整形して完成¶
最後に、 count の返すリストを整形してこの問題を終わらせましょう。 リスト6のように、 print の代わりに out という関数を定義して使います。 この out では、リストの中の (n,m) (n十代がm個)というデータを、一つずつ17、18行目で整形して出力しています。 16行目の左辺の ((g,num):es) は、 (g,num) がリストの先頭、 es がそれ以外で、 リストの先頭の (g,num) が17行目の oneline を作るために使われ、 es が再起的に out に渡されています。 んで、 oneline と out の間に >> という謎の記号がありますが、これの型を、 これまでも何回か出て来た >>= の型とともにリスト7に示します。
- リスト6: 出力を整形するq1_4_5.hs(完成)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ueda@remote:~/Study1_Q4$ cat q1_4_5.hs
import Data.List
main = do cs <- getContents
out $ count $ sort [ (read c :: Int) `div` 10 | c <- lines cs ]
count :: [Int] -> [(Int,Int)]
count [] = []
count ns = (h,len) : count d
where h = head ns
len = length $ takeWhile (== h) ns
d = dropWhile (== h) ns
out :: [(Int,Int)] -> IO ()
out [] = return ()
out ((g,num):es) = putStrLn oneline >> out es
where oneline = (show (g*10)) ++ "〜" ++ (show (g*10+9))
++ " " ++ (show num)
|
- リスト7: >>と>>=の違い
1 2 3 4 5 | uedambp:~ ueda$ ghci
Prelude> :t (>>)
(>>) :: Monad m => m a -> m b -> m b
Prelude> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
|
これだけ見てもピンと来ないかもしれませんが、 >>= は >>= の左側の出力を右側に渡しますが、 >> は渡しません。型を見たら分かるのですが、 m a -> m b -> m b の a が出力に残ってません。 ですので、 putStrLn のように、 関数としては何も出力しない [6] ものが左側にあるときに使います。 おそらく、ghciでリスト8のような例を示しておけばなんとなく分かるかと。
- リスト8: >>の使用例
1 2 3 4 | Prelude> print "a" >> print "b" >> print "c"
"a"
"b"
"c"
|
>>= の使い方も触れておくために、 q1_4_5.hs の main 関数を >>= を使って書き直したのでリスト9 に示します。 ほとんどシェル芸です・・・。 唯一違うのは右から左に読まなければならんことですが [7] 。
- リスト9: >>=を使ってmain関数を書き直したもの
1 2 | main = getContents >>= out . count . sort .
map (\\c -> (read c::Int) `div` 10) . lines
|
最後に動かして本稿を締めます。 リスト10のように、動作しました。 しかし、100代が多いですね・・・。 高齢化社会です [8] 。
- リスト10: 動作確認
1 2 3 4 5 6 7 8 9 10 11 12 13 | ueda@remote:~/Study1_Q4$ ghc q1_4_5.hs
ueda@remote:~/Study1_Q4$ cat data/ages | ./q1_4_5
0〜9 9158
10〜19 9142
20〜29 9052
30〜39 9208
40〜49 9081
50〜59 9161
60〜69 8938
70〜79 8959
80〜89 9143
90〜99 9047
100〜109 9111
|
9.6. おわりに¶
今回は第1回勉強会4問目の問題を片付けました。 次回、5問目を・・・というところですが、 ここまでやってきてちっとも前に進まないので、 記事の趣旨を変えてOpen usp Tukubai のHaskell版の話をしようかちょっと迷っています。 OK貰えたらOpen usp Tukubaiの話をやってみようかと思います。 より、実践的な内容になると思います。
そりでは。
9.7. お知らせ:コード募集¶
本稿で出た問題のHaskellのコードを、 名前あるいはハンドルネームと共に送ってください。 短いもの、あるいは変態的なものをお願いいたします。
email: 編集部のメールアドレス
脚注
[1] | 編集部注: 本人にこれは何だと確認したら「こんにちは。」の略だが書くのが面倒だったのこと。なめてんのか。 |
[2] | 水の中の昆虫です。新橋界隈、特にニュー新橋ビルではあんまり見かけないですね。 |
[3] | 「http://ja.wikipedia.org/wiki/コオイムシ」より引用。 |
[4] | こんなにフリーダムに書いていて精神状態が悪いわけは無い。 |
[5] | https://github.com/ryuichiueda/UspMagazineHaskell/tree/master/Study1_Q4/data |
[6] | 正確には IO () を返す。 |
[7] | これ、なんとかなりませんかね?> Haskellのエロい人。 |
[8] | 違う。 |