Ruby脳でCoffeeScriptのクラスを理解する

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

Ruby脳でCoffeeScriptのクラスを理解する : melborne.github.com

                                                              • -


Rubyは最高の言語だから
もっと普及していいと思うけれども
その障害となっているのはたぶん
Rubyがビジュアルに訴えない言語」となっているからだよ
たしかにRubyにはRuby/TkとかShoesとかがあるけど
現代のプログラミングで「ビジュアル」と言ったら
暗黙的に「Web上の」という修飾が付くよね


一方でJavaScript
jQueryCoffeeScriptの人気を見る限り
最高とは言えない言語だけれども
「ビジュアルに訴える言語」となっている点が
普及の大きな要因になっていると思うよ
つまりブラウザ上で実行できる唯一の言語たる地位が
JavaScriptの大きなアドバンテージなんだね


だから今のところ
「最高の言語でビジュアルなプログラミング」
をすることはできないけれども僕らにはCoffeeScriptがあるよ
CoffeeScriptRubyの影響を大きく受けてるから
この言語を使って「ビジュアル」なプログラミングをすることが
現時点での最良の選択だと僕は思うんだよ


そんなわけで..


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のプロパティを持った
オブジェクトが生成されるんだね
JavaScriptRubyのような
クラスベースのオブジェクト指向ではなくて
コピーベースのオブジェクト指向だから
ここで生成された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) 記述を一部加筆・修正しました。