Project

General

Profile

Actions

Feature #12084

closed

`Class#instance`

Added by sawa (Tsuyoshi Sawada) almost 7 years ago. Updated about 1 month ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:73878]

Description

For meta-programming/debugging purposes, I would like to request the inverse of Object#singleton_class. Namely, a method that is called on a class that is a singleton class, and returns the object it is a singleton of. Since the Singleton module in the standard library http://ruby-doc.org/stdlib-2.3.0/libdoc/singleton/rdoc/Singleton.html assigns the method name instance to such classes, I think Class#instance should be the name for such feature.

Array.singleton_class.instance # => Array
"foo".singleton_class.instance # => "foo"

When the receiver is a class but is not a singleton class, then it should raise an error.

Array.instance # => error

Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #12655: Accessing the method visibilityClosedActions
Related to Ruby master - Bug #11063: Special singleton class should return true for singleton_class? testRejectedActions

Updated by justcolin (Colin Fulton) over 6 years ago

This feature would solve a lot of problems I had while doing what should have been simple meta-programming (if there is such a thing as "simple" meta-programming...).

Something to consider is what happens with nested singleton classes. Let's say you have this code:

Array.singleton_class
     .singleton_class
     .instance

Does that return Array or the first singleton class? I would think the former is more useful, but less obvious.

Actions #2

Updated by jeremyevans0 (Jeremy Evans) about 1 year ago

Updated by Eregon (Benoit Daloze) about 1 year ago

#instance seems a too generic name for such a rarely-needed meta-programming feature, I think #singleton_instance is better.

@justcolin (Colin Fulton) it would result in Array.singleton_class, otherwise that would break the invariant of #singleton_instance being the reverse of #singleton_class.

A concrete use-case would be welcome.

Updated by ufuk (Ufuk Kayserilioglu) about 1 year ago

Agreed that instance is a bad name for this concept and we should not be basing the name on the Singleton class, since it is not the same kind of singleton in the singleton_class.

From what I understand this would be exposing the concept of "attached_object" to user code and that concept is clearly documented inside the CRuby source code to mean exactly this:

...
 *   - attached object: A singleton class knows its unique instance.
 *     The instance is called the attached object for the singleton class.
...

So a good name would be attached_object and its behaviour could be:

Array.singleton_class.attached_object # => Array
"foo".singleton_class.attached_object # => "foo"

Array.attached_object # => nil
"foo".attached_object # => nil

Updated by sawa (Tsuyoshi Sawada) about 1 year ago

ufuk (Ufuk Kayserilioglu) wrote in #note-4:

Array.attached_object # => nil
"foo".attached_object # => nil

That clearly cannot be accepted as a feature because you would then have no way to distinguish whether the class's singleton object is nil or it does not have a singleton object. Note the following:

NilClass.instance # => nil

Updated by matz (Yukihiro Matsumoto) 12 months ago

  • instance is NG. For example, Array.instance => nil is confusing
  • attached_object is better, at least for singleton classes. But there's still no real-world use-case.

For your information, NilClass is not a singleton class. It's a class with only an instance. This is side evidence of this method is confusing. Even the original proposer can misunderstand the concept.

Matz.

Updated by Eregon (Benoit Daloze) 12 months ago

For your information, NilClass is not a singleton class. It's a class with only an instance. This is side evidence of this method is confusing. Even the original proposer can misunderstand the concept.

Interesting, I think what confuses people (including myself) is nil.singleton_class # => NilClass.
And basically that's because #singleton_class has special behavior for true/false/nil (special_singleton_class_of).
Such behavior is not used for any other Ruby value (the rest is TypeError or create a new anonymous singleton class).

It's a bit funny too:

irb(main):002:0> TrueClass.singleton_class?
=> false
irb(main):003:0> true.singleton_class.singleton_class?
=> false

It kind of makes sense given TrueClass, etc are defined as regular classes, and then creating an extra/separate singleton class would be confusing (methods could be defined on either).

I guess it would be possible to create TrueClass as a singleton class (with superclass Object) and name it, but that's another proposal and the pros/cons are unclear to me at this stage.

Updated by jemmai (Jemma Issroff) 12 months ago

matz (Yukihiro Matsumoto) wrote in #note-6:

But there's still no real-world use-case.

We have defined the method described here , for MemoWise, a memoization gem. (For what it’s worth, we named it original_class_from_singleton.)

It’s feasible that someone wants to memoize a method on a singleton class. Here is an example*:

require "memo_wise"

class << String 
  prepend MemoWise

  def example_method
    "example"
  end
  memo_wise :example_method
end

String.example_method
# => “example”

Within MemoWise, we receive the memo_wise(:example_method) call on the singleton class of String, and must resolve it back to define the memoization on the String class itself, not its singleton class. We therefore resolve the original class by searching ObjectSpacefor the class whose singleton class is the one we received:

def self.original_class_from_singleton(klass)
  ObjectSpace.each_object(Module).find do |cls|
    cls.singleton_class == klass
  end
end

I believe this is almost exactly the built in method being proposed.

*It is worth noting that this same functionality could be achieved by the following snippet, instead of opening up the singleton class:

require "memo_wise"

class String 
  prepend MemoWise

  def self.example_method
    "example"
  end
  memo_wise self: :example_method
end

String.example_method
# => "example"

But I think part of the beauty of Ruby is that there are multiple ways to express the same sentiment, and while we allow for opening up the singleton class to do this, it makes sense to me that a gem like MemoWise must support this case, and therefore a method as defined in this issue does have a real world use case. It’s also worth noting that a different design decision in MemoWise might make the need go away soon for MemoWise specifically.

Updated by Eregon (Benoit Daloze) 12 months ago

jemmai (Jemma Issroff) wrote in #note-8:

Within MemoWise, we receive the memo_wise(:example_method) call on the singleton class of String, and must resolve it back to define the memoization on the String class itself, not its singleton class.

That's not clear to me, why do you need to access String? Can't the memoization be stored on the singleton class (String.singleton_class)?
Modules/classes are always "unique", there are no two same instances, so it seems always fine to store the data on the class/module holding/owning (Method#owner) the method (the module/class for which method_defined?(name) is true/has it in instance_methods).

 #   * Performance concern: searches all Class objects
 #     But, only runs at load time and results are memoized

BTW, it (internally) iterates all objects on most Ruby implementations, not just modules/classes, the filtering is typically done on top because there is no "next instance pointer" (e.g., unlike in some Smalltalk).
ObjectSpace.each_object is a clear no-no when anyone cares about the time to load a library/use it during startup (it's O(nb of objects in heap at the time of call)).

Actions #10

Updated by Eregon (Benoit Daloze) 9 months ago

  • Related to Bug #11063: Special singleton class should return true for singleton_class? test added

Updated by ufuk (Ufuk Kayserilioglu) 3 months ago

matz (Yukihiro Matsumoto) wrote in #note-6:

  • attached_object is better, at least for singleton classes. But there's still no real-world use-case.

I recently ran into a real-world use-case inside one of the gems I maintain and I would like make another case for this request.

Use Case - Introspection

To give some context, Tapioca is a gem to generate RBI files for gems and for other DSL generated runtime methods. Tapioca needs to rely heavily on runtime reflection and introspection to achieve what it does so that it can generate a proper RBI representation of what a gem intended to export from its implementation.

One feature we recently added was a way to attribute modules in the ancestor chain of a module/class to the correct gem that the module was mixed in from. In order to do this, we need to hook into the include/extend/prepend operations to keep track of where the mixins are coming from. This part is all working fine, until we get a mixin on the singleton class of a class like:

module Foo
end

class Bar
  class << self
    include Foo
  end
end

In this case, the mixin is happening on #<Class:Bar> but Tapioca needs to match that to one of the ancestors of Bar so that it can generate the mixin in the RBI file. In order to do that, Tapioca needs to be able to find Bar from #<Class:Bar>.

Workarounds

Our first (naive and failed) attempt to do that was to parse the to_s representation of the singleton class to figure out the Bar part. This failed, because some classes override the inspect method on the class. Most prominently ActiveRecord::Base subclasses have an overridden inspect class method that has modified output. This ends up changing the output of the inspect method on the singleton class, since Ruby makes an internal call to inspect on the attached class to get the class name. It is impossible to get the original inspect method called via bind_call tricks, since the call to the attached class inspect method happens inside Ruby.

Reluctantly, we had to resort to the ObjectSpace walk solution for matching a singleton class to its attached object (which is a class in our case), which really slowed down our implementation.

Relevant work on Tapioca can be found here and here

Suggestion

However uncommon, this use-case does not currently have a good workaround, other than resorting to the terrible method of walking the object space for each identification we need to perform.

If this is a compelling use-case for Class#attached_object feature, I would love to implement it such that a call to Class#attached_object throws if the receiver is not a singleton class to begin with. I think that has the cleanest semantics, since callers can always check with Module#singleton_class? before calling Class#attached_object.

So the proposed implementation would work like this:

# Success cases
Array.singleton_class.attached_object # => Array
Array.singleton_class.singleton_class.attached_object # => #<Class:Array>
"foo".singleton_class.attached_object # => "foo"

# Error cases
Array.attached_object # => error
"foo".attached_object # => error
NilClass.attached_object # => error (since `NilClass.singleton_class? #=> false`)
true.singleton_class.attached_object # => error (since `true.singleton_class.singleton_class? #=> false`)

Updated by Dan0042 (Daniel DeLorme) 2 months ago

I think it would be nicer if #attached_object returns nil instead of raising an error. Since nil can't be a valid return value anyway. So you don't need to check for #singleton_class? if you want to avoid the cost of Exceptions.

Find all singleton objects:
ObjectSpace.each_object(Class).filter_map{ |c| c.attached_object rescue nil }
vs
ObjectSpace.each_object(Class).filter_map{ |c| c.attached_object if c.singleton_class? }
vs
ObjectSpace.each_object(Class).filter_map(&:attached_object)

Updated by ufuk (Ufuk Kayserilioglu) 2 months ago

Dan0042 (Daniel DeLorme) wrote in #note-12:

I think it would be nicer if #attached_object returns nil instead of raising an error. Since nil can't be a valid return value anyway. So you don't need to check for #singleton_class? if you want to avoid the cost of Exceptions.

I have no problems with returning nil if that is desired, since, as you said, nil is never going to be a valid return value. However, I fear that people will see:

NilClass.attached_object # => nil
nil.singleton_class # => NilClass 

and expect NilClass.singleton_class? to be true, but it is false. For that reason, raising an error felt safer.

Additionally, as I stated above, this is not meant to be an easy to use API for mass consumption, so terseness of the code that uses this method is not my primary concern. As in most reflection related APIs (looking at const_source_location and autoload?: https://bugs.ruby-lang.org/issues/17354) the usage can be complicated as long as it gets the job done.

@matz (Yukihiro Matsumoto) Do you have any opinions on the final state of this feature request?

Updated by ufuk (Ufuk Kayserilioglu) 2 months ago

I put together an implementation for this and created a PR: https://github.com/ruby/ruby/pull/6450

Updated by Eregon (Benoit Daloze) 2 months ago

I'm +1 for adding Class#attached_object.
nil vs raise seem both fine to me, although I slightly prefer raise for clarity for the nil case.

Updated by matz (Yukihiro Matsumoto) about 1 month ago

attached_object looks good to me. Accepted.

Matz.

Actions #17

Updated by ufuk (Ufuk Kayserilioglu) about 1 month ago

  • Status changed from Open to Closed

Applied in changeset git|0378e2f4a8319440dd65c82b16f189161472d237.


Add Class#attached_object

Implements [Feature #12084]

Returns the object for which the receiver is the singleton class, or
raises TypeError if the receiver is not a singleton class.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0