Gauche:スロットアクセス

Gauche:スロットアクセス

hiraさんのとこより:

2004/03/02 07:42:32 PST: slotにprivateなアクセッサを定義しようとして、あれ?っと思いました。 generic functionでディスパッチということは、自他の区別なんて無いのかと。 moduleのimport/exportが近いけど、staticな変数の可視/不可視の制御なので、ちょっと違う。 slotにpublic/privateの概念を持ち込むにはどうすればいいのだろう? クラスの中に入る仕組みがあれば良いのだろうけど。 そうすれば、slot-refにイライラしなくても済むっていうオマケも付くのだが。

(slot-ref self 'hoge) ;;21文字 CLOS:フツーの書き方
this.getHoge()        //14文字 Java:thisマニアが自己カプセル化したときの書き方
hoge                  ;; 4文字 CLOS/Java:私が望む書き方

スロット参照構文

Shiro: 私もslot-refにはうんざりしてます。最近はほとんど (ref self 'hoge)ですが、それでも面倒です。

with-slot

スロット名だけ、っていうのは、CLOSだとwith-slots構文が ありますね。Gaucheでも参照に関してはマクロですぐ書けますが、 (set! hoge value)を実現するのは難しそうです。

(with-slots (hoge huge hage) object
  ... hoge ...)  ;; <- この中でのhogeはobject.hogeを参照

いくつかのScheme処理系には、識別子自体をマクロフォームとして 展開できるものがあり(例: SchemeCrossReference:define-id-macro)、 そういうのを使ってwith-slots内のhogeを(ref object 'hoge)に 展開してしまえば、set!も使えます。

ただ、わたし自身は「レキシカルスコープだけ」という単純さが Schemeの強さだと思っているので、複数のスコープルールを持ち込むのは 話をややこしくするだけだと思います。例えば:

(with-slots (hoge fuge hage) object
  (lambda () hoge))

こんなふうにクロージャを作った場合、クロージャ内のhogeは 何をクローズしているでしょうか?

Scheme的に考えれば、この時点でレキシカルなobjectに束縛された オブジェクトのhogeスロット、ですね。 ただ、インスタンス変数にselfやthis無しで アクセスできるようなOOPの場合、暗黙のスコープとして 「その時問題になっているインスタンス」が仮定されるので、 クロージャ作成時のobjectではなく、 クロージャ実行時のobjectを参照したくなる場合もあるんじゃないでしょうか。 (そもそもクロージャなんてない、ってOOPでは、作成時のobjectを 閉じ込むっていうこと自体できませんし)。

[x y]

'ref'は特に、オブジェクトのスロットの先のオブジェクトのスロットの… とたどってゆくときにうざくなります。

  (ref (ref foo 'bar) 'baz)

今、ぼんやりと、これをこんな風に書けたらどうかなあと思っています。

  [foo 'bar 'baz]

メカニズムとしてはリーダーマクロでrefのネストに展開します。 これだと、普通のスロットアクセスは [this'hoge] とか (set! [this'hoge] value) になります。どんなもんでしょ。

ちなみに、[x y]を(ref x y)に展開する、とだけ定めておくと、 自動的にこういうこともできます。

  (define x '#(a b c))
  [x 1]  => b           ;; vector-ref
  (define x "abc")
  [x 2]  => #\c         ;; string-ref

暗黙のself

Shiro: 何をselfにします? define-methodは第一引数を特別扱いしませんよ。 ああ、そうじゃなくて、selfへの束縛は自分で作ってもいいのか。 うーん、暗黙の変数参照はいや〜んだなあ。それに、複数のオブジェクトが 絡むパターンで困りませんか。

例えば、オブジェクトyのa,b,c,dスロットの値をxにコピーするとき、 よくあるパターンはこんなんです:

  (for-each (lambda (s) (set! (ref x s) (ref y s)) '(a b c d))

self明示だと:

  (for-each (lambda (s) (set! [x s] [y s])) '(a b c d))

暗黙のselfにするとどう書きます?

xがselfであるならこうなりますね。

(for-each (lambda (s) (set! [s] (ref y s))) '(a b c d))
=>(for-each (lambda (s) (set! (ref self s) (ref y s))) '(a b c d))

selfの省略は自クラスではなく自インスタンスに対するものなので、片方のみが[]アクセスとなります。 「可視性とアクセッサのサンプル」のcf4と書けずにcf2にした感がありますね。 そもそもgeneric functionにクラス/インスタンスメソッドという区別が無いんだよなぁ。あえて言うならクラスメソッドか。 んー、クラス/インスタンスメソッドなんて言い出すと、またクラスの内外が問題になってくる。。。 つづきは可視性のところで。-- hira

object-applyの利用

Shiro: これだと、object-applyを自分の目的に使いたいクラスが 困ってしまうんですよね。

可視性の制御

Shiro: Scheme的には、「好きなOOPを使う」ってことでいいんじゃないですか。 共用するライブラリは何らかの共通フレームワークがあったほうがいいので あんまり毛色の違うマクロや名前がぶつかるシステムはまずいですが、 外に見せるのがクロージャだけ、というのならそういう問題もないと 思います。

このへんも参考になるかな:Scheme:制約の拡散

可視性の制御にも、他のメカニズムを持ち込むのではなくレキシカルスコープ 一本でゆくのが正しいと思うので、CLOSスタイルのスロット定義に うまくクロージャを絡められるとおもしろいとは思います。 例えばvirtual slotのsetterとgetter間で状態を共有するのに、今は インスタンスを介するしか方法がないですが、いちいちそのために 別スロットを作るというのはどうも汚い。 何か、クラス定義時点で、インスタンス作成時に作られる静的スコープ みたいなものが作れるとよさげです。

hira:うん。それが出来ればいいですね。 define-classの中にdefine-methodを書くようなイメージになりますか? そうなると「メソッドはクラスに属さない」という事から逸れそうだけど。

fuyuki: これは私も気になっているところです。 CLOSとクロージャというのを私はおおざっぱに 大小2本の刀があるようなイメージで捉えているんですが、 両者がうまく使い分けられるか? 協調できるか? といった点には いまだに自信がありません。 あと、CLOSは本当にSchemeになじむのか? ということも。

fuyuki: ちょっと余談ですが、私はこのところ 「多重継承の禁止」や「メソッドを束ねるクラス」は 「最適化として」正しいのかもしれないなんてことをぼんやり考えています。

可視性とアクセッサのサンプル

Java

public class Hoge
{
    private Object fuga;
    public        Object getFuga()           { return fuga; }
    public        void   setFuga(Object f)   { fuga = f; }
    public        void   cf1(Hoge y)         { fuga = y.fuga; }           //NG
    public        void   cf2(Hoge y)         { fuga = y.getFuga(); }      //OK
    public        void   cf3(Hoge y)         { setFuga(y.getFuga()); }    //めんどい
    public static void   cf4(Hoge x, Hoge y) { x.fuga = y.fuga; }         //NG
    public static void   cf5(Hoge x, Hoge y) { x.setFuga(y.getFuga()); }  //めんどい
}

Scheme:シンプルなメッセージパッシング

(define (make-hoge)
  (let1 fuga #f
    (define (self m . args)
      (define (get-fuga)    fuga)
      (define (set-fuga! f) (set! fuga f))
      (define (cf1 y)       #f)
      (define (cf2 y)       (set! fuga       (y 'get-fuga)))
      (define (cfs y)       (self 'set-fuga! (y 'get-fuga)))
      (define (cf3 y)       (set-fuga! (y 'get-fuga)))
      (cond ((eq? m 'get-fuga)  (apply get-fuga  args))
            ((eq? m 'set-fuga!) (apply set-fuga! args))
            ((eq? m 'cf1)       (apply cf1       args))
            ((eq? m 'cf2)       (apply cf2       args))
            ((eq? m 'cf3)       (apply cf3       args))
            ))
    self))

(define (cf4 x y) #f)
(define (cf5 x y) (x 'set-fuga! (y 'get-fuga)))

instance-privateとclass-private

privateといってもinstance-privateとclass-privateがある。 クロージャの束縛に「ドットで直アクセス」なんて出来ないということ。

          instance-private  class-private
    CLOS        ×               ×
    MPS         ○               ×
    Java        ×               ○
    Ruby        ?               ?(WRITE-ME)

番外編:Message Passing Style (MPS)について

hira:MPSのスロットアクセスは、なかなか良い感じです。 でもそれ以外の面で、色々と不満があるのも事実。 MPS側の視点から理想を眺めてみよう、というコーナー。

  1. 新しい型になりたい: 現状では、型は常にプロシージャ。 あるプロシージャに対して、型を付与することって出来ないのだろうか。
  2. generic functionにかまってもらいたい: 型が常にプロシージャなのでgeneric functionが利用できない。 object applyとか、便利な機能を利用したい。
  3. メッセージのディスパッチ部分を書くのが面倒: 人間コンパイラにはなりたくない。
  4. ctagsがメソッド(メッセージ)を拾ってくれない: トップレベルでdefine*しないと拾ってくれないっぽい。
  5. 関数世界との関係にどう折り合いをつければいいのか: CLOSはgeneric functionにより関数世界にとけ込んでいる。 MSPは?関数世界をRubyのKernelやObjectのように扱えばいいのかしら?

※関数世界とは(proc obj arg...)という世界。オブジェクト指向世界は(obj proc arg...)となる。

Shiro: Gaucheのクラスは型システムって面もあるので(弱いけど)、 プロシージャを拡張するという考えではなく、新しい型をプロシージャ的に 振る舞わせるって考えはありますね。要はobject-applyを使うんですが。

舞台裏はこんな感じ。ただ、ユーザは気にする必要はありません。

(define-class <mps-meta> (<class>) ())

(define-syntax define-mps
  (syntax-rules ()
    ((define-mps (name . args) . body)
     (define-mps name (lambda args . body)))
    ((define-mps name maker)
     (begin
       (define-class name ()
         ((%maker   :allocation :class :init-value maker)
          (%closure :init-keyword :%closure))
         :metaclass <mps-meta>)
       (define-method object-apply ((class <mps-meta>) . args)
         (make class :%closure (apply (class-slot-ref class '%maker) args)))
       (define-method object-apply ((obj name) . args)
         (apply (ref obj '%closure) args))
       name))))

以下のような具合に、ほぼクロージャベースのオブジェクトのように使えますが、 作られるオブジェクトはfooクラスのインスタンスになっています。

gosh> (define-mps (foo x)
        (lambda (msg . args)
          (case msg
            ((get) x)
            ((set!) (set! x (car args))))))
#<class foo>
gosh> (define f (foo 4))
f
gosh> (f 'get)
4
gosh> (f 'set! 5)
5
gosh> (f 'get)
5
gosh> (class-of f)
#<class foo>

ディスパッチ部分に関しては、別途マクロを書くのがよいのでは。 それはdefine-mpsとはorthogonalに書けると思います。

hira:凄い。これでobject-*メソッドを定義出来ます。Gaucheの加護が得られるようになりました。やったー。

fuyuki:ディスパッチマクロの例としてはこんなのがあります。 http://lists.sourceforge.jp/mailman/archives/gauche-devel-jp/2003-January/000118.html

hira:上のメールから抜粋しときました↓。

> 3. case によるディスパッチはやっぱり格好悪い。

> 3はかなり一般的な問題だと思うので(gettext.scmでもやっている)、何か適当 > なユーティリティを提供したほうがいいのかもしれません。

そうっすねえ。このへんOO言語ならかえって悩まなくても いいんでしょうが… Gaucheのオブジェクトシステムだとこういうローカルな メソッドバインディングは扱いにくいですね。

実行する側からだと、メッセージによってディスパッチする 手続きは効率もそんなに悪くないですし、素直だと思うんですよ。 問題がコードの字面だけなら、こんなマクロを使ってしまえば:

(define-syntax dispatching-lambda
  (syntax-rules ()
    ((_ method more ...)
     (dispatching-lambda-sub () () method more ...))))

(define-syntax dispatching-lambda-sub
  (syntax-rules (define)
    ((_ (symbol ...) (proc ...))
     (lambda (message . args)
       (case message
         ((symbol) (apply proc args)) ...
         (else (error "unknown message" message)))))
    ((_ (symbol ...) (procs ...) (define (method . args) . body) more ...)
     (dispatching-lambda-sub (symbol ... method)
                             (procs ... (lambda args . body))
                             more ...))
    ))

こんなふうに書けますね:

 (dispatching-lambda
   (define (get) ...)
   (define (next seed) ...)
   (define (update! val) ...)
   (define (delete!) ...)
   (define (insert! key val) ...))

こういうマクロをひとつ標準で持っておくと便利かもしれませんね。

symbolをapplicableにする

('hoge obj) --> (slot-ref obj 'hoge)

欠点

利点

空気を読んでsymbolをlambdaにする

(define-method x->^ ((sym <symbol>)) (cut ~ <> sym) )

(define-method x->^ ((lst <list>))
  (match lst
    [(x)      ($ list $ (x->^ x) $) ]
    [(x . xs) (pack$ cons (x->^ x) (x->^ xs)) ] ) )

(define-method x->^ ((vec <vector>))
  (match vec
    [#(x)      ($ vector $ (x->^ x) $) ]
    [#(xs ... )
      (^y (map-to <vector> (^z ((x->^ z) y) ) xs) ) ] ) )
('hoge obj) ---> ERROR
(map (x->^ 'hoge) objs) ---> (map (cut ~ <> 'hoge) objs)
(map (x->^ 'foo 'bar) objs) ---> (map (cut ~ <> 'foo 'bar) objs)
; x->^ は他の場合にも定義できそうな気がします。
; refではなくslot-refを使いたいときはどうするか?という問題もあります。
; もしくは、<collection>では問答無用に x->^するという選択もありかなとか。

applyよりも柔軟なapplyを定義する

(apply 'hoge obj rest) ---> ERROR
(x-apply 'hoge obj '())  ---> (~ obj 'hoge)
(x-apply 'hoge obj rest) ---> (cons (~ obj 'hoge) (apply x-apply 'hoge rest))

; 実験的(どう書けばいいかよくわかっていないのですけど・・・)
; applyの末尾のリストの扱いをどうするかよくわからないのですが、感じとして:
(x-apply 'foo 'bar obj1 obj2) ---> (list (~ obj1 'foo 'bar) (~ obj2 'foo 'bar) )
(x-apply '(foo bar) obj) ---> (list (~ obj 'foo) (~ obj 'bar) )
(x-apply '(foo . bar) obj) ---> (cons (~ obj 'foo) (~ obj 'bar) )
(x-apply #(foo bar) obj) ---> (vector (~ obj 'foo) (~ obj 'bar) )

symbolとobjectの列が交互にあらわれるシーケンスをつかった参照構文

(define (%ref% refn pred . arg)
  ($ (^x (if (= (length x) 1) (car x) x ) )
  $ map
      (match-lambda
        [( ((? (complement pred) obj) ) ( (? pred syms) ... ) )
          (apply refn obj syms) ]
        [( ((? (complement pred) objs) ...  ) ( (? pred syms) ... ) )
          (map (cut apply refn <> syms) objs) ] )
  $ flip slices 2 $ group-sequence arg :key pred)
)

(define (%flip-ref% refn pred . arg)
  ($ (^x (if (= (length x) 1) (car x) x ) )
  $ map
      (match-lambda
        [( ( (? pred syms) ... ) ((? (complement pred) obj) )  )
          (apply refn obj syms) ]
        [( ( (? pred syms) ... ) ((? (complement pred) objs) ...  )  )
          (map (cut apply refn <> syms) objs) ] )
  $ flip slices 2 $ group-sequence arg :key pred)
)

(define (~~     . arg) (apply %ref% ~ symbol? arg ) )
(define (flip~~ . arg) (apply %flip-ref% ~ symbol? arg ) )
(~~ obj 'x) = (~ obj 'x)
(~~ obj 'x 'y) = (~ obj 'x 'y)

(~~ obj1 obj2 'x 'y  obj3 'u 'v 'w  )
== (list (~~ obj1 obj2 'x 'y ) (~~ obj3 'u 'v 'w ) )
== (list (list (~ obj1 'x 'y) (list obj2 'x 'y) ) (~ obj3 'u 'v 'w) )

参照の分配(~の意味上の反転)

yamasushi(2013/04/29 06:00:10 UTC)slot-refが使いにくいと感じるのは、反対の意味をもつ働きをさせようとするからで、

(bunpai x 'a 'b 'c) == (list (~ x 'a) (~ x 'b) (~ x 'c) )
(values-bunpai x 'a 'b 'c) == (values (~ x 'a) (~ x 'b) (~ x 'c) )

のような演算子があればいいのではないでしょうか? つまり、~の意味を反転させたようなものです。 オブジェクトのほうをセレクタに作用させる、というイメージです。これとmatchやreceiveを組み合わせればいいのではないでしょうか?

(~ symbol1 symbol2 )の解釈

yamasushi(2013/04/28 12:07:31 UTC)selfがない場合について。

symbol1というモジュールのsymbol2という変数を参照する

globalなオブジェクトとしてモジュールシステムそのものがあると考える。

(~ 'gauche 'cond-list)

でgaucheモジュールのcond-listを参照する。
また、

(bunpai 'gauche 'scheme)
 == ( (シンボルから'gaucheモジュール変数をrefできるobj)
      (シンボルから'schemeモジュール変数をrefできるobj) )

と見なせる。

symbol1で指定したタイプのシステム情報にsymbol2で問い合わせる

この場合には、上のケースは

(~ 'module 'gauche 'cond-list) 

のようになる。

(~ 'enviroment 'PATH )

でPATH環境変数を取得する。 また、

(bunpai 'environment 'module)
 == ( (シンボルから環境変数をrefできるobj)
      (シンボルからモジュールをrefできるobj) )

と見なせる。


Tags: OOP, slot

More ...