USP Magazine 2014年12月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?

Mon May 4 12:50:23 JST 2015 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 4315, keywords: この記事は最終更新日が6年以上前のものです。

9. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?

産業技術大学院大学・USP研究所・USP友の会 上田隆一

USP友の会のシェル芸勉強会 (脚注:シェルのワンライナー勉強会)は、 日々、他の言語からの他流試合に晒されているのである。 そこで上田は、Haskellで自ら他流試合を行い、 さらにシェル芸勉強会をいじめる自傷行為に手を 染めるのであった。

9.1. はじめに

 こ [1] 。富山の産んだブラックエンジェル上田です。 最近、次女が重たくなってきて、背負いながら物を書いていると けっこうしんどくなって参りました。 俺はコオイムシ [2] かと、そういう気分になります。 「魚類、モノアラガイ、他の昆虫等を先端に二対の爪がある 鎌状の前肢で積極的に捕らえ、口針から消化液を送り込み 溶けた肉質を吸入する体外消化を行う。」 ことも、 「オスは卵塊保護中は動きを制約されるが、 通常と変わらない程度に給餌もし、 時には他のオスが卵を背負ってる時に、 その卵を襲って捕食してしまう事もある。」 ことも、コオイムシと私に共通した特徴です [3] 。 漢字で書くと「好意無視」となるのも、 なかなか素敵であり、ただならぬシンパシーを感じます。

9.2. 前回のおさらい

 さて、なんか精神状態を問われそうなスタートになりましたが [4] 、 本題に入りましょう。 現在解いているのは第1回勉強会の4問目です。 こんな問題でした。

次のような ages ファイルを作り、 ans のように集計してください。

  agesans は図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 に渡されています。 んで、 onelineout の間に >> という謎の記号がありますが、これの型を、 これまでも何回か出て来た >>= の型とともにリスト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 ba が出力に残ってません。 ですので、 putStrLn のように、 関数としては何も出力しない [6] ものが左側にあるときに使います。 おそらく、ghciでリスト8のような例を示しておけばなんとなく分かるかと。

  • リスト8: >>の使用例
1
   2
   3
   4
Prelude> print "a" >> print "b" >> print "c"
   "a"
   "b"
   "c"
   

>>= の使い方も触れておくために、 q1_4_5.hsmain 関数を >>= を使って書き直したのでリスト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]違う。
ノート   このエントリーをはてなブックマークに追加 
 

やり散らかし一覧

記事いろいろ