Programming log - Shindo200

イベント参加記録とプログラミング系の雑記

Ruby の優先順位で引っ掛かったときの話

似たようなメソッドを持つクラスを大量に作るとき、一個ずつクラスを定義するのは面倒なので、メタプログラミングでクラスを定義することにしました。

module Domi
  class Tresure; end

  StandardTresureCards = [
    {:name => :Copper, :cost => 0, :coin => 1},
    {:name => :Silver, :cost => 3, :coin => 2},
    {:name => :Gold, :cost => 6, :coin => 3}
  ]

  StandardTresureCards.each do |card|
    self.const_set(card[:name], Class.new(Tresure) do
      define_method("play") do
        # ...
      end
    end)
  end
end

このコードは問題なく実行できますし、全てのクラスに play メソッドが定義されています。ただ、end) という書き方が少しダサいので、このように書き直してみました。

module Domi
  class Tresure; end

  StandardTresureCards = [
    {:name => :Copper, :cost => 0, :coin => 1},
    {:name => :Silver, :cost => 3, :coin => 2},
    {:name => :Gold, :cost => 6, :coin => 3}
  ]

  StandardTresureCards.each do |card|
    self.const_set card[:name], Class.new(Tresure) do
      define_method "play" do
        # ...
      end
    end
  end
end

Ruby は引数付きのメソッドを呼ぶときに括弧を省略することができますので、これで問題ないはずなのですが、この後に play メソッドを呼ぶと「undefined method」エラーが返って来ます。なぜでしょうか。

原因を調べる

今回のエラーは括弧を省略したことで発生しました。括弧の中からコードを外すと優先順位が低くなるので、ブロックが Class.new ではなく Module#const_set に渡されてしまったのでしょうか。Module#const_set にモンキーパッチを当てて確かめてみます。

class Module
  def const_set(*args)
    p "#{args.first} => #{block_given?}"
  end
end

Module.const_set "Foo", Class.new(Object) do; end
Module.const_set("Bar", Class.new(Object) do; end)

出力結果はこの通りでした。

"Foo => true"
"Bar => false"

括弧を省略した方(Foo)にはブロックが渡されていて、括弧を付けた方(Bar)にはブロックが渡されていません。括弧を省略することでコードの優先順位が変わり、ブロックが Module#const_set に渡されてしまったことが原因のようですね。

対策

Class.new のブロックに do end より優先度が高い {} を使う方法がありますけど、コーディングスタイルの都合で出来れば使いたくありません。なので、Class.new ではクラスの定義だけをして、その後に class_eval のブロック内でメソッドを定義することで対策しました。

module Domi
  class Tresure; end

  StandardTresureCards = [
    {:name => :Copper, :cost => 0, :coin => 1},
    {:name => :Silver, :cost => 3, :coin => 2},
    {:name => :Gold, :cost => 6, :coin => 3}
  ]

  StandardTresureCards.each do |card|
    klass = self.const_set(card[:name], Class.new(Tresure))
    klass.class_eval do
      define_method("play") do
        # ...
      end
    end
  end
end

これならば全てのクラスに play メソッドが定義されています。問題ないようなので、この方法でやっていこうと思います。

おまけ問題

次の4つのコードのうち、実行すると SyntaxError が返されるコードはどれでしょうか?

# A
1.upto(10) {}

# B
1.upto(10) do; end

# C
1.upto 10 {}

# D
1.upto 10 do; end

答えは「C」。(文字色を白にしています。)

余談

今回掲載したコードを見て、私が何を作ろうとしたのか分かる人には分かってしまいますね。作りかけですが、興味がある方はこちらのリポジトリを見てください。
OdaillyJP/rdominion