本を読む

読書やコンピュータなどに関するメモ

関数型言語shの基礎文法最速マスター

 関数型言語shの文法一覧です。他の関数型言語をある程度知っている人がこれを読めば、shの基礎をマスターしてshを書けるようになっています。以下、Clojureあたりを想定して説明します。

注意:これは基礎文法最速マスターねたのパロディです。動作は本物ですが、意味はコジツケです。

REPL

 shの処理系は、POSIX準拠のUnix系環境であれば標準で用意されています。REPLを起動するには、shを実行します。

sh

 すると、プロンプトが表示されます。

$

 shのほかに、REPLに行編集機能を付けたbash・zsh・tcshなどもありますが、ここでは割愛します。

 なお、REPLとして使うほかに、あらかじめ用意したスクリプトをshで実行することもできます。

sh hoge

シーケンス

 shの扱うデータは、すべて、ある単位(ラインと呼びます)のデータが並んだシーケンスです。たとえば、seq関数(Linuxで使えます)は、指定した範囲の整数が並んだシーケンスを返します。

$ seq 1 3
1
2
3

 厳密には、シーケンスはfdというモナド(って何?)のようなものにくるまれています。REPLは、式を評価して得られたシーケンスをfdから受けとり、1ライン1行で表示します。

関数の返す値をほかの関数に渡す

 関数の返す値をほかの関数に渡すには、"|"を使います。

$ seq 1 100 | head -n 2
1
2

 head関数は、渡されたシーケンスの先頭からn要素を取り出す関数です。反対に、tail関数は、渡されたシーケンスの末尾からn要素を取り出す関数です。

$ seq 1 100 | tail -n 3
98
99
100

シーケンスを結合する

 関数の返すシーケンスと、別の関数が返すシーケンスを結合するには、";"を使います。

$ seq 1 2 ; seq 10 12    
1
2
10
11
12

 注意が必要なのは、";"は"|"より優先順位が低いことです。";"を優先するには"{}"で囲みます。

$ { seq 1 2 ; seq 10 12 ; } | head -n 3
1
2
10

 2つ目のseq関数の後にも";"が付いていることに注意してください。これは、シーケンスの末尾との結合を意味します。

 "()"で囲む方法もあります。"()"の場合は、2つ目のseq関数の後に";"は付きません。"()"の場合は、全体が新たなシーケンスを意味するためです。

$ ( seq 1 2 ; seq 10 12 ) | head -n 3
1
2
10

遅延評価

 関数が返すシーケンスは、すべて計算されてから次の関数に渡されるのではなく、必要な部分だけ計算されます。たとえば、yes関数は無限に続くシーケンスを返します。

$ yes 10
10
10
10
10
10
(以下略)

 これを上記のhead関数に渡すと、yes関数の計算が終わってから(つまり無限時間後に)head関数に渡されるわけではなく、必要な値だけが渡ります。

$ yes 10 | head -n 3
10
10
10

関数定義

 新たに関数を定義するには、以下のように書きます。

$ foo() { yes 10 | head -n 3 ; }
$ foo
10
10
10

 この"{}"は、「シーケンスを結合する」で解説した"{}"と同じものです。そのため、"()"で定義することもできます。

$ foo() ( yes 10 | head -n 3 )
$ foo
10
10
10

 いわば、シーケンスの一連の処理に名前を付けたものといえます。

 オプションは"$番号"で参照します。

$ foo() { seq 1 $1 ; }
$ foo 3
1
2
3

map

 map相当の処理は、以下のように書きます。

$ seq 1 3 | while read n ; do echo $(( n + 3 )); done
4
5
6

 whileがmap、read nがnへの値の束縛、doが処理の開始、doneが処理の終端に当たります。$((~))は数値演算してその値を取り出す操作です。echoは値を返す操作を意味します。doの前やdoneの前の";"は、"{}"と同様にシーケンスの先頭や末尾との結合を意味します。

 なお、この場合の";"はwhileの中のものとみなされるため、"|"より優先されます。

内包表記

 シーケンスを作る内包表記には、forを使います。

$ for i in $(seq 1 100) ; do echo $(( i + 2 )) ; done | head -n 2
3
4

条件

 条件が真のときと偽のときとで値を変えるには、以下のように書きます。

$ [ 1 = 1 ] && echo 3 || echo 4
3
$ [ 1 = 2 ] && echo 3 || echo 4
4

 "[]"が条件、"&&"の後が真のとき、"||"の後が偽のときです。

再帰

 seqのように無限に自然数列のシーケンスを返す関数naturalは、再帰を使って以下のように書けます。

$ natural() { echo 0 | _natural ; }
$ _natural() { read n ; echo $n ; { echo $(( n + 1 )) | _natural ; } ; }
$ natural | head -n 5
0
1
2
3
4

高階関数、クロージャ

 関数や無名関数を、関数の引数に渡すこともできます。

$ foo() { eval "$1" ; }
$ foo "echo 'Helo'"
Helo

 evalは、渡された関数や無名関数を呼び出すことを意味します。Common Lispでいうapplyやfuncallに相当します。

 こんなことも。

$ seq 1 5 | while read n ; do foo "echo $n" ; done
1
2
3
4
5

 つまり、無名関数"echo $n"として渡された中の"$n"には、呼び出し先の環境ではなく、呼び出し元のレキシカルな環境での値が束縛されているわけです。funarg問題を解決したクロージャですね。

まとめ

 shは関数型言語です(キリッ

コメント

管理人のみ閲覧できます

このコメントは管理人のみ閲覧できます

>非公開コメントさん
こんにちは。お褒めいただき(?)ありがとうございます!

末尾再帰のご指摘ですが、シェルスクリプトの関数の再帰は、スタックじゃなくてforkなので、少なくともスタックオーバーフローにはならないようです。おお、アクターモデルw

という重箱の隅はさておき、私もfork爆弾になるかと思ってました。が、試してみたところ、実はけっこう呼び元のプロセスが捌けるようで、10,000ぐらいまで行ってロードアベレージ5ぐらいにおさまってました。びっくりです。ちなみに、試したsh実装はdashでした。

まあshはbignumをサポートしてないので、いずれにせよ厳密には無限じゃないですね。

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://emasaka.blog65.fc2.com/tb.php/708-4d67c008

 | HOME | 

Categories

Recent Entries

Recent Comments

Recent Trackbacks

Appendix

emasaka

emasaka

フリーター。
連絡先はこのへん

Monthly


FC2Ad