TL; DR
- あるオブジェクトから、特異クラスの特異クラスに定義されたインスタンスメソッドは呼び出せない
- 特異クラスの特異クラスについても「スーパークラスの特異クラスは特異クラスのスーパークラス」
はじめに
「メタプログラミングRuby」には、特異クラスについて以下のような記述があります。
特異クラスはクラスである。クラスはオブジェクトである。オブジェクトは特異クラスを持っている......。これはどこまで続くのだろう?他のオブジェクトと同じように、特異クラスも特異クラスを持っているはずだ。
特異クラスの特異クラスが役に立つ場面を見つけたら、ぜひ世界に発信しよう。
(メタプログラミングRuby 第2版 P.130)
前々から心に留まっていたこのコラム。そこで本記事では、特異クラスの特異クラスの使い方をひねり出してみました1。
私は業務等の大規模開発でRubyを使ったことがありません...「普通にこう書いたほうが楽」等ありましたらコメントしていただけるとありがたいです。
動作を確かめる
まずは、特異クラスの特異クラスがどのように動くのかを確認します。検証したバージョンは以下の通りです。
$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
特異クラスの特異クラスに定義されたインスタンスメソッドは呼び出せない
オブジェクトは特異メソッド(=特異クラスのインスタンスメソッド)を呼び出すことができます。
str = "abc"
class << str
def greet_singleton
puts "singleton"
end
end
# selfを使って書いても同様
def str.greet_singleton
puts "singleton"
end
str.greet_singleton # singleton
「何を当たり前のことを」という話ですが、特異クラスの特異クラスのインスタンスメソッドになると話が変わってきます。
str = "abc"
class << str
class << self
def greet_singleton_singleton
puts "singleton_singleton"
end
end
end
# NoMethodError!
str.greet_singleton_singleton
もちろん、以下は可能です。
str = "abc"
class << str
class << self
def greet_singleton_singleton
puts "singleton_singleton"
end
end
end
# 特異クラスから特異クラスの特異クラスのインスタンスメソッドを呼ぶ
str.singleton_class.greet_singleton_singleton # singleton_singleton
「メタプログラミングRuby」に出てきたメソッド探索の仕組み「右へ一歩、それから上へ」で考えたら納得しました。(図中の #
は特異クラスを表します)
スーパークラスの特異クラスは特異クラスのスーパークラス
「スーパークラスの特異クラスは特異クラスのスーパークラス」、こちらも「メタプログラミングRuby」本文に出てきたRubyのクラスの仕組みです2。
この関係は特異クラスの特異クラスでも成り立ちました。以下のように、str
の特異クラスの特異クラスが String
の特異クラスを継承していることが確認できます。
# Stringの特異メソッド
puts String.try_convert("a")
# strの特異クラスからも呼び出される
puts str.singleton_class.try_convert("a")
図で書くと以下のようになります。
特異クラスの特異クラスを使ってみたい
特異メソッドの定義でメタプログラミングを使う
というわけで実際に特異クラスの特異クラスを使ってみます。1つ思いついたのは、特異クラスの定義内でDSLを使う場合です。
person = Person.new("Taro", 20)
class << person
class << self
include Debugger
end
# インスタンス変数をデバッグするメソッドを定義
# (呼び出すとdebugメソッドが作られる)
debug_vars :name, :age
end
person.debug
@name: "Taro"
@age: 20
debug
を person
の特異メソッドとして定義するためには、特異クラス内で特異メソッド debug_vars
が使える必要があります。そのため、debug_vars
を定義する場所は person
の特異クラスの特異クラスになります。
# 使いまわせるようモジュール化
module Debugger
# 指定したインスタンス変数を表示するメソッド `debug` を定義するメソッド
def debug_vars(*var_names)
define_method :debug do
var_names.each do |name|
value = instance_variable_get("@" + name.to_s)
puts "@#{name}: #{value.inspect}"
end
end
end
end
正直使える場面は多くなさそうです... (大抵は設計をちゃんとしてクラスに組み込んだほうがよさそう)
特異クラスはどこまで作れるのか
最後に、「特異クラスの特異クラスの特異クラスの...」を限界までたどってみたいと思います。理論上限界はありませんが果たして...
module SingletonTracer
def trace_singleton(n)
singleton = self
n.times do
singleton = singleton.singleton_class
end
singleton
end
end
obj = Object.new
class << obj
include SingletonTracer
end
手始めに100回。余裕ですね。
puts obj.trace_singleton(100)
#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Class:#<Object:0x00007f2fe2dc4420>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
徐々に増やしていくと、2500回でスタックの限界に到達してしまいました。
puts obj.trace_singleton(2500)
(irb#1):39:in `to_s': stack level too deep (SystemStackError)
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
from (irb#1):39:in `to_s'
... 2319 levels...
(2023/5/29追記: 再度試してみたところ11887回まで成功しました。メモリ負荷をあげても結果が変わらず、当時の2500回で SystemStackError
する状態は再現できませんでした...他にも要因があるのでしょうか...?)
表示しなければさらに入れ子を増やせますが、100000に増やしたらirbが固まってしまいました(それはそう)