2017年07月29日

オブジェクト指向?

これら一連の記事は、numb-lambda インタープリタから始まる連載企画になっています。

今回は、オブジェクト指向を取り扱います。

最初にお断りしておきます。(R5RSレベルでは*1) Scheme にはクラスのようなデータ構造はありません。さらに構造体がありません。Scheme 標準でリストの他にある複合データ型は、ベクタ(=配列; array)くらいです。文字列は、ありますが、それを複合データ型と呼べるかは疑問です。

それでも、Scheme を使ってオブジェクト指向のプログラミングは可能です。我々には強力なλ(lambda)があります。

オブジェクト指向の3つの要件を掲げておきましょう。

  • データ隠蔽

  • カプセル化

  • ポリモーフィズム


Scheme では、この要件を満足させることができるのです。ただし、データ隠蔽のレベルをコントロールする public, protected, private のような機能はありません。

それでは、オブジェクト指向のプログラミングをしてみましょう。ですが、その前に......

データ型および比較
この連載では、主に整数とシンボルを扱って来ました。hello, world のところで、少しだけ文字列も扱いました。残りは実数(浮動小数点数) *2 と文字があります。

それらのデータを比較するには、述語を組み合せます。

シンボルの比較 eq?
数の比較 =, <, <=, >=, >
文字の比較 char=?, char<?, char<=?, char>=?, char>?
文字列の比較 string=?, string<?, string<=?, string>=?, string>?

この他、R5RS には、eqv?equal? が規定されていますが numb-lambda では実装していません。

null? ;空リストかどうか
number?
symbol?
char?
string?
pair?

なぜか、numb-lambda では、vector? は実装していません。

それと、論理演算ができます。
not
and
or
このうち、not だけが関数で、and と or はスペシャルフォームです。and は複数の引数を取りますが、それらを左から順に評価し、#f になった時点で値を決定し残りは評価しません。同様に or も複数の引数を取り、それらを左から順に評価し、#t になった時点で値を決定し残りの評価はしません。

今回は、オブジェクトに与えるメッセージとしてシンボルを使います。ですから、eq?が使えれば十分です。他の述語については、また、利用する都度、説明します。

ここでは、詳しく説明しませんが、シンボルと文字列は異なるということだけは強く意識してください。そして、eq? は高速という点も。
> (eq? 'symbol 'symbol)
#t
> (define sym-a 'symbol)
> (define sym-b 'symbol)
> (eq? sym-a sym-b)
#t
> (eq? "string" "string")
#f
> (define str-a "string")
> (define str-b "string")
> (eq? str-a str-b)
#f
> (eq? str-a str-a)
#t
> (eq? '(a b) '(a b))
#f
> (define lst '(a b))
> lst
(a b)
> (eq? lst lst)
#t
> 
eq? は、アドレスの比較だと考えれば良いでしょう。ただし、(eq? '() '())のように、空リストの比較は #t にならなくてはならないのですが numb-lambda の実装にバグがあり、ABORT してしまいます。*3

オブジェクト指向
前置きが長くなってしまいました。オブジェクト指向のプログラミングを始めましょう。

基底クラス
以下に基底クラス material を示します。
(define material
  (lambda (super weight)
    (lambda (message)
      (cond
       ((eq? message 'hold)
(lambda ()
  (display "implement 'hold.")
  (newline)))
       ((eq? message 'drop)
(lambda ()
  (display "implement 'drop.")
  (newline)))
       ((eq? message 'eat)
(lambda ()
  (display "cannot eat.")
  (newline)))
       ((eq? message 'what-is-the-weight?)
(lambda ()
  (display weight)
  (display " kg.")
  (newline)
  weight))
       (else (if (null? super)
 (lambda () '())
 (super message)))))))
ここで、lambda が入れ子になっていますが、material が呼ばれると、super, weight引数にとる lambda が動作します。この lambda は、message を引数に取る lambda 式を戻り値として返します。ここで、戻り値となった内側の lambda 式は、まだ動作しません

ここで、プロパティ と呼ばれるものが、weight です。message はオブジェクトが解釈できるメッセージとなっています。ここでは、'hold, 'drop, 'eat, 'what-is-the-weight? が解釈可能です。

cond 式の話をしていませんでした。cond 式の一般形を次に示します。
(cond
  (<式11> <式12> <式13>...)
  (<式21> <式22> <式23>...)
  (<式31> <式32> <式33>...)
  ...
  (else <式o1> <式o2> <式o3>...))
ここで、cond内の各行の最初の式、<式m1> が評価され、#t が返されると <式m2>, <式m3>, <式m4>... が順に評価され最後の式が cond 式の値となります。<式m1> の評価値として #fが帰った場合、次の行がテストされます。どの式も適合しないときは、else のある行が逐次実行され、最後の式が返ります。
以上で、行と呼んでいるのは、ここで示した一般形にのみ通用します。通常は、一般形の各行が複数行に分けて書かれていることがあります。あくまで、ここで行と言っている単位は、括弧で見分けてください。

cond スペシャルフォームを説明しました。コードは読めるようになったでしょうか。上の関数を評価したらインスタンスを作ってみましょう。
> (define foo (material '() 0))
> ((foo 'eat))
cannot eat.
> ((foo 'what-is-the-weight?))
0 kg.
0
> ((foo 'drop))
implement 'drop.
> ((foo 'hold))
implement 'hold.
> ((foo 'unknown))
'()
> (define bar (material '() 1000000))
> ((bar 'what-is-the-weight?))
1000000 kg.
1000000
> 
ここでは、foo, bar という2つのインスタンスを作ってメッセージを送っています。括弧が二重になっている意味は解るでしょうか。例えば(foo 'eat)lambda 式を返します。それを呼び出すには、括弧の最初の式とする必要があります。ここでは引数はありません。また、'what-is-the-weight? を送ったとき、とくに注意してください。これは、副作用ばかりでなく値も返しています。俗語で言うところの weight プロパティに対する getter ですね。

今度は、この material から、food を派生させます。
(define food
  (lambda (weight)
    (let ((super (material '() weight)))
      (lambda (message)
(cond
 ((eq? message 'hold)
  (lambda ()
    (display "Okay, you can hold.")
    (newline)))
 ((eq? message 'drop)
  (lambda ()
    (display "Oh! my God.")
    (newline)))
 ((eq? message 'eat)
  (lambda ()
    (display "Tasty?");
    (newline)))
 (else (super message)))))))
ここで、基底クラスは外側の lambda の内側にある letでインスタンスが作られます。つまり、food が呼ばれる都度、foodmaterialのインスタンスが作られます。インスタンスを生成してみましょう。
> (define baz (food 1.618))
> ((baz 'eat))
Tasty?
> ((baz 'drop))
Oh! my God.
> ((baz 'hold))
Okay, you can hold.
> ((baz 'what-is-the-weight?))
1.618 kg.
1.618
> 
各メソッドがオーバーライドされているのが解ると思います。そして、注目すべくは、やはり、'what-is-the-weight? メッセージを送ったときで、このハンドラは food 内にありません。これは、派生元(スーパークラス?)である material が処理しています。

それでは、もう一段、派生させてみましょう。
(define egg
  (lambda (weight color)
    (let ((super (food weight))
  (color color))
      (lambda (message)
(cond
 ((eq? message 'hold)
  (lambda ()
    (display "Don't hold on tight.")
    (newline)))
 ((eq? message 'drop)
  (lambda ()
    ((super 'drop))
    (display "It has broken.")
    (newline)))
 ((eq? message 'eat)
  (lambda ()
    (display "Delicious.")
    (newline)))
 ((eq? message 'what-is-the-color?)
  (lambda ()
    (display color)
    (newline)
    color))
 (else (super message)))))))
food から、派生しています。(let 式)
color というプロパティをひとつ追加しました。また、対応するメッセージ 'what-is-the-color? を追加しました。インスタンスを生成してみましょう。
> (define qux (egg 2.718 "white"))
> ((qux 'drop))
Oh! my God.
It has broken.
> ((qux 'what-is-the-weight?))
2.718 kg.
2.718
> ((qux 'what-is-the-color?))
white
"white"
> 
(とんでもない重さの玉子ですが)色を塗り変えるには、メッセージを追加しなくていはなりません。
cond 式に次の一節を追加してみてください。
 ((eq? message 'change-the-color!)
  (lambda (new-color)
    (let ((old-color color))
      (set! color new-color)
      old-color)))
egg 関数を評価し直したら、早速インスタンスを作ってメッセージを送ってみます。
> (define quux (egg 1.602 "light-gray"))
> ((quux 'change-the-color!) "black")
"light-gray"
> ((quux 'what-is-the-color?))
black
"black"
> 
'change-the-color! メッセージは、俗に言う setter です。しかし、戻り値は、元の値にしています。

ここで、オブジェクトの実装には閉包(クロージャ; closure )を利用しています。これは以前にも一度紹介していますが、どこだったか覚えていますか。近代的な言語は、ほとんど閉包を利用可能となっています。この手法を使ってオブジェクトを実装しているのを初めて確認したのは或る書籍です。*4

今回は以上とします。


*1 簡単に紹介しますと Scheme 言語の規格書です。インターネットで公開されています。

因みにR6RSも存在するようです。


*2 numb-lambda では、実数(浮動小数点数)は最低限、ふつうの人が電卓で利用する程度にしか実装していません。おそらく数学的には情報不足だと思います。

*3 近日、修正します。

*4 ジェラルド・ジェイ・サスマン、ハロルド・エイブルソン、ジュリー・サスマン共著 和田英一訳「計算機プログラムの構造と解釈 第二版」 翔泳社 ISBN-10: 4798135984, ISBN-13: 978-4798135984
私の所有しているものはピアソン・エデュケーションのもので絶版となっていますが、和田先生のツイートによると内容は全く同じだそうです。