読者です 読者をやめる 読者になる 読者になる

Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

オブジェクト指向について考えたいので、Lispでオブジェクトもどきみたいなものを実装してみる

はじめに

最近になって、「関数型とは何か」みたいな文章が増えている。それらの文章は玉石混交しているし、技術的ではない部分も多いので、自嘲の意味で「ポエム」と自ら呼んでいたりする。その関係もあってか、手元にある『On Lisp』と『実用Common Lisp』にある「Lispによるオブジェクト指向」に関する章を読んだりしていた。そこで、考えたことをメモしておく。

以下、利用している処理系はRacket languageを想定している。

Lispでオブジェクト指向っぽいものを作ってみる

この項目はLispによってオブジェクト指向を作るための道筋みたいなものである。もし、Lispに興味が無ければ、一挙に飛ばしてもいい。

はじまり

よく言われているように、「関数型」と呼ばれる言語の特徴として「副作用」をできるだけ排除する傾向にある、ということはできる。副作用とは、ある変数などにおいて変化を発生させることを指す、と僕は理解している。例えば、Rubyならば、「破壊的操作」というものがある。例としては、下のようなものが挙げられる。

a = “foobar”
a.reverse! 
puts a # => “raboof"

このように、aという変数は、reverse!というメソッドの後で変化する。あるいは、aに再代入することによっても、変化させることが出来る。もちろん、コンピューター自体が、そもそも副作用の塊であるという事情があるため、何らかの形で上手くその辺りと付き合っていかなくてはならないわけで、これらの状態を格納しておくための変数が必要になる。

例えば、簡単な銀行口座について実装することを考えてみよう。この銀行口座は、口座内にある金額を引き出したり、あるいは預金したりすることができることにする。そこで、銀行口座が一つの場合には、一つの変数に対する操作を定義してあげればいいことになる。

(define balance 100)
(define (check-amount amount func)
  (if (< amount 0)
      (display "Invalid amount")
      (func amount)))
(define (withdraw amount)
  (check-amount amount withdraw!))

(define (withdraw! amount)
  (let ([after-withdraw (- balance amount)])
    (if (>= after-withdraw 0)
        (set! balance after-withdraw)
        (display "Insufficient funds"))))

(define (deposit amount)
  (check-amount amount deposit!))

(define (deposit! amount)
  (set! balance (+ balance amount)))

確かに、一つの変数に対しての場合、この状態でよいかもしれない。しかし、例えば複数の口座が必要になる場合はどうだろうか? この関数は、balanceという一つの変数に対して、余りにも密接に結びつき過ぎているといってもいいと思う。なので、もしかしたら複数の口座毎に、それを操作する関数を定義するのもありかもしれないが、それは余りにも柔軟性に欠ける。そこで、他の変数を使えるようにという意図で、例えばget-balanceとかset-balance!という変数を定義して、取得する変数並びに、セットする変数への振り分けをその都度変えるという設計にしてもいいだろう。確かに、こちらのほうがある程度、関数自体と変数自体は切り離されている、とはいえ、これはこれで筋が悪いようにも思える。

(define balance-1 100)
(define balance-2 100)

(define (get-balance name)
  (cond [(symbol=? name 'esehara) balance-1]
        [(symbol=? name 'robot) balance-2]))

(define (set-balance! name amount)
  (cond [(symbol=? name 'esehara) (set! balance-1 amount)]
        [(symbol=? name 'robot) (set! balance-2 amount)]))

(define (check-amount name amount func)
  (if (< amount 0)
      (display "Invalid amount")
      (func name amount)))

(define (withdraw name amount)
  (check-amount name amount withdraw!))

(define (withdraw! name amount)
  (let ([after-withdraw (- (get-balance name) amount)])
    (if (>= after-withdraw 0)
        (set-balance! name after-withdraw)
        (display "Insufficient funds"))))

(define (deposit name amount)
  (check-amount name amount deposit!))

(define (deposit! name amount)
  (set-balance! name (+ (get-balance name) amount)))

このような変更の問題として、「どれだけの変数が必要なのかわからない場合に対応しようがない」という問題がある。ある変数とその関数の結びつきは、それがそれ自体のみに使われることが保証されている点においてのみ、あるべきだ。上の場合の問題は、必要な分だけ、必要な分岐を用意してあげなければいけない、という欠点が存在している。また、もう一つの欠点として、例えば上のような方法は、例えばset-balance!の直接利用を隠蔽し尽くすことができないという問題もある。

このような二つの問題を解決するために、オブジェクト指向……というよりも、「オブジェクトもどき」を実装することを考えてみる。ここで「オブジェクトもどき」と呼んでいるのは、オブジェクト指向には「継承」という考え方もあるのだけれど、今回の実装ではそのあたりは考慮しないためだ。ポイントはある構造体に対して、それらの要素に対して操作体系を与え、その操作体系から、そ のオブジェクトを利用するということである。

ハッシュを利用してオブジェクトもどきを作ってみる

方針としては二つ考えられる。一つには、ハッシュを使うことによって、データ構造を定義すること。この方針は『On Lisp』で採用されている方針である。これをRacket languageで書き直すと、下のようなものとして定義することが可能になる(話を簡単にするために、以下の例では、とりあえず引数チェックは抜かしている):

((define (balance-hash-obj name amount)
  (make-hash (list (list 'name name)
                   (list 'amount amount)
                   (list 'withdraw
                         (lambda (obj amount)
                           (let ([next-amount (- (car (hash-ref obj 'amount)) amount)])
                             (if (< next-amount 0) "Insufficient funds"
                                 (hash-set! obj 'amount (list next-amount))))))
                   (list 'deposit
                    (lambda (obj amount)
                      (hash-set! obj 'amount (list (+ (hash-ref obj 'amount) amount))))))))

ここでhashに対し、無名関数をセットしてあげることによって、関数を定義している。しかし、このオブジェクトの場合、利用が余りにも鬱陶しい。

((car (hash-ref balance-hash-1 'withdraw)) balance-hash-1 100)

問題点は二つある。まず一つに、上記のmake-hashにセットされている関数は、別段そのオブジェクト特有のものではないということ。だから

((car (hash-ref balance-hash-1 'withdraw)) balance-hash-2 100)

とやったとしても、依然として問題は無い(balance-hash-1用のメソッドがbalance-hash-2にも適用できる!)。もう一つは、わざわざ該当する関数をhash-refおよびcarで取得しなければいけない、という欠点が存在している。なので、もう少し簡単にこれらを引き出せるような関数を定義してやるべきだろう。例えばRubyなんかは、sendというメソッドで、メタ的に任意のメソッドを呼び出すことが可能なわけだから、似たようなものを定義してみる:

(define (send obj method . params)
  (let ([attr (car (hash-ref obj method))])
  (if (procedure? attr) (apply attr obj params) attr)))

こうすることによって、下のようにメソッドが実行できることが確認できる。

(send balance-hash-1 'withdraw 100)
(send balance-hash-1 'amount)

これで、普段知っているオブジェクト指向に近いのがわかる。もちろん、これがlispの語順的には不自然ではあるのだが(それは逆説的に、他の言語から見れば自然にも見える)、とはいえ、これ自体は問題がないわけではなく、中身はいわゆるhashでしかないので、他から簡単に操作ができる。これは別の観点からすれば、モンキーパッチしやすいということになる。例えば下のような方法を考えることができるだろう:

(hash-set! balance-hash-1 'amount '(1000))

これについては補足が必要で、RacketというLisp処理系は、Hashの性質として、変更可能か、それとも変更不可能かという情報を内部に持っている。例えば、make-hashで造られるhashは、hash-set!で変更が可能になる、といったように。また、上記の方法は、list化しているという点で、少々余計な感じも否めない。そこで、今度はクロージャとして、引数を閉じ込めてあげて、その引数を操作することを考えてみよう(引数も、束縛されている変数と捉えることは可能だ):

(define (balance-closure-obj name money)
  (lambda (method)
    (let ([get-money (lambda () money)]
          [get-name(lambda () name)])
    (case method
      [(money) get-money]
      [(name) get-name]
      [(withdraw) (lambda (amount)
                    (let ([next-money (- (get-money) amount)])
                    (if (< next-money 0)
                        "Insufficient funds"
                        (set! money (- money amount)))))]
      [(deposit) (lambda (amount) (set! money (+ money amount)))]))))

そうすると、下のように利用することができる:

((balance-cls-1 'withdraw) 100)
((balance-cls-1 'money))

この方法は、hashで作るよりも扱いやすいという性質があるが、やはり上みたいにsend関数があったほうが、余計な括弧が少なくて済むだろう:

(define (send-cls obj method . params)
  (apply (obj method) params))

とはいえ、Lispであるならば、(withdraw obj)といったように使いたい筈だ。これを実現するのにも二つの方針があって、それこそマクロを書くか、それともそれ用の関数を定義するかということである。ここでは、後者のほうが実装としては簡単なので(前者はスリリングで面白い道だとは認めるのだけれども……)、そちらのほうで実装してみよう:

(define (withdraw obj . params)
  (apply (obj 'withdraw) params))

この関数の特徴として、同じようなメソッドが定義されている場合にも、同じように実行が可能であることである。例えばPythonやRubyであるならば、「ダックタイピング」という方法があると思われるけれども、そのような役割をすることができる。

本当は、「継承」の実装などがあるのだけれど、今回はそのあたりは飛ばそうと思う。

暫定的な結論

どちらかというと、Lispは関数型と呼ばれることもあるけれど、同様にオブジェクト指向的な側面も吸収できてしまう言語というのは間違いない。もちろん、処理系とその仕様にもよるわけだが、今回利用したRacketでもClassベースのプログラムは可能であるし、Common Lispは、その仕様上にCLOSが存在している。確かに『On Lisp』では「オブジェクト指向言語の必要性が一番低い言語」ということが書いてある。

とはいえ、場合によってはあったほうが便利な時もあることは間違いがない。便利な時というのは、上記のように、異なる状態が同じ構造を持っていて、それに対してなんらかの操作を保証したい場合がそうだろうとも言える。オブジェクトシステムはあったほうが便利な時はあるんだと思うけれど、それに類するシステムは、たとえそれが「オブジェクト指向」を名乗っていなくても、存在しているだろうと思う。むしろ、そこにおいて「これでもやっていける」という筋道を立てるのがベターなのであって、実装目的のためにどのようなスタイルを選び取るかという問題に行き着くだろうというのが、穏便な意見だろうと思う。

議論が混合しやすいのは、いわゆるオブジェクト指向的な「操作の保証、オブジェクトとしてのまとまり」という側面と、いわゆる「手続き型言語」と呼ばれるような言語に特徴的な「再代入が容易である」という側面が混合されているのだろうと思われる。そもそも、オブジェクト指向においても、安易な再代入は推奨されていないように感じるし、そのあたりについては「出来るだけ副作用が発生しないようにしておいたほうがいいよね」ということになるのだと思う。

参考文献

計算機プログラムの構造と解釈[第2版]

計算機プログラムの構造と解釈[第2版]

  • 作者: ハロルドエイブルソン,ジュリーサスマン,ジェラルド・ジェイサスマン,Harold Abelson,Julie Sussman,Gerald Jay Sussman,和田英一
  • 出版社/メーカー: 翔泳社
  • 発売日: 2014/05/17
  • メディア: 大型本
  • この商品を含むブログ (1件) を見る

第三章において、クロージャを利用した変数の閉じこめ方法が載っている。こういう風な閉じこめ方を局所状態変数と呼んでいる。ちなみに、このようにある関数のスコープに閉じ込めておいて、擬似的なプライベート環境を提供する方法は、例えばJavaScriptのプラグインを作成する場合において、利用されている印象がある。無料に公開されている版もあるので、気になる人がいれば参考にどうぞ

On Lisp

On Lisp

マクロを利用したオブジェクト指向を組み立てる方法の案内がある。

実用 Common Lisp (IT Architects’Archive CLASSIC MODER)

実用 Common Lisp (IT Architects’Archive CLASSIC MODER)

解説としては一番平坦な本だとは思う。CLOSを使いながら、オブジェクト指向が適した例として、検索ツールの実装例が掲載されている。

あわせて読みたい