まずはANSI Common Lispの話から始めよう。
ANSI Common Lispのmemberはキーワード引数付きの高階関数として設計されている。
この程度の事ならSchemeでの演算と大した違いはない。
しかし、Schemeの目から見ると、こういったイカれた事も可能だ。
そもそも、memberとは、「リストの中から探したいアイテムを先頭にしたリストを返す」機能の筈なんだけど、ANSI Common Lispのmemberだと、「リストの中で、探したいアイテムと一致しないアイテムを先頭としたリスト」を返す機能まで含まれてる、って事だ。正直言ってワケが分からん(笑)。
しかし、ANSI Common Lispの場合、基本的には1関数に機能が詰め込めるなら詰め込む方針で設計されている。
正直言うと、インタプリタとして見た場合、こういう機能満載主義のせいで、ANSI Common LispはSchemeより速度は遅いと思う。ただし、ANSI Common Lispはコンパイラ指向なんで、インタプリタ上で速いか遅いかは問題にはならない。何故ならコードをコンパイルしちまえば最適化する確率が常に高いから、だ。
よってANSI Common Lispの関数が機能塗れで重い事をユーザーはあまり気にしてない。むしろ、機能が多ければ多い程、書くコードは常に短くなるから、だ。
さて、ANSI Common Lispには構造体がある、んで、ちとこう言った構造体を作ってみる。
foo、bar、bazと言う3つの構造体を作ってみた。
それぞれのインスタンスも作ってみる。
そしてこれらを含めたリストを定義してみよう。
当然、変数fugaを探せ、なんて言った場合はmemberはそれを先頭としたリストを返す。
ここで、「目的とする」アイテム自体が返らず、それを先頭とした残りのリスト全部が返る設計にしてる事を不思議がる人が多いかもしんない。
真値を返すように書くのがメンド臭かった・・・ってのは冗談だが、単に「ひょっとしたら残りのリストに何か役立つ情報があるかもしんない」と考えたんだろう。真値を返すように設計する、とか目的のアイテムだけ返すようにする、って事は情報を塗りつぶしてしまうと言うのと同義だ。Lisperは情報を塗りつぶすのを好まない。
少なくとも設計方針としてはLispは常に関数型指向だ。と言うことは常に返り値を別の関数の引数として直接連鎖させて書いていくと言うのが好まれるスタイルとなる。
と言う事は返り値に情報が詰まってた方が有利なんだな。そしてデータは偽値でない限りは常に真だ。よってこういう設計方法はLispに関して言えば常に好まれる(また、memberが単なる述語じゃないのもこういった理由だ)。
それに、単に目的とする値だけ返したい、ってぇのなら「常に目的とする値を先頭にした残りのリストが返る」以上、carを取る事に特に煩雑さはない。
さて、ここで問題だ。
今、ここでリスト要素の、barインスタンスのyスロットが4、って条件のブツを探せ、と言ったらどうなるだろうか。
この辺は、さすがに構造体が仕様に含まれてる事がANSI Common Lispでは想定内なんで、次のように書けば凌ぐ事が出来る。
ANSI Common Lispでは、:testと言うキーワード引数で、等価判定述語をラムダ式で与える事が出来る。
ここでは、まず、*data*から抜いてきたiがbar型であるかどうか調べた上、そのyスロットが改めて4と等価かどうか調べてる。
あるいは次のように書いてもいい。
:keyキーワードで要素の「どこを」等価判定するか指定出来る。当然、探しているブツがbarのインスタンスであり、bar-yが4である、事が条件だ。
結局、Racketで言うと、memberはコンピュータサイエンスの宿題的な意味だと
と書くのが典型的なパターンだけど、ANSI Common Lispでは
と言うようなカンジで定義されてる、って事になる。
さて、ANSI Common Lispは原則member1つしかないが(※1)、伝統的にSchemeではmember、memv、memqと言う3つの同系統の関数が用意されている。
これらは、それぞれ、等価関数としてequal?、eqv?、eq?を用いたヴァージョンだ、と言う事だ。
Schemeは基本、ミニマリズムな為、Genericな方向性のANSI Common Lispとはこの辺が違う。特に等価判定によって結果が違う事があり得る場合は、関数は分けておく、ってのが伝統だ。
しかし、実はこの方針は、ちと面倒なトコがある。
例えばだ。次のようなリストを考えよう。
(define *data* '("星田" "オステオパシー" "代表" "田和" "洋一"))
ここから"代表"と言う文字列を探すとすれば、等価関数にequal?を用いるべきなんで、それを含んでるmemberを使う。
ところが文字列、だ。ぶっちゃけた話、リストに含まれてる要素が全部文字列だけ、だと分かってるのなら、string=?で比較した方が速い。equal?はstring=?を含んでるが、含んでるが故、単純に言うとスピードではstring=?そのものに負けるんだ。
伝統的に言うと、この辺の解決策がR5RSまでには存在しなかった。
そしてANSI Common Lispならこの辺、memberの:testキーワードにstring=を与えるだけ、で解決可能なわけだ。
さて、こういう状況の中でSRFIが立ち上がった。
んで、平たく言うと、SRFIで成される提案は、結構な確率でANSI Common Lispの機能をSchemeに持ってこようぜ、ってのが多い。
ANSI Common Lispは機能塗れで、ある意味不格好なんで、Schemeのミニマリズムとは相反してるんだけど、便利なモノは便利だろ、ってのはそりゃその通りなわけだ。
そしてSRFI-1でmemberを改良しようぜ、って提案が成されたわけだ。
つまり、memberをオプショナル引数として等価判定関数を取る真っ当な高階関数にしようぜ、と言う提案だ。
そして、こうなると、事実上memqもmemvも要らない子になる。もっと言うと、memqもmemvもmemberで定義出来る、って意味になるわけだ。
とまぁ、長い間こう言う「提案状態」でほっとかれたわけだ。パワーアップしたmember?を使いたい人は、SRFI-1が使える状態になってるのなら、自身が使ってるR5RS実装にSRFI-1を読み込んで使う、と言う状態だったわけだな(そしてそれはある意味、現時点でも続いている)。
んで、多分、だけど、R6RSと言う仕様はこのSRFI-1の提案を全面的に採用した、と思う。ただし、肥大化したR6RSは反対者が多かったんで、これに従ったメジャーなScheme処理系は非常に少ない。知られてるモノでもR6RSに追従したのは、旧PLT Scheme(現Racket)、Gauche、Chez Schemeくらいしかなかったんじゃないか?Scheme実装者コミュニティは割れ、結果殆どの処理系はR5RSに留まる事となる。
前回のR6RSでは離反者が多く出た為(特に、PLTがSchemeを止めていくのはこれがキッカケ、って言って良い)、R7RSは「仕様をR5RSのように小さくまとめた」ブツと「R6RSのようにライブラリてんこ盛りの仕様」の2つの仕様にしようよ、とダジャレなような事を考えたわけだな(笑)。
小さいヴァージョンのR7RSは割に簡単に決まったが、反面どデカヴァージョンは全然決まらない(笑)。そしてR5RSのままとどまった実装者は「成り行きがどうなるか」じっと見てる。この時点でR7RSの「小さい」版でも手を出そうとしてないわけだ。そして場合によっては「デカいR7RS」からも離反するだろう。
これが現時点でのR7RSの状態だ。すなわち、Schemeのデファクトスタンダードは依然とR5RSだ、ってのが世界的な傾向なんだ。
以前も言ったけど、現時点でR7RS-smallに準じた処理系を嬉々として作ってるのは日本人実装者が殆ど、って状況なわけ。歴史ある著名なScheme実装の殆どはR7RS-smallでさえ無視してる、ってのが今現在の状況で、R7RS-largeがどうなるか、現時点でもサッパリ分からんので殆ど「出る出る詐欺」の状態になってるんだな。
さて、R7RSはR6RSを受け継いだ部分も多く、レコード型が入った事によりmemberもR6RSで採用されたmemberになっている。
いずれにせよ、ここではオプショナルである高階関数としてのmemberが採用されている。つまり、R5RS時代と違い、レコード型の中身を引きずり出して比較する等価判定関数を与えられるmemberへとヴァージョンアップしている。
これは、R6RSに続き、レコード型を含んだ仕様になっている以上避けられない決定だと思われる。
つまり、ここで初めて、ANSI Common Lispのようにレコード型の中身を覗けるmemberになったんだ(※2)。
Gaucheでの例
RacketもPLT時代にR6RSに準じてたんで、memberはオプショナル引数として等価判定関数を取れるように拡張されている。
よってラムダ式を駆使して、構造体の中身まで調べた結果を返す事が可能になってる。
ガンガンラムダ式を利用してmemberを使っていって構わない。
往年の、教科書の演習問題的なmemberに比べて遥かに強力で使い勝手があるようになってる筈だ。
※1: member-if、member-if-notと言う亜種が存在するが、これらは原則、memberの複雑なキーワード引数による動作を整理した限定版、だと言う事が出来る。
※2: つまりここで書いた「memberも値を探すのが基本的な機能であって、構造体の中身をチェックするようには書けない」ってのは一般的なSchemeの話、つまりデファクトスタンダードであるR5RSの事だ。
しかしながらSRFI-1/R7RS/Racketのmemberは拡張されてるので書くことが出来る。
注釈の6番を参照の事。