Ruby脳でCoffeeScriptのクラスを理解する
Ruby
は最高の言語だから、もっと普及していいと思うけれども、その障害となっているのはたぶん「Rubyがビジュアルに訴えない言語」となっているからだよ。たしかにRubyにはRuby/TkとかShoesとかがあるけど、現代のプログラミングで「ビジュアル」と言ったら、暗黙的に「Web上の」という修飾が付くよね。
一方でJavaScript
は、jQuery
やCoffeeScript
の人気を見る限り、最高とは言えない言語だけれども「ビジュアルに訴える言語」となっている点が普及の大きな要因になっていると思うよ。つまりブラウザ上で実行できる唯一の言語たる地位が、JavaScriptの大きなアドバンテージなんだね。
だから今のところ「最高の言語でビジュアルなプログラミング」をすることはできないけれども、僕らにはCoffeeScriptがあるよ。CoffeeScriptはRubyの影響を大きく受けてるから、この言語を使って「ビジュアル」なプログラミングをすることが現時点での最良の選択だと僕は思うんだよ。
そんなわけで..
JavaScriptのことをよく知らないRuby脳の僕が、Coffeescriptのクラスのことを少し学んだので、ここでRubyのクラスと対比しつつ説明してみるよ。きっと誤解があると思うけど間違っていたら教えてね。なお以下ではCoffeeScriptのことを単にCoffeeと呼ぶよ。
さっそくCoffeeを使って簡単なクラスを定義してみるよ。
class Duck
constructor: (@name, @age) ->
say: ->
"Quack Quack #{@name}!"
mofi = new Duck('Mofi', 12)
pipi = new Duck('Pipi', 9)
tete = new Duck('Tete', 5)
mofi.say() # => "Quack Quack Mofi!"
pipi.say() # => "Quack Quack Pipi!"
tete.say() # => "Quack Quack Tete!"
Rubyを知っているならこのコードはすぐ読めるよね。new関数でDuckオブジェクトを生成して、sayメソッドを呼んでいるよ。
対応するRubyコードはこんな感じかな。
class Duck
def initialize(name, age)
@name, @age, = name, age
end
def say
"Quack Quack #{@name}"
end
end
mofi = Duck.new('Mofi', 12)
pipi = Duck.new('Pipi', 9)
tete = Duck.new('Tete', 5)
mofi.say # => "Quack Quack Mofi"
pipi.say # => "Quack Quack Pipi"
tete.say # => "Quack Quack Tete"
インスタンス変数への初期値の代入構文はRubyにもほしい機能だよね。
一見これらのコードは同じに見えるけど、異なる挙動が2つほどあるよ。
1つ目はCoffeeでは先のコードで、既にインスタンス変数への外部からのアクセスが可能になっている点だよ。確かめてみよう。
mofi.name = "mofy"
mofi.name # => mofy
mofi.say() # => Quack Quack Mofy!
読み出しも書き込みもできる。Rubyではメソッドを介してじゃないとインスタンス変数にアクセスすることはできないので、Coffeeと等価にするにはアクセッサメソッドを定義する必要があるよ。
2つ目はsayの呼び出しには常にカッコが必要な点だよ。CoffeeでRubyのようにカッコを省略すると次のような結果が返るよ。
mofi.say
# => function () {
return "Quack Quack " + this.name + "!";
}
これはJavaScriptに変換されたsayのコードそのものだよ。そしてCoffeeではsayの後のカッコがそのメソッドを実行させるんだね。
つまりこういうことだよ。Coffee(JavaScript)ではオブジェクトの後に続く.nameや.sayはオブジェクトの内部変数nameやsayにセットされた値にアクセスする方法なんだよ。そしてJavaScriptでは関数はファーストクラスのオブジェクトだから、他のデータと同じように内部変数にそのままセットできるんだ。JavaScriptではこのような内部変数をプロパティと呼ぶそうだよ。
さてこれらの点を考慮してRubyのコードを修正すると次のようになるよ。
class Duck
attr_accessor :name, :age
def initialize(name, age)
@name, @age, = name, age
end
def say
->{ "Quack Quack #{@name}" }
end
end
mofi = Duck.new('Mofi', 12)
pipi = Duck.new('Pipi', 9)
tete = Duck.new('Tete', 5)
mofi.say.call # => "Quack Quack Mofi"
pipi.say.() # => "Quack Quack Pipi"
tete.say[] # => "Quack Quack Tete"
tete.say # => #<Proc:0x00000100866680@-:8 (lambda)>
attr_accessorの説明は不要だよね。sayメソッドはProcオブジェクトを返すようにして、呼び出し側でProc#callすればCoffeeと同様の結果が得られるよ。Proc#callの別名() []もここで示したよ。
さて一応先のCoffeeコードをJavaScriptにコンパイルしたものにも目を通してみるよ。CoffeeScriptの公式サイトでTRY COFFSCRIPTすると、次のJavaScriptのコードが得られるんだ。
var Duck, mofi, pipi, tete;
Duck = (function() {
function Duck(name, age) {
this.name = name;
this.age = age;
}
Duck.prototype.say = function() {
return "Quack Quack " + this.name + "!";
};
return Duck;
})();
mofi = new Duck('Mofi', 12);
pipi = new Duck('Pipi', 9);
tete = new Duck('Tete', 5);
mofi.say();
pipi.say();
tete.say();
JavaScriptのことはよくわからないから、ここからの説明は僕の推測を大いに含んでいるよ。まずCoffeeにおけるconstructor: () ->
というのがfunction Duck(){}
に変換されているから、constructorは関数定義になることがわかるよ。このDuck関数を実行してnewに渡すとnameとageのプロパティを持ったオブジェクトが生成されるんだね。JavaScriptはRubyのようなクラスベースのオブジェクト指向ではなくて、コピーベースのオブジェクト指向だから、ここで生成された3つのオブジェクトmofi, pipi, teteは、Duckオブジェクトのコピーと考えればいいのかな。
次にCoffeeにおける say: ->
が、Duck.prototype.say = function(){}
と変換されているよ。つまり、Duckオブジェクトのprototypeという名のプロパティにsayプロパティが生成されてここに関数がセットされている。なるほどコピーベースのオブジェクト指向においては、Duck.say = function(){} とすると関数の実体がすべてのオブジェクトにコピーされてしまって、効率上問題がある。だからprototypeという共通の器を作ってそこに関数を置けるようにしたんだね。
メソッドの追加
さて次にオブジェクトに別のメソッドを追加してみよう。Rubyでインスタンスメソッドを追加するにはクラスを再オープンすればいいよね。
class Duck
def how_old
->{ "I'm #{@age} years old." }
end
end
mofi.how_old.call # => "I'm 12 years old."
pipi.how_old.call # => "I'm 9 years old."
Coffeeで同じことをするには上で学んだようにDuckのprototypeプロパティに関数をセットすればいいはずだよ。これはCoffeeでは次のようにするよ。
Duck::howOld = ->
"I'm #{@age} years old."
mofi.howOld() # => "I'm 12 years old."
pipi.howOld() # => "I'm 9 years old."
Rubyで::は定数のスコープ演算子を表すから、これはちょっと間違えそうだね。
JavaScriptにコンパイルするよ。
Duck.prototype.howOld = function() {
return "I'm " + this.age + " years old.";
};
mofi.howOld(); # => "I'm 12 years old."
pipi.howOld(); # => "I'm 9 years old."
いいみたいだね。ちなみにJavaScriptではクラスを再オープンすることはできなさそうだね。Duckを再定義すると別のDuckオブジェクトが定義されてしまうよ。
プロパティの追加
Coffeeでは個々のオブジェクトに簡単にプロパティを設定できるよ。
pipi.color = 'brown'
pipi.swim = ->
"swim #{@age} days!"
pipi.color # => 'brown'
pipi.swim() # => 'swim 9 days!'
もちろんこれらは他のオブジェクトからは参照できないよ。
tete.color # => undefined
tete.swim() # => TypeError: Object #<Duck> has no method 'swim'
JavaScriptの対応コードは次のようになるよ。
pipi.color = 'brown';
pipi.swim = function() {
return "swim " + this.age + " days";
};
pipi.color;
pipi.swim();
Coffeeのオブジェクトにおけるこの軽量さはRuby脳にはちょっと驚きだよ。まるでRubyのHashのようだね。
さてRubyでもオブジェクト固有のメソッドを定義できるので、等価コードを書いてみるよ。
class << pipi
attr_accessor :color
def swim
"swim #{@age} days!"
end
end
pipi.color = 'brown'
pipi.color # => "brown"
pipi.swim # => "swim 9 days!"
tete.color # => undefined method `color'
tete.swim # => undefined method `swim'
Rubyではpipiオブジェクトについてシングルトンクラスを開いて、各メソッドを定義する必要があるよ。
ちなみにprototypeプロパティに定義された関数と同名の関数をプロパティにセットすると、どうなるかは想像がつくよね。そのオブジェクトに関してはそれが優先して呼び出されるんだ。
class Duck
constructor: (@name, @age) ->
say: ->
"Quack Quack #{@name}!"
mofi = new Duck('Mofi', 12)
pipi = new Duck('Pipi', 9)
tete = new Duck('Tete', 5)
mofi.say = ->
"Gaa Gaa #{@name}!"
mofi.say() # => "Gaa Gaa Mofi!"
pipi.say() # => "Quack Quack Pipi!"
tete.say() # => "Quack Quack Tete!"
この挙動はRubyでも同じだね
クラスメソッド
さて次にDuckクラスにクラスメソッドを定義することを考えてみるよ。まずはRubyにDuckの総数をカウントするcountクラスメソッドを定義してみるよ。
class Duck
@@count = 0
def self.count
@@count
end
def initialize(name, age)
@name, @age, = name, age
@@count += 1
end
end
mofi = Duck.new('Mofi', 12)
pipi = Duck.new('Pipi', 9)
tete = Duck.new('Tete', 5)
Duck.count # => 3
クラス変数@@count
を初期化しDuck.countメソッドを定義して、@@countにアクセスできるようにする。そしてinitializeでカウントアップするよ。
CoffeeにおいてDuckクラスの外でDuckのプロパティをセットするのはDuck.count = 0
でできるけど、クラス定義の中では次のように書くみたいだね。
class Duck
@count: 0 または @count = 0
constructor: (@name, @age) ->
Duck.count += 1
mofi = new Duck('Mofi', 12)
pipi = new Duck('Pipi', 9)
tete = new Duck('Tete', 5)
Duck.count # => 3
インスタンス変数と同じ@
を使うよ。ちょっと紛らわしいけどプロパティの中と外で@
の意味が変わることを覚えとけばいいね。
プライベート・メソッド
さて次にDuckにプライベートメソッドを定義してみるよ。Rubyではprivateキーワードで簡単にできるよね。eatメソッドで呼ばれるfoodメソッドを定義するね。
class Duck
def eat
->{ "eat " + food }
end
private
def food
"meat!"
end
end
mofi = Duck.new('Mofi', 12)
mofi.eat.call # => "eat meat!"
mofi.food # => private method `food' called
それでプライベートメソッドでインスタンス変数を呼ぶことももちろんできるよ。
class Duck
def eat
->{ "eat " + food }
end
private
def food
"#{@age} meat!"
end
end
mofi = Duck.new('Mofi', 12)
mofi.eat.call # => "eat 12 meat!"
Coffeeでプライベートメソッドを定義するには、ちょっとわからないけど次のようにするのかな。
class Duck
eat: ->
"eat " + food()
food = ->
"beans"
mofi = new Duck('Mofi', 12)
mofi.eat() # => 'eat beans'
mofi.food() # => TypeError: Object #<Duck> has no method 'food'
オブジェクト内のfood変数に”beans”を返す無名関数をセットするよ。
次にfoodでインスタンス変数を呼ぶよ。
class Duck
eat: ->
"eat " + food()
food = ->
"#{@age} beans"
mofi = new Duck('Mofi', 12)
mofi.eat() # => 'eat undefined beans'
残念ながらこれがうまくいかないんだよ。ちょっと僕には理由がわからないんだけど..
ここでは引数でオブジェクトを指し示すthisを受け渡して目的を達成するよ。
class Duck
eat: ->
"eat " + food(this)
food = (obj)->
"#{obj.age} beans"
mofi = new Duck('Mofi', 12)
mofi.eat() # => 'eat 12 beans'
僕が勉強したのはここまでだよ。Coffeeではクラスの継承もできるみたいなんだけどそれはまた別機会にするよ。
(追記:2011-09-09) 記述を一部加筆・修正しました。
blog comments powered by Disqus