Project

General

Profile

Actions

Feature #6478

closed

BasicObject#__class__

Feature #6478: BasicObject#__class__

Added by trans (Thomas Sawyer) almost 14 years ago. Updated 10 days ago.

Status:
Feedback
Target version:
-
[ruby-core:45180]

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

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

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?

  1. 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_call trick.

  2. 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.

  3. bind_call is 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:

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) 14 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 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.

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) 13 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 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.

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.

Actions

Also available in: PDF Atom