Feature #6478
closedBasicObject#__class__
Added by trans (Thomas Sawyer) almost 14 years ago. Updated 10 days ago.
Description
How else is one supposed to get the class of a subclass of BasicObject?
Updated by mame (Yusuke Endoh) almost 14 years ago
Actions
#1
[ruby-core:45182]
- Status changed from Open to Feedback
I don't understand you.
--
Yusuke Endoh mame@tsg.ne.jp
Updated by trans (Thomas Sawyer) almost 14 years ago
Actions
#2
[ruby-core:45183]
=begin
Sorry, I'll be more specific via example:
class Foo < BasicObject
end
foo = Foo.new
foo.class #=> raises NoMethodError
How to get class?
I suggest adding #class feature if there is no current means.
=end
Updated by mame (Yusuke Endoh) almost 14 years ago
Actions
#3
[ruby-core:45184]
- Status changed from Feedback to Assigned
- Assignee set to matz (Yukihiro Matsumoto)
Okay, thanks. I assign this to matz.
--
Yusuke Endoh mame@tsg.ne.jp
Updated by nobu (Nobuyoshi Nakada) almost 14 years ago
Actions
#4
[ruby-core:45195]
- Status changed from Assigned to Feedback
- Target version changed from 1.9.3 to 2.0.0
=begin
((Why)) do you need it?
BTW, it's possible with pure-ruby.
class Foo < BasicObject
include ::Kernel.dup.module_eval {
alias_method(:class, :class)
undef_method *(instance_methods - [:class, :object_id])
self
}
end
p Foo.new.class
=end
Updated by trans (Thomas Sawyer) almost 14 years ago
Actions
#5
[ruby-core:45196]
=begin
To ensure proper functionality when creating new instances from subclasses.
class Foo < BasicObject
def initialize(stuff)
@stuff = stuff
end
def dup
class.new(@stuff)
end
end
class Bar < Foo
end
We can't use (({Foo})) in dup, otherwise Bar would not be right.
=end
Updated by trans (Thomas Sawyer) almost 14 years ago
Actions
#6
[ruby-core:45197]
"BTW, it's possible with pure-ruby."
That's a rather nasty implementation. Is there no better way than that? I tried binding Kernel method but that didn't work, obviously, b/c BasicObject isn't "an instance of Kernel".
Updated by nobu (Nobuyoshi Nakada) almost 14 years ago
Actions
#7
[ruby-core:45204]
=begin
Seems what you want is (({dup})), not (({class})).
class Foo < BasicObject
mix ::Kernel, dup: :dup, clone: :clone
end
=end
Updated by trans (Thomas Sawyer) almost 14 years ago
Actions
#8
[ruby-core:45210]
That was just one example. Here, you can look at this for more cases:
https://github.com/rubyworks/ostruct2/blob/master/lib/ostruct2.rb
Just ctrl-f for class.
But what's this about "mix"? What Ruby are you running!? This is interesting, b/c I was thinking that I could use #respond_to? and I don't see anyway to add it to my BasicObject subclass except the "nasty" approach you demonstrated earlier.
Updated by nobu (Nobuyoshi Nakada) almost 14 years ago
Actions
#9
[ruby-core:45216]
=begin
(({Module#mix})) is a feature introduced last year, but may be removed from 2.0.
=end
Updated by Eregon (Benoit Daloze) almost 14 years ago
· Edited
Actions
#10
[ruby-core:45218]
nobu (Nobuyoshi Nakada) wrote:
Seems what you want is (({dup})), not (({class})).
class Foo < BasicObject
mix ::Kernel, dup: :dup, clone: :clone
end
But that would include all methods from Kernel with the current behavior of #mix, as mix ::Kernel would do.
So you need to opt-out all methods:
class Foo < BasicObject
meths = (::Kernel.instance_methods - [:dup])
mix ::Kernel, meths.each_with_object(dup: :dup) { |m,h| h[m] = nil }
end
(And Foo.new.dup fails with "undefined method `initialize_dup'")
That behavior of #mix is not very intuitive I think, what do you think about:
diff --git a/class.c b/class.c
index 8e637c0..e9d7a7e 100644
--- a/class.c
+++ b/class.c
@@ -769,8 +769,9 @@ do_mix_method_i(st_data_t key, st_data_t value, st_data_t arg)
st_table *aliasing = argp->aliasing;
st_data_t old, alias;
- if (aliasing && st_lookup(aliasing, ID2SYM(id), &alias)) {
- if (NIL_P(alias)) return ST_CONTINUE;
+ if (aliasing) {
+ if (!st_lookup(aliasing, ID2SYM(id), &alias) || NIL_P(alias))
+ return ST_CONTINUE;
id = rb_to_id(alias);
}
if (st_lookup(argp->mtbl, id, &old)) {
(and corresponding changes for the three other functions).
That is, if a Hash of methods is given, only import these methods.
Updated by yhara (Yutaka HARA) over 13 years ago
Actions
#11
[ruby-core:48276]
- Target version changed from 2.0.0 to 2.6
Updated by nobu (Nobuyoshi Nakada) over 13 years ago
Actions
#12
[ruby-core:48298]
=begin
"Method transplanting" is introduced into 2.0, so you can write:
class Foo < BasicObject
include ::Module.new {
[:dup, :initialize_dup, :initialize_copy].each {|m|
define_method(m, ::Kernel.instance_method(m))
}
}
end
I expect someone would make such method in (({Module})) as an external library.
=end
Updated by alexeymuranov (Alexey Muranov) over 13 years ago
Actions
#13
[ruby-core:49200]
Maybe BasicObject is not intended to be subclassed directly? Why not to subclass Object instead? I do not think it is wrong that basic objects do not know who their class is, after all they are basic.
Updated by naruse (Yui NARUSE) about 8 years ago
Actions
#14
- Target version deleted (
2.6)
Updated by trinistr (Alexander Bulancov) 15 days ago
Actions
#15
[ruby-core:124721]
I would like to revive discussion for this feature request as I feel that BasicObject#__class_ is still needed.
Note: obviously, it is possible to transplant methods with define_method (that is very nice!), but one doesn't always control the class of the object.
What for?
- Inspection of objects. Currently, it's not possible to print anything reasonable for a BasicObject-derived object.
- Introspection. Everything in Ruby is an object with a class, and not being able to get that class is weird and painful.
It's possible to use Kernel.instance_method(:class).bind_call(o), but why is this even required? It's very un-ergonomical.
Why?
-
Printing of classes is especially useful for error messages, and Ruby's internals can just do that (though maybe this is also just
Kernel#class?):class A < BasicObject; end 1.clone(freeze: A.new) # in 'Numeric#clone': unexpected value for freeze: A (ArgumentError)Creating such messages in regular code is unnecessarily hard when any object can happen, it requires conditionals and/or using the
bind_calltrick. -
Getting a class of an object is probably mostly useful for interactive use (which is still important), but the long incantation to do it is inconvenient and hard to remember as something that can be done.
-
bind_callis an order of magnitude slower than just using the method:class B < BasicObject; define_method(:__class__, ::Kernel.instance_method(:class)); end b = B.new class_method = Kernel.instance_method(:class) Benchmark.ips { |x| x.report("instance_method.bind_call") { Kernel.instance_method(:class).bind_call(b) } x.report("bind_call") { class_method.bind_call(b) } x.report("__class__") { b.__class__ } } ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [x86_64-linux] Warming up -------------------------------------- instance_method.bind_call 107.359k i/100ms bind_call 119.100k i/100ms __class__ 2.187M i/100ms Calculating ------------------------------------- instance_method.bind_call 1.022M (± 3.3%) i/s (978.28 ns/i) - 5.153M in 5.046952s bind_call 1.147M (± 4.6%) i/s (871.47 ns/i) - 5.836M in 5.096893s __class__ 22.494M (± 3.6%) i/s (44.46 ns/i) - 113.699M in 5.062084s
Compatibility
I believe virtually no code would be negatively impacted by addition of BasicObject#__class__. Code would need to both check for the presence of #__class__ and do something completely divorced from #class to be affected. On the other hand, it would allow for a method that doesn't conflict with a keyword.
Searching for uses of __class__ on GitHub (https://github.com/search?q=__class__+language%3Aruby&ref=searchresults&type=code&utf8=%E2%9C%93) seems to show just a few current uses:
- defining
__class__to refer to the class of a proxy object itself -
alias __class__ class, seemingly to call directly instead ofself.class - Rubinius has it (https://github.com/rubinius/rubinius/blob/84368419a49767ef9549a5778812e5f54b6c6223/core/alpha.rb#L146)
- Pythonification
There are some results that define __class__ if it doesn't exist already, but those should not be impacted, as the meaning would be the same.
In short
Adding BasicObject#__class__ would
- make it possible to always get the class for any object in the same way;
- be faster than current solution;
- have no negative impact on existing codebase.
Updated by jneen (Jeanine Adkisson) 15 days ago
Actions
#16
[ruby-core:124723]
I wonder if exposing a static Object.class_of(thing) would be appropriate? There's also the singleton_class to consider as well.
Updated by Eregon (Benoit Daloze) 15 days ago
Actions
#17
[ruby-core:124734]
In terms of naming I think __class__ looks like a hack that Python would use and not Ruby-like.
Why not add BasicObject#class?
I think Kernel.instance_method(:class).bind_call(o) is fine for most cases.
FWIW performance-wise on TruffleRuby there isn't much difference when storing the UnboundMethod in a constant:
truffleruby 33.0.1 (2026-01-20), like ruby 3.3.7, Oracle GraalVM Native [x86_64-linux]
Calculating -------------------------------------
instance_method.bind_call 33.492M (± 3.0%) i/s (29.86 ns/i) - 169.962M in 5.079892s
bind_call 631.582B (± 1.1%) i/s (0.00 ns/i) - 3.157T in 4.998908s
__class__ 750.398B (± 1.3%) i/s (0.00 ns/i) - 3.751T in 4.999106s
BasicObject are very difficult to use because they miss so many "standard" methods from Kernel.
As such I see them as mostly meant to proxy all the Kernel methods to maybe some delegate object or so, and the responsability of the BasicObject subclass to implement Kernel-like methods to make the object usable in normal Ruby code.
Updated by byroot (Jean Boussier) 14 days ago
Actions
#18
[ruby-core:124735]
Not that I'm for nor against this feature request, but:
In terms of naming I think class looks like a hack that Python would use and not Ruby-like.
I mean, it's consistent with __send__ and __id__, aliases of send and object_id meant to be used by generic code that want to ensure it can deal with any object, even ones that would redefine common methods.
Why not add BasicObject#class?
I suspect that would break various proxy classes in gems. Might not be a huge deal.
Updated by Eregon (Benoit Daloze) 14 days ago
Actions
#19
[ruby-core:124738]
byroot (Jean Boussier) wrote in #note-18:
I mean, it's consistent with
__send__and__id__, aliases ofsendandobject_idmeant to be used by generic code that want to ensure it can deal with any object, even ones that would redefine common methods.
Yeah, I dislike those too 😅
Specifically I think it looks very unidiomatic (and ugly) when used in Ruby code.
I think at least it's clear it's not good to go towards having every Kernel method as a __x__ variant on BasicObject.
Also even __send__ and __id__ can be redefined, so the KERNEL_CLASS = Kernel.instance_method(:class); KERNEL_CLASS.bind_call(obj) approach is in fact safer.
So my take on this is KERNEL_CLASS = Kernel.instance_method(:class); KERNEL_CLASS.bind_call(obj) is good enough,
and also most BasicObject subclasses should implement Kernel-like methods.
Concrete examples from real code where this is not good enough would most likely be helpful to move this forward if people want that.
Updated by byroot (Jean Boussier) 14 days ago
1Actions
#20
[ruby-core:124755]
So my take on this is KERNEL_CLASS = Kernel.instance_method(:class); KERNEL_CLASS.bind_call(obj) is good enough,
Yeah me too, I was just mentioning the existence of __id__ etc. But I agree that when code need to work with any arbitrary object, like e.g. zeitwerk or tapioca, then UnboundMethod is the safe way.
That being said it's unfortunate that it doesn't perform better on CRuby. I just had a look and It does allocate a Class (at least for module methods) and a method_entry_t, which is quite heavy for a call like #class.
Updated by trinistr (Alexander Bulancov) 10 days ago
Actions
#21
[ruby-core:124820]
jneen (Jeanine Adkisson) wrote in #note-16:
I wonder if exposing a static
Object.class_of(thing)would be appropriate? There's also the singleton_class to consider as well.
This is an interesting idea. It would solve the problems around this, in a simpler way than an UnboundMethod. But it defintely feels off for Ruby, though we have some methods like that (Regexp.linear_time? comes to mind).
Eregon (Benoit Daloze) wrote in #note-19:
I think at least it's clear it's not good to go towards having every
Kernelmethod as a__x__variant on BasicObject.
Also even__send__and__id__can be redefined, so theKERNEL_CLASS = Kernel.instance_method(:class); KERNEL_CLASS.bind_call(obj)approach is in fact safer.So my take on this is
KERNEL_CLASS = Kernel.instance_method(:class); KERNEL_CLASS.bind_call(obj)is good enough,
and also most BasicObject subclasses should implement Kernel-like methods.Concrete examples from real code where this is not good enough would most likely be helpful to move this forward if people want that.
I agree, porting all methods from Kernel is unreasonable. It's just that having access to object's class is fundamental and would provide replacements for many generic methods (especially methods and co). In my experience, __class__ would be much more useful than __id__, for example.
IMHO, redefining basic methods is not a compelling argument, Kernel#class could be redefined too.
On the other hand, #class being redefined happens more often than for #__class__.
KERNEL_CLASS = Kernel.instance_method(:class)
This looks even less Ruby-like to me than a silly method name :p
Also, due to constant resolution, placing such constants requires extra consideration or littering the code with them.
Concrete examples from real code where this is not good enough would most likely be helpful to move this forward if people want that.
Ractor.shareable?(Kernel.instance_method(:class)) # => false
So Ractor-compatible code can not use such a constant (relevant #17513 is still open).
And just basic stuff like this would be much easier to write:
def my_chunkers(collection)
raise ArgumentError, "#{collection.__class__} is not enumerable" unless Enumerable === collection
collection.chunk(&:itself)
end
byroot (Jean Boussier) wrote in #note-18:
Why not add BasicObject#class?
I suspect that would break various proxy classes in gems. Might not be a huge deal.
That's that I thought, too, there probably is code relying on class being proxied, otherwise I would push for that name.