Determine Class from Eigenclass
Asked Answered
G

3

9

In Ruby, getting the eigenclass of a class Foo is a simple as

eigenclass = class << Foo; self; end
#=> #<Class:Foo>
eigenclass = Foo.singleton_class #2.1.0
#=> #<Class:Foo>

I'm interested in the inverse operation: getting the owner of the eigenclass from the eigenclass itself:

klass = eigenclass.owner
#=> Foo

I'm not sure if this is possible, given that the eigenclass is an anonymous subclass of Class, so Foo appears nowhere in its inheritance hierarchy. Inspecting the method list of the eigenclass isn't encouraging either. eigenclass.name returns nil. The only thing that gives me hope this is possible:

Class.new # normal anon class
#=> #<Class:0x007fbdc499a050>
Foo.singleton_class
#=> #<Class:Foo>

Clearly, the eigenclass's to_s method knows something about the owner, even if this information is hardcoded when the eigenclass is instantiated. Therefore the only method I'm aware of is some hacky Object.const_getting from that like

Object.const_get eigenclass.to_s[/^#\<Class\:(?<owner>.+)\>$/, :owner]
#=> Foo
Gery answered 1/2, 2014 at 21:19 Comment(1)
More succinctly: Given the value of "foo".singleton_class, how can we get back to "foo"?Paviour
G
3

Refining @BroiSatse's answer in a ruby-implementation-agnostic way,

class A; end
class B < A; end
class C < A; end
eigenclass = A.singleton_class
ObjectSpace.each_object(eigenclass).find do |klass|
  klass.singleton_class == eigenclass
end
#=> A

This is also reliable when handling branches in subclass trees, the only reason why @Andrew Marshall's elegant answer doesn't work.

Gery answered 1/2, 2014 at 22:17 Comment(0)
P
6

Use ObjectSpace.each_object passing it the singleton class to find all classes that match the given singleton class:

Klass = Class.new
ObjectSpace.each_object(Klass.singleton_class).to_a  #=> [Klass]

However, since a class’s singleton class inherits from its superclass’s singleton class, you’ll get multiple results if the class you’re trying to find has subclasses:

A = Class.new
B = Class.new(A)
B.singleton_class.ancestors.include?(A.singleton_class)  #=> true

candidates = ObjectSpace.each_object(A.singleton_class)
candidates.to_a  #=> [A, B]

Fortunately, classes/modules are sortable by their place in the inheritance tree (same order ancestors gives). Since we know all the results must be part of the same inheritance tree, we can take the max to get the correct class:

candidates.sort.last  #=> A
ObjectSpace.each_object(B.singleton_class).max  #=> B
Prolific answered 1/2, 2014 at 22:18 Comment(8)
This doesn't seem to work if there's a branch in the inheritance tree: class A; end; class B < A; end; class C < A; end ObjectSpace.each_object(A.singleton_class).to_a #=> [C, B, A] ObjectSpace.each_object(A.singleton_class).sort #=> ArgumentError: comparison of Class with Class failedGery
@ChrisKeele Hmm, interesting. I don’t think there’s really an elegant way around that issue unfortunately. It fails, of course, because it doesn’t know whether to put B before C or vice-versa. Given that, I think that your own answer is probably the most reliable without being over-engineered.Prolific
It's looking like it. TIL that Modules are sortable though, which is super nifty in other metaprogramming contexts.Gery
I'm wondering how that compare-function is actually implemented? I mean it's only a partial order, so a comparison-based sort intuitively won't work. But Enumerable#sort is comparison-based, right? It must be kind of hackily color the subtrees and lexicographically compare tuples of colors. Kinda strange.Lounging
According to the Module#<=> docs it returns nil if the two modules have no relation or the comparison isn't possible. I imagine ancestors of equal depth are in the latter category, and Enumerable#sort raises an error if <=> doesn't return -1, 0, or 1.Gery
Andrew, some prefer max to sort.last. :-)Baronial
@ChrisKeele: Ah, so it just fails if the items don't form an inheritance chain :) I'm glad to hear that, it would have to be an uber-hacky compare function otherwise :PLounging
@CarySwoveland Good point, not sure why that simplification escaped me when writing this—I prefer it too. Updated.Prolific
G
3

Refining @BroiSatse's answer in a ruby-implementation-agnostic way,

class A; end
class B < A; end
class C < A; end
eigenclass = A.singleton_class
ObjectSpace.each_object(eigenclass).find do |klass|
  klass.singleton_class == eigenclass
end
#=> A

This is also reliable when handling branches in subclass trees, the only reason why @Andrew Marshall's elegant answer doesn't work.

Gery answered 1/2, 2014 at 22:17 Comment(0)
U
1

Use ObjectSpace:

e = class << 'foo'; self; end

ObjectSpace.each_object(e).first    #=> 'foo'

To get object from inside of eigenclass:

class << 'foo'
  puts ObjectSpace.each_object(self).first
end

#=> 'foo'  
Urethrectomy answered 1/2, 2014 at 21:51 Comment(4)
That's quite clever, but not 100% reliable: class A; end; class B < A; end; ObjectSpace.each_object(A.singleton_class).first #=> BGery
I'm pretty sure this is because most implementations avoid instantiating eigenclasses until needed, so if A hasn't invoked its own eigenclass in its definition, it first gets called when subclassing B.Gery
Which ruby version are you using, I am still getting A running code above.Urethrectomy
I'm on MRI 2.1.0-p0 ATM.Gery

© 2022 - 2024 — McMap. All rights reserved.