USP Magazine 2014年4月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?
Mon Jun 23 20:10:26 JST 2014 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 2258, keywords: この記事は最終更新日が7年以上前のものです。
最新号:
1. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?
産業技術大学院・USP研究所・USP友の会 上田隆一 (脚注:順に助教、アドバイザリーフェロー、会長)
こんにちは。USP研究所元社員の上田です。USPマガジンに久しぶりに何か書けと言われました。ご存知のように(?)本誌はもともと、USP友の会が世に送り出した日本唯一のシェルスクリプト総合誌です。最近のUSPマガジンは良くも悪くも大人しいので、友の会っぽさ(脚注:一般的にはダメな意味で扱われる。南無阿弥陀仏。)を取り戻すため、黒いエンジェルちゃんとして舞い戻って来た次第です(脚注:意味不明。ところで本連載は脚注まみれにする予定ですので悪しからず。なんとなく。なんとなくクリスタル。滝川ク(以下略)。)。
ところでシェルスクリプトと言っても、USP研究所のシェルスクリプト(つまりユニケージ)は「データの変換こそプログラミングだ」と言わんばかりに入出力にこだわったものです。Tukubaiコマンドを見ると分かる通り、ほとんどのコマンドは文字列の加工のためにあり、しかもほとんどが標準入力から標準出力に字を流すだけのものです。
このようなシェルスクリプトの使い方はまだそんなに普及しているわけではありませんが、プログラミングの世界には、同じ思想のものが存在します。あのこわいこわい(脚注:なぜこわいのか。「なごやこわい 関数型」で検索。)「関数型言語」です。「違う!」と言う人もいますが、一緒です(脚注:私の頭の中では)。プログラミングというのは、究極のことを言えば、材料を自分の欲しい姿に変換することです。その変換は、「入力」と「出力」を考えれば十分OKなはずなのですが、世の中には余計なものがなんと多いことか。
ということで、本連載ではユニケージの親戚とも言える(脚注:言い過ぎ)関数型言語を取り上げます。 具体的にはHaskellをやります。なぜか。自分が好きだからです。問答無用です。
よし、USPマガジンでHaskellやる名目が立った。
1.1. Haskellで何やりましょう?
んで、Haskellで何をやるのか。モナドとか高階関数とかややこしい言葉を使って煙に巻くのか。いや、理屈より先に手を動かした方がよいでしょう。ということで、これまでやってきたシェル芸勉強会(脚注:/?page_id=684)の問題を、順に淡々と解いて行くことにしましょう。(脚注:これなら毎回ネタを考えないで済むという黒い判断。)
1.2. 第1回問題1
第1回シェル芸勉強会(脚注:hbstudy#38でやらせてもらったものです。2012年10月27日のことでした。)の記念すべき最初の問題は、こんなものでした。
/etc/passwd から、ユーザ名を抽出したリストを作ってください。
シェルのワンライナーだと簡単ですね。
ueda@ubuntu:~$ cat /etc/passwd | awk "-F:" '{print $1}'
...
sshd
ueda
mysql
postfix
等、さらっとできます。さあこれをHaskellでやりましょう(脚注:今、とても面倒くさいという気分です。)。
やる前に、Haskellの環境を整えましょう。特にこだわりがないならUbuntu Linuxの新しいものか、 Macがおすすめです。
Ubuntuの場合、次のコマンド一発でHaskellの環境がインストールできます。
ueda@ubuntu:~$ sudo apt-get install haskell-platform
Macの場合は、
uedamac:~ ueda$ brew install ghc
でコンパイラをインストールできます。
1.2.1. 最初なのでとりあえず手を動かしてみる
では、解いて行きます。最初なので、Haskellのコードを書いてコンパイルすることから始めます。
まず、次のようなファイルを準備してください。1行のHaskellのプログラムです。
```hs ueda@ubuntu:~$ cat q1_1.hs main = getContents >>= putStr ```これを次のように ghc (The Glasgow Haskell Compiler) (脚注:GCC?、DHC?、いいえGHCです。) でコンパイルします。
```bash ueda@ubuntu:~$ ghc q1_1.hs [1 of 1] Compiling Main ( q1_1.hs, q1_1.o ) Linking q1_1 ... ```すると次のように q1_1 というファイルができているはずです。
ueda@ubuntu:~$ ls q1_1*
q1_1 q1_1.hi q1_1.hs q1_1.o
これを実行してみましょう。 /etc/passwd を cat してパイプに通します。
ueda@ubuntu:~$ cat /etc/passwd | ./q1_1
...
ueda:x:1000:1000:Ryuichi Ueda,,,:/home/ueda:/bin/hs
mysql:x:104:111:MySQL Server,,,:/nonexistent:/bin/false
postfix:x:105:112::/var/spool/postfix:/bin/false
/etc/passwd の中身が表示されたと思います。 q1_1.hs は今のところ、 cat のように入力されたテキストをそのまま出力するコマンドになってます。
文法は少し書いて動かしてみてから詳しく勉強した方がいいと思いますので、先に進みます。今度は q1_1.hs を図1のように加筆します。これはまだ答えではありません。寄り道したコードなのでご注意を。
- 図1: 加筆した q1_1.hs
コンパイルして /etc/passwd の内容を入力すると、次のように最初の行が改行無しで出力されます。
```bash ueda@ubuntu:~$ cat /etc/passwd | ./q1_1 root:x:0:0:root:/root:/bin/hsueda@remote:~$ ```さて、コードの説明をしていきます。まず、図1の4,5行目から。Haskellは「関数型言語」というだけあって、関数を並べてプログラムしていきますが、この4,5行目は関数 main' の定義です。2行目の main も関数ですが、ちと事情がややこしいのでかなり後から説明をします。
4行目は、関数 main' で「何が入力されて何が出力されるか」 を書いたものです。4行目によると、 main' では 「 String が入力されて String が出力される」 ということになっています。 String というのは文字列を表す「型」というものですが、 とりあえずそれだけ覚えて5行目に移ります。
5行目には、イコール( = )で挟まれて左辺と右辺がありますが、 左辺の main' cs は、 関数 main' がとる引数は cs であるという意味で、 cs は4行目の型の指定どおり、 String 、 つまり文字列です。
右辺には処理の内容が記述されます。まず、 lines cs ですが、これは cs が lines という関数に入力するという意味になります。C言語ライクな言語だと lines(cs) と書くようなところですが、Haskellの場合、括弧がいりません。コマンドとオプションの関係に似ています。
lines は文字列を行単位に分割してする関数で、今後も頻繁に登場します。 次に head (lines cs) ですが、これは lines cs の出力を head に入力するという意味になります。 head 関数は、与えられた入力(この場合は行単位に分割された各行)の最初の一つを出力する関数です。
したがって、 main' を日本語にすると、「文字列を受け取って最初の一行を返す関数」ということになります。
というわけで、 /etc/passwd を入力すると、 main' cs の出力は /etc/passwd の先頭の一行ということになります。 それが2行目の putStr に渡って画面に出力されます (脚注:くどいようですが2行目の説明は当分の間しません。)。
1.2.2. そろそろ型の話をしないと説明が難しいので
さて、最初からHaskellの初心者を完全に置いてきぼりにしている感が否めないので、今の部分を別の視点からもう一度説明します。
とりあえず今やったことを最もはしょって説明すると、main' に「何か文字列が入力されたらそれを加工して返す」という関数を書くと、 q1_1がその通り文字列を加工するプログラムになるということになります。したがって、我々は main' だけに集中すればよいことになります。
ということは、とりあえずシェル芸のように標準入出力を扱うだけの問題に対しては、今書いたコードをコピペして main' だけ書き換えればよいということになります。もっと複雑なことをするときは、何か別の関数を書いて main' の中で使えばよいことになります。
結局、Haskellでプログラムするということは、関数と関数をくっつけて別の関数を作っていくという作業にすぎません。このとき、関数にはくっつくものとくっつかないものがあります。それを決めるものが「型」です。型は ghci というHaskellのインタプリタで調べることができます。
例えば、 lines の型は
Prelude> :t lines
lines :: String -> [String]
というように、 String を入力し、 [String] を出力するものです。 [ ] は「リスト」というもので、ある型のものを順番に並べたものですが、いずれ詳しく説明します。
さて、これで図1の lines の型は 分かりました。では、 head はどうでしょう?
Prelude> :t head
head :: [a] -> a
a というもののリストが入力、 a の型のものが出力、と読めます。 この a ですが、実は String に化けることができます(脚注:型推論というやつです。本稿では難しい単語は全て脚注に追い出します。難しい言葉を並べて初心者を煙に巻く奴にこれまでいやというほど嫌な目に遭っているので。だいたい一ヶ月後には追い抜いてますが。)。ですので、 head (lines cs) (脚注:この括弧は、 lines cs というものを一つにとりまとめるだけの括弧で、 head () のようなC言語的な解釈をしてはいけません。) と書いたときの head の型は、
head :: [String] -> String
となります。そのため、 lines cs というものを受ける事ができるのです。 実は、 head (lines cs) は (head . lines) cs とも書けます。この場合、解釈の上では、 「 cs を head . lines という関数に入力する」 という解釈になります。 head . lines は二つの関数をくっつけて一つにしたもの (脚注:「合成関数」というものです。)で、 その証拠に、 ghci に聞いたら、
Prelude> :t head . lines
head . lines :: String -> String
とちゃんと型が返ってきます。
ここで、はっきり断っておきます。
Haskellは難しくありません。 単に関数を型を合わせてパズルのように連結していくだけです。
難しいと感じるのは、使うアルゴリズムがC言語系のものと違うからです。実際にはHaskell云々、関数型云々というよりも、こっちの方が本質的な問題です。
1.2.3. では解答
初回なので思いっきり回りくどくなりましたが、さっさと解答して本稿を締め殺します。
- 図2: 問題1の解答
@remote:~$ cat q1_1.hs
ueda= getContents >>= putStr . main'
main
' :: String -> String
main' cs = unlines $ map ( takeWhile (/= ':') ) ( lines cs ) main
実行してみましょう。
ueda@remote:~$ ghc q1_1.hs
1 of 1] Compiling Main ( q1_1.hs, q1_1.o )
[Linking q1_1 ...
ueda@remote:~$ cat /etc/passwd | ./q1_1
...
ueda
mysql
postfix
できました。さて、難しいものがまた出てきましたので、解説を・・・えっ?もうページが足りない??なんと。では、次回ということで・・・。
さいならさいなら。