match-lambdaは一引数関数、match-lambda*は可変長引数の関数、と何度か書いてきたが、考えてみると、これだけだと不親切だったかもしんない、って気づいた。
そもそも可変長引数、って言われても意味が分からん、って思う人もいるのでは、と。
「馬鹿にすんな、俺は可変長引数くらいの意味を知ってる。」
って人もいるだろ。うん、こう言う人の半分くらいは恐らく理解してるだろうが、半分くらいは理解してないと思う(笑)。
つまり、表面的には可変長引数の意味を理解してる、って人はそれなりにはいるだろう。ただし、ラムダ式が絡むと途端に混乱する人が出てくる。何故なら、一般的に言うとラムダ式そのものが異様な見た目だから、だ。そして異様な見た目と遭遇した時、かなりの人は今まで学んだ事がスッポ抜けるんだ。
ましてや今回はmatch-lambdaにmatch-lambda*だ。ただでさえラムダ式はややこしく見えるのに、余計長くなったそれの亜種の事なんてマトモに考えたくねぇだろう。分かる。分かるんだよ。
ところで、ラムダ式は無名関数の事だ、とか、Lispでは関数がファーストクラスオブジェクト、だとかそのテの言い回しを色んなトコで聞いた事がある人が多いだろう。
ここではSchemeに於いては、ラムダ式は実は次のような意味だ、と言う事を説明してみよう。恐らくオブジェクト指向言語を学んだ人の方がむしろ分かりやすいと思う。
端的に言うと、Schemeには、整数型、浮動小数点型、文字型、文字列型、と同じように関数型がある、ってこった。そしてその関数型のコンストラクタをラムダ式と言う。
以上。O.K.?
この説明はプログラミングの世界に入って来たばかりの人にはサッパリの説明だが、そこそこプログラミングを行っている、またはオブジェクト指向に慣れてる人にはこの説明の方が核心を突いてるたぁ思う。
殊更「関数型言語の神秘性」を強調する為にラムダの深淵を如何にも魔術染みて話す、って事も可能なんだけど(それが伝統的に長い間行われている・笑)、それは僕の趣味じゃないし、ハッキリ言って「経験者」にはこっちの説明の方が簡単だ。そもそも関数を関数としてデザインすれば変数<->関数の対立構造が出てくるが、「関数も型の1つ」だとデザインされていれば変数に関数型による値を代入する事をもって「関数定義」とすれば済む話だ。
少なくとも、元々Schemeと言う言語はそういう工学的な主張を込めて設計されたわけだ。
通常、Javaのような言語だと、「ある機能を用いてある値を得たい」と言う場合、様式的には、
- 型を宣言して変数を準備する。
- コンストラクタでデータ領域を確保する。
- 使いたい値を改めて代入する。
と3ステップくらいかかるケースが多い。
一方、Lispは動的型付けなんで1.のステップが要らない。そして2と3を合わせて一気に行える。
繰り返すがラムダ式の本質は、オブジェクト指向的に言うと、「関数型と言うデータ型」に対してのコンストラクタだ。Javaで言う「コンテナ型の生成」と、ステップ数が違うにせよ、やってるこたぁ変わらない。
そして次の例を見てみる。
例えばラムダ式で
(lambda () (+ 2 3))
と記述すると、一見2 + 3の計算結果である5が返ってくるような気がするだろう。
しかし予想と違って、次のような何だか訳が分からんモノが返ってくる。
オブジェクト指向風に言うと、上のケースではラムダ式、と言う関数型、と言うデータのコンストラクタは、「2 + 3を計算する」と言う関数オブジェクト、あるいはインスタンスを返すように設計されている。しかし「計算自体は行われていない」。あくまで、「2 + 3を計算する」と言う「行為」と言うか「予定」がデータ型として返ってくるわけだ。Racketではそれが #<procedure>として表現されている。
実値を取り出すには返ってくる「関数オブジェクト」自身を実行しなければならない。次のように、だ。
Schemeのルールでは、リストの第0要素が関数オブジェクトだった場合、その関数オブジェクトが実行される。上の例で見ると「カッコが多い」気がするが、これはリストの第0要素にコンストラクタであるラムダ式を置いた為だ。フツーの言語、例えばPythonだったら次のように書いてる事に相当する。
JavaScriptならこうだ。
Rubyはちと書法が変わるが似たようなモノにはなる。
さて、ラムダ式が、オブジェクト指向で解釈すると「関数型」と言う「データ型」のコンストラクタなんだ、って事を把握した。そしてラムダ式と言うコンストラクタで「関数型」と言うデータ型を生成したと同時に「関数型」のインスタンスが即返ってくる、と言う事も分かった。そしてLispに於いてはリストの第0要素が「関数型」と言うデータ型だった際には即そのデータ型が「実行」される事も分かった。
従って、例えば次のようにSchemeで書いた場合の意味も分かると思う。
ここでコンストラクタlamdbaが作った関数インスタンスは「受け取った引数をそのまま返す」データ型、となる。2を受け取ったんで2を返してるわけだ。
ところが、当然と言えば当然だが、次のように書けばエラーになる。
これは上の関数インスタンスは「1引数しか受け取らない」と言う前提だから、だ。
つまり、「二引数を受け取りたい」場合は次のように書かざるを得ない。
これも当然、って言えば当然の話だ。
でも仮に「受け取る引数の個数が決まってない」場合は?
そう、当然「可変長引数を受け取る関数オブジェクトにしなければならない」と言う意味になる。
これはSchemeの仕様では、次のように書く、と決まっている。
引数リストがリストになっていない。剥き出しの(例えば)xと書く。これが可変長引数用の記法で、また、受け取った引数はリストとしてまとめられる。
これがSchemeに於けるラムダ式の記法だ。
既に知ってる、って人はそれでイイ。しかし敢えてまずはラムダ式の基本に立ち返ってみた。
さて、準備が整った。
ここでやっとラムダ式の拡張である、match-lambdaとmatch-lambda*に入っていく。
星田さんが述懐していたが、Gaucheのリファレンスマニュアルは、他のSchemeの実装に比べて分かりやすいのは事実だが、それでもこれは分かりづらい説明だろう。
言っちゃえば、Gaucheと言う処理系はプログラミング初心者Welcomeな処理系ではない、って事だ。ANSI Common Lispの経験やら、他Scheme処理系を使った事がある、って人がたどり着く処理系だ。
そして実は、少なくともRacketとMIT Schemeを除くと、商用を除いた多くのLisp処理系で「うちはプログラミング初心者を歓迎します!」って実装を見た事がない。
判別方法?簡単だ。その処理系がIDEを備えているかどうか、ってのが違いだ。少なくとも、「初心者Welcome」の処理系が、プログラミング初心者がEmacsを設定する、なんて想定してるわけねーだろ、ってな話だ。
これがgcc及びclangとVisual C++の違いだ。前者はプログラミング経験者が使う事を想定してるけど、後者ではMicrosoftがマジメにNew Comerを歓迎してる、って意味になる。
そしてPythonは初心者Welcomeだけど、反面Rubyはそうじゃない、って事だ。Pythonは取り敢えずIDLEを立ち上げれば即刻遊べる環境を用意してるが、Rubyはそうではない。
この差はマジな話、大きいんだ。結果、Racketが世界で一番ユーザー数を抱えるフリーのLisp処理系だ、ってのも当然だし、加えると、Racketユーザーは基本的にIDEであるDr. RacketがEmacsでの環境より上だ、って思っている。
まぁ、その意見が本当に正しいかどうかはさておき、いずれにせよ、プログラミングをやった事が無い人にとっては、IDE付きのプログラミング言語の方がアピールする、って当たり前の事実があるだけ、だ(※1)。
閑話休題。
Gaucheでのリファレンスマニュアルのここでの書き方は、プログラミング初心者に対しては確かに不親切だが、一応最初の例示を解題してみよう。
まず先の論の通り、match-lambdaはlambdaと同様に、「関数型」と言うデータ型のコンストラクタだ。そして関数mapは関数を第一引数に受け取り、後続するリストにそれを適用する。
そして、match-lambdaは1つしか引数を受け取らない。従って後続するリストも1つのみ、と言う縛りがある。
従って、後続するリストは次のようなリストだ、と想定されているわけ。
'((apple 1.23 (1.1 lbs)) (orange 0.68 (1.4 lbs)) (cantaloupe 0.53 (2.1 kg)))
これは単一のリストだ。よってmatch-lambdaが要求する「単一の引数になる」と言う条件を満たす。
mapによって繰り返し処理が成されてるが、「単一の引数」と言う条件で考えれば、ラムダ式と同様に、1つの引数に対して1つの作業をする事は出来る。
一方、単一の引数ではなく、例えばここで与える引数を
'(apple 1.23 (1.1 lbs)) '(orange 0.68 (1.4 lbs)) '(cantaloupe 0.53 (2.1 kg))
等と3つ与えれば、ここで設定されたmatch-lambdaはエラーを返す。
これは原理的には先に見た
((lambda (x) x) 1 2 3)
がエラーを起こすのと同じだし、また形式的には、
(map (lambda (x) x) '(1) '(2) '(3))
がエラーを起こすのと同じ事だ。
ラムダ式が一引数しか想定してないのに、3つも引数を与えられたら計算するモノも計算されなくなってしまう。
一方、match-lambda*は可変長引数を取る。形式的には、
(lambda x ...)
と書くのと同じだ。
従って、原理的には、
'(apple 1.23 (1.1 lbs)) '(orange 0.68 (1.4 lbs)) '(cantaloupe 0.53 (2.1 kg))
を与えても構わない。
ちと実験してみよう。
まず、quantity(重さ)の項目がlbs、つまりポンド表記が来るかkg、つまりキログラムで来るのかは分からない。従って、ここのパターンはunit(単位)にして誤魔化そう。
また、match-lambda*は可変長引数を受け取るので、パターン自体が可変長である、って言う前提になるし、加えるとまだ「本体をどう書けば良いのか」分からない。
従って、まずは実験としては、match-lambda*でパターンマッチされたitem、price-per-lb、quantity、そしてunitが、一体「どのようにまとめられるのか」見てみよう。
これも形式的には、上の方で見た
((lambda x ...) x y z ....)
と言う、ラムダ式の可変長引数版と書き方は実は同じだ。
加えて、可変長引数を受け取る前提のmatch-lambda*では、引数のパターンが同じなブツが繰り返し現れる場合は...で「以下同文」を表す。
これで、「同じ形式の」可変長引数全てに同じパターンマッチングを噛ます事が出来るわけだ。
さて、結果を見てみると、可変長引数は「itemのリスト」「price-per-lbのリスト」「quantityのリスト」「unitのリスト」と分解され、まとめられてるのが分かるだろう。
これらをまとめてリストになったものが返り値になってるのは、「そうしろ」って書いたからだ。別に多値を返すvaluesを使っても良かったし、その辺は重要じゃない。
あくまで重要なのは、パターンマッチングで判別された「パターン」の構成要素毎がまとめられてリストになってる、って辺りだ。言い換えると、これがmatch-lambda*の機能だ、って事になる。
従って、Gaucheのmatch-lambdaの例と同じ結果を返したいのなら、次のように書けば良い、って事だ。
「itemのリスト」「price-per-lbのリスト」「quantityのリスト」「unitのリスト」と言う4つのリストに対して処理する為に、match-lambdaの時には外側にあったmapが内側に入ってしまった。
ところで、match-lambdaに対してmatch-lambda*にはあるメリットがあるのに気づくだろう。
match-lambdaを用いた式には、単純には引数を追加出来ない。単一の引数しか取れない為に、その引数の方を肥大化させないとならない。与える引数を操作せねばならない、と言う事は、意外とプログラミング上はメンド臭くなったりする。
一方、このように、パターンマッチングで「いくつかのパターンのリストに分けられる」(このケースだと4だが)事がハッキリしてる場合、match-lambda*はいくつでも追加引数を取れる、と言う事だ。
当たり前だ。それが「可変長引数」の強みだし、そのお陰でこの機構を何かに組み込むとしたら、match-lambda*の方がプログラミングはラクになるだろう。
また、以前にも言った事があって、これはGaucheに限らないが、Scheme実装のリファレンスマニュアルで、分類がsyntaxになっていて、☓☓と同じ意味です、とかアッサリと書かれてる場合。それは十中八九、その機能がマクロで書かれている上、その定義式を言ってる、って意味になる。
Gaucheのマニュアルは、従って言外にmatch-lambda/match-lambda*は
として実装されてる、って言ってる。
この辺、Lisp経験が乏しいとピンと来ないだろうが、いずれにせよ、あまりに説明がアッサリしてる場合は、決してマクロに限らないけど、実装方法を説明してる、って考えた方がいい。
Lisperは親切だとは分かりづらい方法で、手の内を明かしてる、って事が良くあるんだ。
なお、僕がGaucheのパターンマッチングのマニュアルを読んで一番ビックリしたのは、パターンマッチング内で'lbsと'kgを判別してた事だ。
パターンマッチング、と言うのは名称の如く、与えられたデータが「どういう"構造"で記述されているか」をパターンとして認識するだけで、具体的なデータの中身が「何か」と言うのは通常感知しない。しかしながらGaucheのパターンマッチングはクオートによってその「データの中身」まで調べている、と言う事になる。
これにはマジで驚いた。
何度か書いているが、この「機能」を実現するには、Racketだとちと面倒臭い表記を使わないとならない。
Racketでは、Gaucheがクオートしたシンボルで流してたトコを==で判別しないとならない。いつぞや書いたが、Lispで==ってのは相当キショイ表現だ。
これに関して言うと、Gaucheの勝ち、だと思う。
※1: ちなみに、このテの話を書くと、十中八九「俺はIDEは使わない」「俺は初心者時代からEmacsを使ってた」とか言う輩がワンサカ現れる。
何度か繰り返すが、プログラマは理論的思考をする、って言うがこういう反応を見れば分かると思う。理論的でも何でもないんだ。このテのおバカ発言の「俺は」の一体どこが理論的なのか、考えてみろ、ってな話だ。
個人的経験を聞いてるワケじゃないんだ。非常にアタマの悪いリアクションだし、一体お前はどこの老舗天ぷら屋の職人の頑固親父なんだ、って言いたくなる。
天ぷら屋に失礼だって?だろうな。天ぷらは喰えるだけマシだ。vimだEmacsだ、なんつーのは食えないだけサイテーだ。
とにかく、プログラマはまずは「俺は」と言う個人的経験をあたかも普遍の事実のように言いたがる。自分の経験が万人の共通基盤だと言うつもりなんか。
こういう輩が集まって、かたやviが常識、かたやEmacsが常識だったりしてみろ。戦争が起こるのは当たり前だろ、って話になる。
加えると、「はじめからEmacsを使ってた」とかのたまう連中は大体大学でコンピュータサイエンスを学んできた層だ。大学なら、教師もいるわ同級生もいるわ先輩もいるわ、ってぇんで教えてもらえる環境が整ってる。言い換えると如何に自分が恵まれた環境にいたのかって事に自覚がない。世の中には独学派もいるんで、みんながみんな、そんな恵まれた環境にいるだろう、って前提がそもそも間違ってるんだよ。
理論的だ、って言うことは科学的態度として客観性がないとならない。しかし、プログラマは大方、その客観性って観点がまず無いんだよな。だから連中が言ってる「理論的」ってのは理論的でも何でもねぇんだ。単にパターンを数多く知ってるだけ。だから文系人間でもプログラミングは出来る。そもそも理論的、って言う程高尚な事をやってるわけじゃない、ってのは見れば分かるだろ、ってこった。
天ぷら職人には悪かったが、実際問題、プログラマは理数系の人間なんかより、全然むしろ職人に近い。そして理数系の感性を持ってる人間が絶対やらない、徒党を組んで誰か個人をいじめる、って事に対しても躊躇がない。Qiita、なんかが嫌いなのは、誰かが問題発言したらプログラマがワーッと集まって非難しだす、とかちと理数系な人間から見ると意味不明の行動を取るヤツが異様に多いからだ。あそこは常に炎上の坩堝だ。
そんなのとマジな話で一緒にされたくねぇんだよ。
ちなみに、マンガやドラマで、プログラマが理数系の代表、みたいなカンジに扱われるのをかなり不快に思ってる。冗談やめろよ、と。そもそもコミュ症=理数系なんざとんでもねぇ話だし。
そもそも、理学系の人間の行動や考え方と工学系のそれらも全く違う。同じ理系、って括りにされたくねぇ程だ。
たまたま近くにいる「一見」理屈っぽい奴らとしてプログラマが多いから、っつったって、それが理学系のステロタイプとは一致してねぇんだ。いや、マジで誤解すんな、そして誤解を蔓延させるな。
結果、意外とこの世の中、ホンモノの「理系の人々」と接点持ってるヤツが少ないんだな、とかある意味感心してはいるのだが。
そして、世の中にいる、プログラマとは性質からなんやら全部違う「ホンモノの理系」の人々は一体どこにお隠れになってるのか。あるいは絶滅してるのか(笑)。
非常に気になっている(笑)。
とにかく、プログラマが「俺は」と言い出したら警戒する事。客観的な観点に欠けた「俺語り」がはじまるだけ、って事だけは言っておこう。