USP Magazine 2014年6月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?
Wed Nov 19 19:51:10 JST 2014 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 2445, keywords: この記事は最終更新日が7年以上前のものです。
出典:USP magazine 2014年6月号
3. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?
3.1. はじめに¶
皆さん、酒飲んだくれてますか? 富山の生んだブラックエンジェル(脚注: 略すと富山ブラック)上田です。
3.2. 今回の問題: シェル芸勉強会第1回2問目¶
・・・と、挨拶の後、 別に面白いことも思い浮かばなかったのでさっさと本題に行きます。 今日からは第三回にしてやっと2問目です (脚注: 遅い。)。
/etc/passwd から、次を調べてください。 「ログインシェルがbashのユーザとshのユーザ、どっちが多い?」
たぶん私がこんな問題を出されたら、解答はこうです。
知るかボケ
・・・すんません (大学受験で出題された400字作文でこんな解答をして 浪人の遠因となった過去が。)。 真面目にやります。やりません。いや、やりますやります。
事前準備として、次のようにcabalというツールで splitというパッケージをインストールしておきます。 root権限でやりましょう。
1 | $ sudo cabal install split
|
cabalがインストールされていない場合は、 第一回でもやりましたが、 次のようにインストールします。
1 2 3 4 | ###Ubuntu###
ueda@ubuntu:~$ sudo apt-get install haskell-platform
###Mac###
uedamac:~ ueda$ brew install haskell-platform
|
また、入力する /etc/passwd はLinuxのものを想定しています。 (脚注: Macでやってもよいのですが、 なーんか /etc/passwd がぐちゃぐちゃでよく分からないんですよね・・・。)
3.3. 手始め: /etc/password の最後のフィールドを取得¶
まず /etc/passwd から、 集計に必要な部分だけ取得することにしましょう。 シェル芸なら図1のような感じですね。
- 図1: シェル芸でシェルのリストを作る
1 2 3 4 5 6 | $ awk -F: '{print $NF}' /etc/passwd | head
/bin/bash
/bin/sh
/bin/sh
/bin/sh
...
|
さて、対応するHaskellのコードは図2のようになります。
- 図2: q1_2_1.hs
1 2 3 4 5 6 7 8 9 10 | $ cat q1_2_1.hs
import Data.List.Split
main = getContents >>= putStr . main'
main' :: String -> String
main' cs = unlines $ map getShell (lines cs)
getShell :: String -> String
getShell ln = last $ splitOn ":" ln
|
関数は三つです。 4行目の main 関数は前回の問題でも出てきましたが、 標準入力から字を読み込んで標準出力に加工した字を出す関数です。
6,7行目の関数の説明はすっ飛ばして先に 9,10行目をやっつけましょう。 この関数は、 /etc/passwd の一行を入力に受け付けます。 受け付ける文字列は、例えば
1 | ueda:x:1000:1000:Ryuichi Ueda,,,:/home/ueda:/bin/bash
|
のようなもので、この関数は、 コロン区切りの最後のデータ /bin/bash を返します。 ちょっと ghci でやってみましょう。 図3のようになります。
- 図3: 関数が無いと叱られる
1 2 3 4 5 6 7 8 | $ ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/ :? for help
(略)
Prelude> let getShell ln = last $ splitOn ":" ln
<interactive>:2:26:
Not in scope: `splitOn'
Perhaps you meant `splitAt' (imported from Prelude)
|
・・・なんか怒られてしまいました。 ちゃんと読むと(脚注: ちゃんと読みましょう。) 「 splitOn なんて関数ねえぞ!」 と言っています。
これを解消する鍵は q1_2_1.hs のコードの1行目にあります。
import Data.List.Split
とありますが、これは Data.List.Split という「モジュール」をインポート、つまり使うという意味になります。 先ほど cabal でインストールしたのがこのモジュールに対応する パッケージです。中身は関数だらけです。
さて、これを知った上でもう一度 ghci で図4のように試してみましょう。
- 図4: 適切なモジュールをimportすると起こられない
1 2 3 4 5 | Prelude> import Data.List.Split
Prelude Data.List.Split> let getShell ln = last $ splitOn ":" ln
Loading package split-0.2.2 ... linking ... done.
Prelude Data.List.Split> getShell "aaa:bbb:ccc"
"ccc"
|
うまくいきました。
getShell の中身について説明すると、 splitOn が、指定した文字列で文字列を切ってリスト化する関数です。
1 2 | Prelude Data.List.Split> splitOn "aho" "thisahothataho"
["this","that",""]
|
last は、リストの最後の要素を返します。
1 2 | Prelude Data.List.Split> last [1,2,3]
3
|
では、6,7行目の説明を。 6,7行目は、標準入力から読み込まれた字を 行ごとに分割してリストにして ( lines cs の部分)、 map で各行に getShell を適用して、 map の返した文字列のリストを unlines で改行を入れてリストをくっつけて返しています。
これも ghci で試せばよいですね。 図5のようにやってみましょう。 便利ですね〜。 ghci で改行を指定するときは、 \\n と書いておけば大丈夫です。
- 図5: ghciで main' を試す
1 2 3 4 5 6 7 | Prelude Data.List.Split> let cs = "aa:bb\\ncc:dd"
Prelude Data.List.Split> lines cs
["aa:bb","cc:dd"]
Prelude Data.List.Split> map getShell (lines cs)
["bb","dd"]
Prelude Data.List.Split> unlines $ map getShell (lines cs)
"bb\\ndd\\n"
|
このような、関数を組み合わせるプロセスは シェル芸にも通じるところがあります。 入出力だけ考えるという点で、 関数型言語とシェル芸は通じるところがあります。
q1_2_1.hs をコンパイルして 図6のように出力を見ておきましょう。
- 図6: q1_2_1.hs の使用
1 2 3 4 5 6 7 8 | ueda@remote:~/GIT/USPMAG/SRC$ cat /etc/passwd | ./q1_2_1
/bin/bash
/bin/sh
/bin/sh
/bin/sh
/bin/sync
/bin/sh
...
|
3.4. さて、数えましょう¶
3.4.1. 関数を新設して undefined で型チェック¶
では、今度は数える部分を書きましょう。 先ほどの q1_2_1.hs は map getShell (lines cs) の出力を unlines してそのまま出力していましたが、 この unlines に入力する前に シェルの種類を数える関数を挟めばよいことになります。 まず、この考えをそのままコードにします。 図7のように書きました。
- 図7: q1_2_2.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ cat q1_2_2.hs
import Data.List.Split
main = getContents >>= putStr . main'
main' :: String -> String
main' cs = unlines $ shellCount $ map getShell (lines cs)
getShell :: String -> String
getShell ln = last $ splitOn ":" ln
shellCount :: [String] -> [String]
shellCount = undefined
|
7行目の unlines の後ろ(脚注: 処理的には手前) に shellCount という関数をはさんで、 shellCount という関数を12,13行目で定義しています。 ・・・といっても、13行目に undefined とあるように、まだ定義していません。
何でこんなことをするかというと、 コンパイラにかけるためで、 undefined と書いておくと
1 2 3 | ueda@remote:~/GIT/USPMAG/SRC$ ghc q1_2_2.hs
[1 of 1] Compiling Main ( q1_2_2.hs, q1_2_2.o )
Linking q1_2_2 ...
|
というように通ります。 当然実行時にエラーが出ますが。 ただ、こうやってコンパイラに通すと 型をチェックすることができます。 そのため、わざわざ undefined と書いてコンパイルしてみたのでした。
3.4.2. shellCount を実装(前半)¶
では、 shellCount を書いたものを示します。 ・・・と言いたいところなのですが、 思ってたよりややこしかったのでまた途中までのコードを 図8に示します。
- 図8: q1_2_3.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ueda@remote:~/GIT/USPMAG/SRC$ cat q1_2_3.hs
import Data.List
import Data.List.Split
main = getContents >>= putStr . main'
main' :: String -> String
main' cs = unlines $ shellCount $ map getShell (lines cs)
getShell :: String -> String
getShell ln = last $ splitOn ":" ln
shellCount :: [String] -> [String]
shellCount shs = map f $ shellCount' [ (1,s) | s <- (sort shs) ]
where f (n,str) = unwords [show n,str]
shellCount' :: [(Int,String)] -> [(Int,String)]
shellCount' shs = shs
|
このコードをコンパイルして実行すると 図9のようになります。
- 図9: q1_2_3.hs のコンパイル
1 2 3 4 5 6 7 8 9 10 11 12 | ueda@remote:~/GIT/USPMAG/SRC$ ghc q1_2_3.hs
[1 of 1] Compiling Main ( q1_2_3.hs, q1_2_3.o )
Linking q1_2_3 ...
ueda@remote:~/GIT/USPMAG/SRC$ cat /etc/passwd | ./q1_2_3
1 /bin/bash
1 /bin/bash
1 /bin/false
1 /bin/false
1 /bin/false
1 /bin/false
1 /bin/sh
...
|
つまり、シェルのパスに個数をくっつけたデータになりました。 シェルの名前はソート済みですので、あとは例えば Open usp Tukubaiを使って 図10のようにやれば答えが出てきてしまうのですが (脚注: ああ・・・答えを書いてしまった・・・。)、 もうちょっとHaskellで辛抱しましょう・・・。
- 図10: q1_2_3 からTukubaiを使って答えを出す
1 2 3 4 5 6 7 | ueda@remote:~/GIT/USPMAG/SRC$ cat /etc/passwd | ./q1_2_3 |
self 2 1 | sm2 1 1 2 2
/bin/bash 2
/bin/false 4
/bin/sh 17
/bin/sync 1
/usr/sbin/nologin 1
|
さて、 q1_2_3.hs のコードを読んでいきます。 まず、17,18行目の shellCount' は、 今のところ入力をそのまま出力しています。 この関数の実装は後回しにします。 同じ型のものを返す関数を仮に書くときは undefined よりもこのように同じものを返しておいた方が デバッグに便利です。 17行目にあるように、この関数の型は
shellCount' :: [(Int,String)] -> [(Int,String)]
です。 (a,b) (ここではa,bはそれぞれIntとString) というものがありますが、 これは「タプル」というもので、 二つの型の値を組み合わせるときに使います。 ですのでこの関数の型は 「IntとStringのタプルのリストを入出力する」 ということになります。
んで、今回作ったメインのものは13〜15行目の shellCount です。ここには新しい書き方が二つも!
まず、15行目の where ですが、 where と書くと、関数の中で関数を定義できます。 ここで定義しているのは関数 f で、 こいつは何をやっているかというと、 数字(Int)と文字列(String)のタプルを入力にもらって、 数字を文字列化し、文字列とくっつけて出力しています。 ghci で等価なコードを試してみましょう。
1 2 3 4 | Prelude> [show 5,"abcde"] <- 5がfの引数のn, "abcde"がstrに相当
["5","abcde"]
Prelude> unwords [show 5,"abcde"]
"5 abcde"
|
次に [ と ] で囲まれた部分ですが、 これはリスト内包というものです。 これもクドクド説明するより例で示した方がいいですね。 例えば下の例だと、リスト [1,2,3] から一つずつ要素を x に結びつけて、 それに一つずつ2を足してリストにします。
1 2 | Prelude> [ x+2 | x <- [1,2,3] ]
[3,4,5]
|
実は、次の map を使った例と等価です。
1 2 | Prelude> map (+2) [1,2,3]
[3,4,5]
|
んで、 q1_2_3.hs のコードでは、 タプルを作るためにリスト内包を使っています。 具体例を下に示します。 例の中で sort も使ってみます。
1 2 3 | Prelude> import Data.List <- sortを使うためにimport
Prelude Data.List> [ (1,s) | s <- (sort ["bbb","aaa","ccc"] ) ]
[(1,"aaa"),(1,"bbb"),(1,"ccc")]
|
さあ、最後に shellCount' を実装します。 今のところタプルの数字、つまり個数はすべて「1」 で出力していますが、これを同じシェルでまとめていきます。
さて次を・・・というところですが、 紙面がもうございません。 今月はこれでお開きにしたいと思います。
3.5. おわりに¶
今回はシェル芸勉強会第1回の2問目を扱いました。 本編ではあまり言及しませんでしたが、 今回は問題を関数に分けていくプロセスが随所で見られました。 Haskellのコードを書くときは、このように 頭を整理してどこで関数を分けることができるのか、 探りながら書いていくことになります。 実はこれ、どんな言語でも通用する発想方法ですので、 何の言語を使うにせよよいトレーニングになるかと。 つまりはHaskellやれということで、 今回を締めさせていただきます。
次回は続きからということで、 まーた途中になっちまいましたが、 来月まで辛抱強くお待ちいただければ・・・と。