関数型言語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をサポートしてないので、いずれにせよ厳密には無限じゃないですね。
コメントの投稿
トラックバック
https://emasaka.blog.fc2.com/tb.php/708-4d67c008