Scheme:オブジェクト指向表現

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つの名詞に対して動詞を複数 書きたいならば、名詞を前に出すほうが楽、という「だけ」のことなのではないかと。

お邪魔終了。

戯: 2003/08/23 20:37:07 PDT ところでForthみたい(?)に、括弧内の第一要素じゃなく最終要素が手続きだと見なされるLisp亜種が有れば、「自然」になるんだろうか?

  #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);

戯: 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に関してどんな操作が 可能なのか一目で判断がつきにくいという欠点もあります。

準備済みの構造と、関数による抽象

  (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)))

委譲

More ...