LoginSignup
5
0

特異クラスの特異クラスを使ってみたい

Last updated at Posted at 2023-05-27

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」に出てきたメソッド探索の仕組み「右へ一歩、それから上へ」で考えたら納得しました。(図中の # は特異クラスを表します)

method_search.png

スーパークラスの特異クラスは特異クラスのスーパークラス

「スーパークラスの特異クラスは特異クラスのスーパークラス」、こちらも「メタプログラミングRuby」本文に出てきたRubyのクラスの仕組みです2

この関係は特異クラスの特異クラスでも成り立ちました。以下のように、strの特異クラスの特異クラスが String の特異クラスを継承していることが確認できます。

# Stringの特異メソッド
puts String.try_convert("a")
# strの特異クラスからも呼び出される
puts str.singleton_class.try_convert("a")

図で書くと以下のようになります。

superclass_singletonclass.png

特異クラスの特異クラスを使ってみたい

特異メソッドの定義でメタプログラミングを使う

というわけで実際に特異クラスの特異クラスを使ってみます。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

debugperson の特異メソッドとして定義するためには、特異クラス内で特異メソッド 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が固まってしまいました(それはそう)

  1. 正直に言うと、昔思いついたネタをQiitaのイベントに便乗して放流しました

  2. 初めて読んだ時にこの仕様の美しさに痺れました...

5
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0