Code Aquarium

minazoko's blog -*- 水底のブログ -*-

(Racket) member-keyの話

とりあえずクラスの基本

racketではクラスは次のように作成します。

(define sub-class%
  (class super-class%
    (super-new)
    ...))

racketのクラスは単一継承で必ずなんらかのクラスを継承しなければなりません。この例では super-class%クラスを継承した sub-class%クラスを生成しています。
特に継承したいクラスがなければ、object%を継承します。object%クラスは全てのクラスのトップレベルクラスです。
super-newは親クラスのコンストラクタ呼び出しでこれも必須です。
クラス名は % をサフィックスするのが慣例になっています。

class キーワードの後ろが「作りたいクラス名」ではなく「スーパークラス」を書くところが独特ですね。
とりあえずこれだけ覚えておけば、あとはフィーリングでなんとかなるはず。
実例を見てみましょう。

;;クラスを作成
(define greeter%
  (class object%
    (super-new)
    (define/public (greet) 
      "hello")))

(define greeter (new greeter%))  ;; インスタンスを生成
(send greeter greet)  ;=> hello  ;; メソッドコール

ここでは new でインスタンスを作っていますが make-objectでも作れます[*1]。
以上。基本の説明終わり。あとはフィーリングで。

クラスを作る関数

動的な言語なのでクラスは実行時に作られます。
「クラスを作る関数」を作るのも簡単です。

;;クラスを作る関数
(define (make-greeter-class)
  (class object%
    (super-new)
    (define/public (greet) 
      "hello")))

(define greeter% (make-greeter-class))  ;; クラスを生成
(define greeter (new greeter%))
(send greeter greet)  ;=> hello

特に問題はありませんね。

外部名の作成

クラスのメンバーを別名で外部公開することができます。

(define (make-greeter-class)
  (define-member-name greet (member-name-key aisatu))
  (class object%
    (super-new)
    (define/public (greet) 
      "hello")))

(define greeter% (make-greeter-class))
(define greeter (new greeter%))
(send greeter aisatu) ;=> hello
(send greeter greet)  ;=> error

define-member-nameの第一引数で指定したメンバー名がレキシカルスコープの外では第二引数で指定したメンバーキーに置き換えられます。第二引数は単なるシンボルではなく membar-keyというオブジェクトである必要があります。

(member-name-key aisatu)  ;=> #<member-key:aisatu>

別名で外部公開するとスコープ外では本来のメンバー名 greet ではアクセスできなくなります。
一方レキシカルスコープ内では、元の greet と 別名の aisatu のどちらでも参照することができます。

特定クラス間でのみのメンバー共有

generate-member-key というマクロも member-keyを作ります。このマクロは引数を取らず、gensym で作成したランダムなシンボルを持つmember-keyを生成します。
この機能を利用してレキシカルスコープ内で作成したクラス間でのみ参照可能なメンバーを作れます。

(generate-member-key) ; => #<member-key:member813>

このメンバーキーで define-member-name するとヒューマンアンリーダブルな別名で外部公開することになり「それなら外部ではまず参照できないよね」というpythonのようなやりかたで外部非公開(のようなものに)しています。
この実例が前のエントリになります。前エントリでは let でスコープを作っていますが レキシカルスコープができれば let でも lambda でも define でもなんでもよいです。

追記 define-local-member-name

もっと直接的にメンバーをローカル化し外部に隠すフォームがありました。

(define (make-hoge-class)
  (define-local-member-name hoge) ;=> メンバーhogeをローカル化
  (class object% (super-new)
    (define/public (hoge) 'hoge)
    (define/public (fuga) (hoge))))

(define h (new (make-hoge-class)))
(send h fuga)   ;=> hoge
(send h hoge)   ;=> error

*1:newとmake-objectはコンストラクタ引数がある場合に使い分けます。new はキーワードパラメータ、make-objectは位置パラメータで引数に適用します

(Racket) Class の External Name Control

RacketのClassシステムの説明にある Internal / External Names がピンと来なかったので、サンプルを書いてみました。
公式ドキュメントでは 比較対象にJavaのprotectedを例にあげていますが、むしろ C++ の friend に近いような気がします。

Racketのdefine-member-nameの例(gist)

generate-member-key はgensymで内部シンボルを作っています。define-member-name で束縛すると、束縛されたシンボルと同名のメソッドが外部から参照できなくなる仕組みです。

ifマクロ

Gaucheでifマクロを書いてみた。

マクロ名は if だとアレなので %if とする。
当然ながら if case cond といった分岐用特殊形式の使用は禁止。
else無指定への対応すると煩雑になるので2つに分けています。

(define-macro (%__if test then else)
  `((assoc-ref 
     `((#t . ,(^() ,then))
       (#f . ,(^() ,else)))
     (boolean ,test))))

(define-macro (%if test then . else)
  `($ %__if ,test
      ,then
      ,($ %__if (null? else)
          (undefined)
          (car else))))

thenとelseの式をサンクにするので名前付letの再帰は上手くいかないかな、と思いましたが大丈夫でした。

(let loop ((n 10) (acc 1))
  (%if (zero? n)
       acc
     (loop (- n 1) (* acc n))))  ;; ループ呼び出しがサンクになってる。
;;=> 3628800

ちなみにClojureで同様にしてrecurをサンクにすると上手くいきませんでした。recurのジャンプ先がloopではなくサンクを作っている fn になってしまうからですね。