個人的にはオブジェクト指向は好きじゃない。
特に、Lisp系言語だと、括弧塗れになるわ形式は逐次処理前提になるわ、とコーディングスタイルが大幅に関数型言語の「それ」と食い違う。
しかし一般に、CLIならともかくとしてGUIが絡むとオブジェクト指向と付き合わないとならない。と言うのも、そのテのライブラリがオブジェクト指向前提、って事になるからだ。
Racketも例に漏れない。
GUIのガワだけはmred-designerでどーにでも作れるが、WYSIWYGで作られたコードを利用するにせよ、Racketのオブジェクト指向の基本的なトコロだけは分かってないとならない。
ここではRacketのオブジェクト指向を軽く解説していこう。
RacketのOOP(オブジェクト指向プログラミング)は大まかにはフツーに巷に良くあるOOPのスタイルになっていて、ANSI Common LispのCLOSとは違い、ユーザー定義型であるデータ(クラス)にメソッドが所属している。
ただし、それでもいくつか特徴がある。
例えば、今、Widget%と言うクラスがあるとして、それを利用したwと名付けたデータを生成するには次のように記述する。
(define w (new Widget%))
まず、Widget%と言うクラス名の末尾の%が何なのか、って話があるが、これは文法的には意味がない。単なる命名規約で、Racketではクラス定義をする際、名前の末尾に%を付けましょうよ、と言う事になっている。結果、Racketで提供されているビルトイン・クラス名は全部末尾が%で終わっている。
この例はクラスがインスタンス化(つまり、wと言う変数にクラスが生成したデータを代入)する際には引数が必要ない例になっている。クラスからデータを生成する際に、newと言うマクロを使う。これはPythonなんかのOOPで見ると異質だが、反面、JavaなんかのOOPではお馴染みだ。Javaでもクラスからデータを生成(インスタンス化)する際にはnewと言うキーワードを用いる。
いずれにせよ、この書式で変数wにはWidget%クラス、と言う雛形により生成されたデータが束縛される事になる。
次に、仮にWidget%クラスにpaintと言うメソッドがあるとしよう。インスタンスwからメソッドpaintを起動するには次のように書く。
(send w paint)
ここがRacketのOOPの大きな特徴で、RacketのOOPではSmalltalk宜しく、「メッセージ送信」が基本となっている。意味はそのまま「インスタンスwにpaintと言うメッセージを送れ」だ。
これはPythonのような通常のOOPのシステムだと、
w.paint()
なぞと書く局面だが、RacketはあくまでSmalltalk方式だ、と言う事だ。
次にRacketのクラス定義だ。Racketでは通常のOOPらしい特徴とRacket独特のブツが混在している。
まずは簡単なクラス定義をしてみよう。
(define Message%
(class object%
(init aString)
(field (text aString))
(super-new)
(define/public (printIt)
(println text))))
まずは大枠から。
通常の、ANSI Common LispのCLOSを含むOOP言語ではクラスを定義した時点でクラス名も同時に定義されるが、Racketではそうじゃない。「クラス名を付ける」のと「クラスオブジェクトと言う雛形を作る」事は分離されている。
言っちゃえばラムダ式を使った関数定義に近い。
(define foo (lambda (...) ...))
また、これが示唆する事は、同じ形式のクラスを別名でいくらでも作れる、と言う事だ。これは何を意味してるのか、と言うと、理論的にはRacketは単一継承のクラスを簡単に作れ、それを基礎としてる、と言う事だ。この辺はC++やPython、及びCLOSの「多重継承を基礎とする」OOPと大幅に違うトコだ。
実際、上の例でも、class(及びMessage%クラス)はobject%クラスを継承している。Racketのクラスのルートはこのobject%クラスであり、どんなクラスもルートクラスとしてのこのobject%クラスの子孫になっている。それがRacketでは明示されている、って事だ。
他には
- initマクロで、クラスをインスタンス化する際に与える引数を指定する。
- fieldマクロで、いわゆるインスタンス変数/メンバ変数の定義をする。通常、initマクロで指定された変数をここに取り込む(上の例だと、クラスのインスタンス化でaStringに与えられた引数をインスタンス変数textに束縛している)。
- super-newマクロで親クラス(上の例ではobject%クラス)を初期化する。
- define/publicマクロで「公開」メソッドを定義する。フツーにここでdefineを使うと、このクラス内では構わないが、一切外部からそのメソッドにはアクセス出来なくなる。
例えば上のクラス定義はPythonだとほぼ次のような意味になる。
class Message(object):
def __init__(self, aString):
self.text = aString
super().__init__()
def printIt(self):
print(self.text)
悔しいけど、さすがPythonの方がシンプルに見えるよな(笑)。
ただし、そのシンプルさにも欠点がある。Pythonのクラスのメソッドは必ず「公開される」と言う単純さがあるから、だ。
Javaなんかを知ってる人だとRacketのdefine/publicはpublic修飾子を付けたメソッドと同様な効果をもたらす、と言う事が予想出来るだろう。逆にdefineでメソッドを作るとprivate修飾子を付けたメソッドと同様な効果がある、と。
RacketもJavaと同様に、プログラマ側がメソッドを公開するか隠蔽するか、を選べる。言い換えるとそういう機能がPythonにはないんだ。
なお、実行例は次のようなカンジだ。
なお、initで指定した引数名、aStringは、事実上、クラス内に外部から実引数を取り込む関数として自動生成される。つまり、aStringは関数になってるんだ。
次もRacketのOOPの簡単な説明だ。
(define Square%
(class object%
(init side)
(field (this-side side))
(super-new)
(define/public (calculateArea)
(expt this-side 2))))
(define Circle%
(class object%
(init radius)
(field (this-radius radius))
(super-new)
(define/public (calculateArea)
(* pi (expt this-radius 2)))))
ここでは、正方形と円の面積計算を行う二つのクラスを作っている。
なお、this-と言う接頭辞には特に意味はない。initで指定したside及びradiusと言う名前に対して、fieldで同名のインスタンス変数名を使えないから、だ。
新しいフィールド名を思いつかない場合、Java風にthis-やPython風にself-と言う接頭辞を利用しても良いだろう、って事だ。
また、ここでのポイントは両クラスでcalculateAreaと言う同名メソッドを定義したにせよ、名前の衝突は起こらない、と言ったOOPの特徴だ。
メッセージ送信により、同名メソッドでも、送信対象のインスタンスに含まれるメソッドが自然と適用される。
次はOOPの華形、継承に付いてザックリと説明しよう。
とは言っても、Racketのクラス作成に於いては、上に書いた通り「単一継承」を必ず要するようになっている。
従って、RacketのOOPでは自然と継承を含んでるんだけど、要は「ここは書き換えたい」と言う部分を指定する、と言う形式になっている。
次のようなコードを書いてその辺をちと見ていってみよう。
(define BalanceError "現在の口座残高は~aしかありません。")
(define BankAccount%
(class object%
(init initialAmount)
(field (balance initialAmount))
(printf "口座を開設しました。口座残高は~aです。~%" balance)
(super-new)
(define/public (deposit amount)
(set! balance (+ balance amount)))
(define/public (withdraw amount)
(if (>= balance amount)
(set! balance (- balance amount))
(error 'BalanceError BalanceError balance)))
(define/public (getBalance)
balance)
(define/public (transfer amount account)
(with-handlers ((exn:fail?
(lambda (exn)
(printf BalanceError balance)
(newline))))
(withdraw amount)
(send account deposit amount)))))
ここでは、「OOPの説明」で良くある「銀行口座のエミュレーション」を行っている。
銀行口座は一般に、
- 入金(deposit)
- 出金(withdraw)
- 残高照会(getBalance)
- 振り込み(transfer)
が行われるので、この4つをメソッドとして備えてるわけだ。
それはともかく、コーディングスタイルとしてはちとキッツいよな(笑)。ホンマ括弧だらけ、ってなカンジ。関数型プログラミングに比べると「流れ」を把握するのがツラい。
結果、視認性を高める為にはメソッド定義では改行を適宜入れた方がいいと思うが、Lisp的なコーディングスタイルとしては結果イマイチな気もしないでもない。
また、set!を多用した破壊的変更が中心になる辺りもOOPの特徴で、やっぱ根本的には関数型プログラミングとは相容れない。
さて、では継承を考えよう。上のBankAccount%クラスを利用して、入金する度に3%の利息がつくInterestAccount%クラスを作ってみる。
繰り返すが、Racketでは単一継承が基礎なんで、基底クラスであるobject%を直接継承しようが、はたまた自作クラスを継承しようが、その辺の「文法」は全く変わらない。
(define InterestAccount%
(class BankAccount%
(super-new)
(inherit-field balance)
(define/override (deposit amount)
(super deposit amount)
(set! balance (* balance 1.03)))))
上のInterestAccount%クラスの場合、classの第一引数を、object%クラスの代わりに特定のクラスにすればそのままそのクラスを単一継承する。この例だと当然BankAccount%クラスを指定する。
- super-newにより継承元の親クラス、この場合はBankAccount%が持ってる全てを初期化する。
- 親クラスが持ってるフィールド(メンバ変数/インスタンス変数)を利用して新しくメソッドを定義したり、あるいはオーバーライドしたい場合、そのフィールド名はそのままでは「隠蔽」され(つまり子クラスであるInterestAccount%から親クラスのBankAccount%クラスのフィールドへはそのままだとアクセス出来ない)ているので、マクロinherit-fieldでそれをこじ開ける。Javaに明るい人は、ここで後付的にprotected修飾子を付け加える、と捉えて良いだろう。
- define/overrideで親クラスから継承したメソッドの機能を書き換える。ここで親クラス(BankAccount%)が持ってたdepositメソッドの機能「だけ」を拡張、あるいは書き換えている。そういう作業をメソッドのオーバーライドと呼ぶ。
- superは特殊なsendだと捉えていいだろう。親クラス(この場合ではBankAccount%)へsuperを使ってメッセージを送信して、結果、親クラスのbalanceの値を設定し、それを2.により子クラス(つまりInterestAccount%)へ戻してる(と言うより2.のinherit-fieldで親クラスのbalanceフィールドがこのクラスのbalanceと完全に同一のモノである事が保証されている)。それを使って題意を満たすように計算するわけだ。
ちょっと2、3、4のプロセスが煩雑には見えるだろう。
これはPythonで言うと次のようなカンジだ。
class InterestAccount(BankAccount):
# 通常、Pythonではここの__init__は省略出来る
def __init__(self, initialAmount):
super().__init__(initialAmount)
def deposit(self, amount):
super().deposit(amount)
self.balance = self.balance * 1.03
やはり手続き型形式に則ってるPythonの方が、OOPに於いてはシンプルに見えるのは事実だ。
いずれにせよ、Racketでもこうやって親クラスとやり取りしつつ目的のブツを得ることが出来る、と言う事だ。
もう一つ簡単な継承の例を見てみよう。今度は現金を引き出す度に3ドルの手数料が差し引かれるChargintAccount%クラスをBankAccount%クラスを継承して定義してみる。
(define ChargingAccount%
(class BankAccount%
(super-new)
(init-field (fee 3))
(define/override (withdraw amount)
(super withdraw (+ amount fee)))))
新しく出てきたマクロはinit-fieldだ。これはフィールド(インスタンス変数/メンバ変数)を定義したと同時に初期値を設定するマクロだ。フィールド名と初期値のコンビは上のようにリストとして与える。
次のようなコードを書いてテストしてみよう。
;; 標準のBankAccountのテスト
(define a (new BankAccount% (initialAmount 500)))
(define b (new BankAccount% (initialAmount 200)))
(send a withdraw 100)
;; (send a withdraw 1000)
(send a transfer 100 b)
(printf " A=~a~%" (send a getBalance)) ; 300になる
(printf " B=~a~%" (send b getBalance)) ; 300になる
;; InterestAccount のテスト
(define c (new InterestAccount% (initialAmount 1000)))
(send c deposit 100)
(printf " C=~a~%" (send c getBalance)) ; 1133になる
;; ChargingAccountのテスト
(define d (new ChargingAccount% (initialAmount 300)))
(send d deposit 200)
(printf " D=~a~%" (send d getBalance)) ; 500になる
(send d withdraw 50)
(printf " D=~a~%" (send d getBalance)) ; 447になる
(send d transfer 100 a)
(printf " A=~a~%" (send a getBalance)) ; 400になる
(printf " D=~a~%" (send d getBalance)) ; 344になる
;; 最後に、ChargingAccountからInterestAccountに振り込みする
;; ChagingAccountでは振り込み手数料が発生し、
;; InterestAccountでは利息が発生する
(printf " C=~a~%" (send c getBalance)) ; 1133である
(printf " D=~a~%" (send d getBalance)) ; 344である
(send d transfer 20 c)
(printf " C=~a~%" (send c getBalance)) ; 1187.59である
(printf " D=~a~%" (send d getBalance)) ; 321になる
キチンと動作するのが分かる。
これでRacketの基本的なOOPの紹介は終了する。
なお、繰り返すがRacketのOOPは単一継承が基本で、複数の親クラスを持った多重継承はサポートしていない。
ただし、それらはここでは扱わない。GUIに備えるにはここまで書いた知識程度で十分だ、と考えるからだ(それに多重継承を要するOOPなんて書きたくない・笑)。
これらを知りたい人は公式ドキュメントを参照して欲しい。
以上。
なお、今回のコードはここにまとめてある。