2011年6月19日日曜日

Ruby, JavaScript, Python で既存のクラスを拡張 - オープンクラス(モンキーパッチ) と Scala の暗黙の型変換

1. 「リストの要素を取得する」関数を例として、クラスの拡張を考える

Lisp でリストの先頭要素を得る関数は car 。先頭以外の残りの要素を取得するには cdr 。

(car '(1 2 3 4 5))   => 1
(cdr '(1 2 3 4 5))   => (2 3 4 5)

( cf. CAR and CDR – Wikipedia )

Haskell で上記に相当する関数は、

  • head
  • tail

類似した対照的な関数に

  • init
  • last

がある。

main = do let xs = [1..5]
          print $ head xs   -- 1
          print $ tail xs   -- [2,3,4,5]
          print $ init xs   -- [1,2,3,4]
          print $ last xs   -- 5

これらの関数名を、他の言語でも使えるように、拡張の方法をそれぞれ見ていく。

 

2. Ruby で組み込みのクラス Array を拡張

Ruby で同様の関数名を探すと、Array クラスに要素一つを取得する関数

  • first
  • last

が見つかる。

first -> object | nil

配列の先頭の要素を返します。要素がなければ nil を返します。

first(n) -> Array

先頭の n 要素を配列で返します。n は 0 以上でなければなりません。

[PARAM] n:
取得したい要素の個数を整数で指定します。

複数の要素を取得するには、引数に要素数を与える関数も定義されている。これは last も同じ。

ところで、Haskell と同じ関数名で呼び出したい場合、Ruby には既存のクラスを拡張する方法が用意されている。

まつもと直伝 プログラミングのオキテ 第21回 オープンクラスとRuby on Rails」によると、

Rubyでは既存のクラスに定義を追加できます。…

後から機能を追加できるクラスのことを「オープンクラス」と呼びます。

既存の Array クラスを拡張するには、同クラスを定義する要領で行えばよい。

class Array
  alias :head :first

  def tail
    last(length-1)
  end

  def init
    first(length-1)
  end
end

xs = (1..5).to_a

p xs.head   #=> 1
p xs.tail   #=> [2, 3, 4, 5]
p xs.init   #=> [1, 2, 3, 4]
p xs.last   #=> 5

他の言語から考えると、既存のクラスを変更できることは過激な仕様に見える。なぜなら、既に存在するクラスは、ある種グローバルで固定的な定数のようなイメージを持っていたため。

よって、オープンクラスを利用したら、変更を把握しておくためのコストが発生する。

デメリットは,オープンクラスには悪影響を及ぼす可能性があることです。オープンクラスはクラスライブラリの状態を大きく変更してしまうので,プログラム全体に影響を与えてしまいます。 …

複数のライブラリがオープンクラスを使って既存のクラスを変更した場合,お互いに矛盾があると回避する方法が存在しません。…

オープンクラスを利用したライブラリを使うときには,ライブラリ同士がお互いに矛盾しないかどうかをよく吟味する必要があります。…

(同上より)

オープンクラスの別名は モンキーパッチ – Wikipedia

オリジナルのソースコードを変更することなく、実行時に動的言語(例えばSmalltalk, JavaScript, Objective-C, Ruby, Perl, Python, Groovy, など)のコードを拡張したり、変更したりする方法である。…

当初はモンキーパッチは、ルールを無視して実行時にこっそりとコードを変更することから、ゲリラパッチと呼ばれていた。これらのパッチを複数当てると、時折直感に反するような相互作用が生まれることがあり、Zope 2では、交戦中のパッチと呼ばれていた。

モンキーの語源はおもしろい。 ^^;

ゲリラゴリラ同音異字に近かったため、何人かの人がゲリラパッチの代わりにゴリラパッチという間違った用語を使用し始めた。交戦することのないゲリラパッチ開発者が作成するのが非常に難しかったため、より弱そうに聞こえるモンキーパッチという用語が作られることになった。[2]

 

3. JavaScript で既存の Array オブジェクトのプロトタイプを拡張

上記のような Ruby の仕組みは、プロトタイプベース的な性質と考えておけばいいのかな?

プロトタイプベースの言語と言えば JavaScript 。

Array オブジェクトのプロトタイプに head, tail, init, last を定義するなら、

Array.prototype.head = function(){ return this[0]; };
Array.prototype.tail = function(){ return this.slice(1); };
Array.prototype.init = function(){ return this.slice(0,this.length-1); };
Array.prototype.last = function(){ return this[this.length-1]; };

var ary = [1,2,3,4,5];
console.log(ary.head());   // 1
console.log(ary.tail());   // [2, 3, 4, 5]
console.log(ary.init());   // [1, 2, 3, 4]
console.log(ary.last());   // 5

 

4. Python で既存のクラスを拡張

組み込みクラスは拡張できない

Python では組込みクラスに手を出せない。

例えば「リスト」。

  • type 関数

で任意のリストに適用すると、返される値は

list

この list に属性を追加しようとすると、TypeError となる。

>>> list.x = 100
Traceback (most recent call last):
  File "", line 1, in 
TypeError: can't set attributes of built-in/extension type 'list'

つまり、list を拡張することはできない。

これに対処するには、組込みクラスを拡張することを諦め、リストをサブクラス化したものを使う。

(cf. ruby vs python | Lambda the Ultimate )

class List(list):
    def head(self):
        return self[0]

    def tail(self):
        return self[1:]

    def init(self):
        return self[0:-1]

    def last(self):
        return self[-1]

xs = List(range(1,6))

print xs.head()   #=> 1
print xs.tail()   #=> [2, 3, 4, 5]
print xs.init()   #=> [1, 2, 3, 4]
print xs.last()   #=> 5

 

ユーザが定義したクラスは、実行時に振舞いを変えることができる

ただし、ユーザが定義したクラスでは、後からメソッドを追加できる。

例えば、Ruby でユーザ定義したクラス Person に後からメソッドを追加するには、class 定義を繰り返せばよい。

class Person
    def initialize(name)
      @name = name
    end
end

ps = Person.new("Tarou")
p ps

class Person
  attr_accessor :age
end

ps.age = 30
p ps

ちなみに、Ruby が奇妙な言語だとはじめて感じたのは、この定義の仕方を見たとき。 class の定義が一つの場所に固定されてない。 Java であるなら、クラス定義は一箇所に限定され、実行時にそれが変化することはない。

この点に関して、「メタプログラミングRuby (p41) では class の役割について、わかりやすく説明されている。

Ruby の class キーワードは、クラス宣言というよりもスコープ演算子のようなものである。もちろん、存在しないクラスは作成する。しかしそれは、副作用と言ってもいいかもしれない。 class の主な仕事は、あなたをクラスのコンテキストに連れていくことである。そのコンテキストであなたがメソッドを定義する。

(太字は引用者による)

同様のことを、Python で書く場合は、

  1. クラスを定義
  2. 引数を一つ与える関数を別に定義
  3. 上記関数をクラスの属性として設定

( cf. 和訳 : なぜPythonのメソッド引数に明示的にselfと書くのか | TRIVIAL TECHNOLOGIES on CLOUD )

class Person:
    def __init__(self, name):
        self.name = name

def setAge(self, age):
    self.age = age

p = Person("Tarou")

Person.setAge = setAge
p.setAge(30)

print p.name
print p.age

 

5. Scala の暗黙の型変換は、既存のクラスを拡張したように見せかける

Scala では implicit conversion を利用すると、既存のクラスを拡張したように見せかけることができる。 implicit converson は、コンパイラがあるクラスのオブジェクトを別のクラスに自動的に変換してくれる機能。

定義の仕方は、

  1. 変換元のクラスをラップしたクラスを定義。
  2. 上記クラスに「拡張すると想定したメソッド」を定義。
  3. implicit def により、変換元から変換先を指定。

書き方は以下の通り。

implicit def 適当な名前(変換元のクラス) = 変換先のクラス

これにより、変換元のクラスに対して「拡張したと想定したメソッド」を呼び出したとき、コンパイラが自動的に変換先のクラスにしてくれる。

例えば、Scala の List クラスを拡張し、head, tail に相当するメソッド「先頭, 尾部」を定義したかのように見せかける。

class ListWrapper[T](xs : List[T]){ 
  def 先頭: T = xs.head
  def 尾部: List[T] = xs.tail
}

implicit def list2ListWrapper(xs: List[Any]) = new ListWrapper(xs)

val xs = Range(1,6).toList
println(xs.先頭)   // 1
println(xs.尾部)   // List(2, 3, 4, 5)

( cf. Ruminations of a Programmer: Why I like Scala's Lexically Scoped Open Classes )

コード上では、リストに対してメソッドを拡張したかのように見える。

 

6. Haskell はクラスとメソッドという関係がないので、自由に拡張できる

Haskell では、クラスとメソッドが結合している関係が存在しないので、既存のクラスを拡張という概念がそもそも存在しない。同様のことをしたいなら、単純に適用したい型を引数に与える関数を定義すればいい。

car, cdr をリストに対して適用できるようにするには、

car :: [a] -> a
car (x:_)  = x

cdr :: [a] -> [a]
cdr (_:xs) = xs

別名を付けるだけなら、

car = head
cdr = tail

 

オブジェクトは関数とデータ構造が密結合している

これを見て、「Why OO Sucks」のオブジェクト指向に対する批判を思い出した。

Objection 1 - Data structure and functions should not be bound together
Objects bind functions and data structures together in indivisible units. I think this is a fundamental error since functions and data structures belong in totally different worlds.

( cf. Matzにっき(2007-04-12) )