Scheme:オブジェクト指向表現
yasu で出た話題
Schemeの表現は、メソッドが列記されているだけで、人間に優しい形になっていない ように見えます。オブジェクト指向のように、人間のイマジネーションを刺激する ような形にはなるのでしょうか? それとも関数型プログラミングでは、人間の イマジネーションを刺激しなくてもよく、ただ数学的であるだけで全部解決できて しまうのでしょうか?
(define animal-human-run (略)) (define animal-human-eat (略))
に関していろいろと。
HashedWiki:クロージャとオブジェクトから参照されてたり。 InterWikiでdiscussion。
Schemeとオブジェクト指向について
(Shiro): Schemeの表現は、主流のオブジェクト指向表現と相反するものでは無いと思います。 単にSchemeでは言語仕様内にオブジェクト指向メカニズムを含めていないというだけで、 Schemeでオブジェクト指向を使う方法は色々あります。
特定のSchemeに依存しないで議論しようとすると、関数をずらずらと 書き並べる形になり、これは主流のオブジェクト指向の表現に比べると冗長で 見づらいことは否めません。私も、50行以内の書き捨てプログラムならともかく、 まとまったプログラムをオブジェクト指向メカニズムのサポート無しで書くのは しんどいです。
Schemeでオブジェクト指向メカニズムを使う方法は色々あります。
CLOS的なメカニズム(Gaucheでも採用しています)ならこんな感じ:
(define-class <animal> () ...) (define-class <human> (<animal>) ...) ; <animal>を継承 (define-method run ((self <human>)) ...) (define-method eat ((self <human>) thing) ...) (define shiro (make <human> :name "shiro")) (run shiro) (eat shiro 'pizza) ...
CLOS風のOOは、マルチメソッド (複数の引数のクラスを基にメソッドをディスパッチ できる)のが特徴ですが、そのためにメソッドを適用する時に、(動詞 名詞 ...) のように書かねばなりません。多くの主流OO言語では 名詞.動詞の順になり、 この方が「自然」だとも言えるでしょう。そのようなOOメカニズムを書くことも できます。具体的に既存の実装がすぐに思い浮かびませんが、例えば次のような システムを書くのは容易でしょう。
(define animal (object () (run () ...) (eat (food) ...))) (define human (object (animal) (work (job) ...)) (define shiro (object (human) ...)) (shiro run) (shiro eat 'pizza)
名詞.動詞という語順について
お邪魔割りこみします -戯
名詞.動詞という語順が「自然」なのかどうか?については、疑問を感じています。 語順つまり文法なんて慣れ(と本能?)次第でどんなものでも受けつけられようになる んじゃないかなと。つまり自然は事象じゃなく受けいれる人間の中に有ると。
Smalltalkでのあの語順の意味というか価値については、 「カスケード(メッセージの流し込み)」という文法が それにあたるんじゃないかと思います。 オブジェクトXに対してaしてbしてcしたいと思ったら、 「X a; b; c.」と書けるというアレ。
つまり、MultipleDispatchのために1つの動詞に対して名詞を複数書きたいならば、 動詞を前に出すほうが楽で、一方カスケードのために1つの名詞に対して動詞を複数 書きたいならば、名詞を前に出すほうが楽、という「だけ」のことなのではないかと。
お邪魔終了。
- そうか。カスケードか。なぜ「自然」と思ったかというと、書き捨てコードを がーっと書いてく時に X.a().b().c() の順で書くほうが、(c (b (a X)))よりは 考えの流れにそっているなあ、と感じていたからでした。--Shiro
- 確かにそういう語順のほうが自然ですね。蛇足ですがSmalltalkのカスケード式は、レシーバを保存しておいてメソッドの返り値は捨てるというシンタックスシュガーですので、(c (b (a X))) ではなく単に (begin (a X) (b X) (c X)) になります。--SHIMADA
- 勘違いしてました。Shiro
戯: 2003/08/23 20:37:07 PDT ところでForthみたい(?)に、括弧内の第一要素じゃなく最終要素が手続きだと見なされるLisp亜種が有れば、「自然」になるんだろうか?
- カリー化を考えれば不自然に思えます。むしろ FC++ を使用した以下のようなコードをラップして C++ (の Metaprogramming)で実現してやった方がいいかも。コンパイルがまた遅くなる……
#include "prelude.h" using namespace fcpp; struct X { void a() { ... } void b() { ... } void c() { ... } }; X x; (&X::c ^of^ &X::b ^of^ &X::a) (&x);
- でも、 of の逆で束縛させる in や後置演算子を作るくらいなら、 State Monad を使った方がいいかもしれませんね。う〜ん、悩むなぁ。--shelarcy
戯: 2003/08/23 20:37:07 PDT 脱線ですが複数について。というか多数について。 (手続きとかのための)名前は大量消費したいと思わないけど、 名前に対応する有り得べき実装は多数(クラスの数だけ)作りたい、 という場合には、クラスに名前経由で実装をぶら下げるのがコスト的に妥当なのでしょうね。 要するにクラスの名前空間としての性質に期待してる。 (逆に実装の数がせいぜい名前の数と同じくらいで十分だと思う場合、つまり実装をケースバイケースで使い分けずに済む 場合は、伝統的なLispのスタイルになるんだろうな。)
抽象化の方向
(Shiro): 「イマジネーションの刺激」というのが具体的に何を指しているか よくわからないのですが、抽象化の単位としてオブジェクトを持って来るか 関数を持って来るかで考え方が変わって来る例はあると思います。
関数型における抽象化の力は、関数に対して別の関数を渡したり 関数から関数を返したりして初めて表れます。それをはじめると、 その先にも豊かなイマジネーションの世界が広がっているとは思います。
例:木のトラバース
関数指向の考え方をちょっと示してみます。 与えられた「木」のすべての葉を深さ優先で辿り、 与えられた処理を施して行く、という処理を考えてみます。 但し、木の具体的な実装はわかりません。
オブジェクト(クラス)指向ならば、「木」の持つ普遍的な操作に注目し、 そういう操作を備えた抽象クラスをまず考えることでしょう。
class Tree { public: virtual bool isLeaf(); virtual void walk(void (*proc)(Tree *)); }; class Leaf : Tree { public: bool isLeaf() { return true; } void walk(void (*proc)(Tree*)) { proc(this); } }; class Node : Tree { public: bool isLeaf() { return false; } void walk(void (*proc)(Tree*)) { list<Tree*>::iterator i; for (i = children.begin(); i != children.end(); i++) { i->walk(proc); } } list<Tree*> children; };
とか。C++最近触ってないからどっか変かもしれませんが。 rubyやpythonならもっとすっきり書けるでしょう。
関数型では、抽象的な「木」に関する可能な操作を直接関数で渡してやります。
(define (tree-walk tree proc leaf? walker) (define (rec node) (walker (lambda (n) (if (leaf? n) (proc n) (rec n))) node)) (if (leaf? tree) (proc tree) (rec tree)))
ここで、leaf? は木のノードを取り、それが葉かそうでないかを返す関数、 walkerは関数と木のノードを取り、ノードのすべての子供に対して渡された 関数を適用する関数。です。例えば木がリストで表現されているとしたら、 leaf? は (lambda (x) (not (pair? x))) であり、walkerはfor-eachです。 木がファイルシステムだとしたら、leaf?は (lambda (x) (not (file-is-directory? x)))に、 walkerは(lambda (proc x) (for-each proc (list-directory x)))とかに なるでしょう。
クラス指向のメリットは、データ定義を見ればどういう操作が可能かわかる ところです。一方、その操作をしてやるには具体的なデータが抽象的なTree クラスを継承してなければなりません (インタフェース継承だけで良いんですが)。
関数指向のメリットは、tree型に対して適用可能な操作というものを限定して いないことです。tree-walkを適用したくなったら、leaf? と walkerに相当する 関数を(なんならその場ででも)作って渡してやれば良いのです。しかし、渡す操作が 2〜3個でなく5個も10個も必要だったりすると繁雑になりますし、 tree-walkを適用しているコードを見ただけではtreeに関してどんな操作が 可能なのか一目で判断がつきにくいという欠点もあります。
- (nobsun): tree-walk の例は面白いのでちょっと脱線気味に参入します。:-)
tree のトラバースは、要するに与えられた tree のすべてのノードを一列のリストに ならべればよいわけです。そこで、children という「tree のノードをもらって、 その子のノードをリストにして返すような抽象的な手続きを考えます。 もしある種の木構造についてこの children が定義できたとするとその種(型)の木構造 にたいして、深さ優先のトラバースは、(define (dfs t) (cons t (concat (map dfs (children t))))
で表現できます。また、幅優先のトラバースも children をつかって表現できます。 木構造は、この children によるトラバースができきるような構造であると抽象化する ことも可能です。これは、Java の interface のやりかたですよね。 ただ、Scheme にはそのような型の概念がないので表現できませんが、Haskell では のやり方でデータの抽象化を行います。
- (Shiro) : うわー、ここでlazyかeagerかの差が出ますねえ。 Schemeでこれ(children)をやってしまうと、ノード数が10^5とか10^6とかになった時に gcしまくりになってしまいそうです。Schemeでも意識してlazy walkerを作ることは できますが。
準備済みの構造と、関数による抽象
- 戯: Lispが木構造とかを事前準備無くサクサクアクセスできるのは、木の更に構成要素である細胞(^^;もといLinkedList Cellの 組み合わせというメタな(?)構造が、事前に準備済みであってそれの存在を仮定できるから、ではないのですか? 細胞を対象とした操作を事前に用意しておくことは、C++でも出来るし、Lispでも用意してあるからこそ操作可能なのだし。 C++ではその操作を多彩に組み合わせた大型処理の手続きを(その場で)作るのは無理ですが、 これはOOPだからではなくC++が静的言語だからであって。
- (Shiro) この木の例に関しては、コンスセルによる表現を全く仮定していないっす。 (walkerはリストを返す、とかいう制約もついていません)。 コンスセルだろうがアレイだろうが適当なコンテナタイプだろうがファイルシステム だろうが、詳細をクロージャの中に隠蔽する、というのがクロージャ指向 :-) の流儀だと思います。
- (nobsun) リンクトリストではないコンスの例(SICP Exercise 2.4)
(define (cons x y) (lambda (m) (m x y))) (define (car z) (z (lambda (p q) p))) (define (cdr z) (z (lambda (p q) q)))
委譲
- 戯021214のスルとかサレルとか繋がりですが、「具体的なデータが抽象的なTreeクラスを継承」しなくても良いのでは。
- これは近年しばしば言われる「継承より委譲だぜ」によって克服されるべきものなのでわないかと。
- 「それ(継承まんせー)をObject指向と呼ぶのだ」と言われてしまえば返す言葉もないですがm(__)m
- この場合の委譲は、(Work)Dataの「並び(戯021211参照)」を具体DataからContainerに委譲しちゃえ、という事になると思います。
- で、Lispがやってることは、超強力なContainerが用意されてるから、それに出来るだけ多くの事柄を委譲しようぜ、ということなんじゃないかと。
- Containerに乗せるのは関数(Cの意味のであったとしても)であっても良いのね。そういやCでも関数ポインタ配列(NULL終端)を渡したりするし。
- RubyやJavaやC++だと、List以外の色々なContainerも有るぜよ、ということですね。色々有ったからといって3倍強いかどうかは別問題ですが。
- これは近年しばしば言われる「継承より委譲だぜ」によって克服されるべきものなのでわないかと。
- Shiro (2002/12/13 18:25:13 PST): ここでの「超強力なContainer」がクロージャを指している ならそのとおりです。このtreeの例では、コンスセルには依存しない、 クロージャによる抽象を表現したつもりでした。
- んで、mainstreamなOOPにおいてcontainer作って委譲でやってもいいんですが、
そのcontainerという枠をどっかで定義しといて、呼び出し側もそれに関数ポインタ
やらなんやらをセットして呼び出す感じになるんですよね? (勘違いしてたら
直して下さい)。それって、クロージャが自動的にやってくれることを
わざわざ人間が書き下している、という感じがします。
(Paul Grahamのいう「人間コンパイラ」)
- もちろん、関係が複雑になってくるとちゃんと枠組が明文化されてたほうが いいので、containerクラスやらインタフェースやらをちゃんと書いとくことにも 意味があるでしょう。大きな組織では特に。 ただ場合によってはその場でちょろっとクロージャを2〜3個作って渡せば 済むところまで、その枠組みを強制されるとつらいなあ、と思います。 (逆に、全部クロージャ、というのもつらい。規模が大きくなって来たときには あるていど制約がかかる枠があったほうが理解しやすくなる)。
- 戯 2002/12/13 20:36:16 PST いや、Containerってものは本質的に委譲先である (どんな機能を期待されてるかは上述の通り) よね、という話のつもりです。
- rubyのiteratorを思い浮かべて書きました。rubyはmainstreamじゃないでしょうか?(^^; ま、(内部)iteratorがOOP的か?という議論もありますが。
- Shiro: ああ、rubyのiteratorはクロージャと等価なんで (と言い切ってしまうと 語弊があるのかな? スコープの考え方がrubyはオブジェクト中心っぽいし。 でも表現力としては等価と言って差し支えないかと)、そういう意味でなら そうですね。
- Shiro: 「委譲」ってば、話は逸れますが、
一部の言語にある "delegate" にどうも違和感があります。
要するに「型検査付き機能限定クロージャリスト」ですよね。それだけのために
新たな構文を導入しなければならないのかなあ。
コンパイラが静的に型を知ることができないとだめなんで、新しい言語要素の
導入は仕方無いにせよ、もちっとメタな仕組みにして、その一例として
delegateみたいなことができるよ、というほうが綺麗なような。
- 戯: どの言語ですか?(^^;言語によって意味が違ってくるような。そういやrubyにゃForwardableってのが有る
- Shiro: .Net方面です。Dにもあるか。 私的にはdelegateって聞いたらproxyオブジェクトみたいのを思い浮かべるんで そういう違和感もある。
- ああC#。DeleGates(?)とかいう珍妙な命名は純粋にMSの所業だと思います。MethodPointerであるかどうかと、委譲であるかどうかは、俺も別問題だと思うんですけどね。というわけ(?)でDでは名は違います。-戯
- Tiki:Delphiの特許だそうですから、特許として成り立たせるためにわざわざ独自性を強調した、んだったりして(違
- Dの場合は「できないと駄目」ではなく「できないと(Performanceで)損をする」ですかね。(IDEが)RTTIを使ってアクセスCode(Source)を逆生成するわけですから、ソースや静的Compile結果はCacheみたいなもんだと感じてます。
- (Dで)構文かどうかについては、単にPascalだから構文を増やさないと気がすまなかった、というだけかも(藁
- 冗談はともかく、やっぱり考え方にはまず静的ありき、そこに色々なものを足した、のでしょうね。
- 戯021214で嘆いたように(^^;、CだのPasだの系の言語は、データとデータがくっつくだけでもう大事業なんですよね。