Search code examples
rubyoopconstructor

Ruby class constructor using `()` rather than `.new()`


Please do not ask me "why do you want to do this?" I'm just exploring the nooks and crannies of Ruby, with no specific goals in mind.

Some Ruby classes can be instantiated with just parentheses rather than explicitly invoking the #new method. For example,

irb> require 'pathname'
irb> p=Pathname('/etc/xdg/autostart')
  => #<Pathname:/etc/xdg/autostart>
irb> n=Pathname.new('/etc/xdg/autostart')
  => #<Pathname:/etc/xdg/autostart>
irb> p==n
  => true

And some use this for casting/coercion, as in Integer(4.5). And there are probably other uses I haven't encountered yet, or can't remember.

However, I haven't found a way to do this for custom classes:

irb> class Foo ; def initialize(*args) ; @args=args ; end ; end
  => :initialize
irb> f1=Foo.new(1,2,3)
  => #<Foo:0x00007f330a29f4e8 @args=[1, 2, 3]>
irb> fp=Foo(1,2,3)
(irb):7:in `<main>': undefined method `Foo' for main:Object (NoMethodError)
Did you mean?  for

The only thing I can figure out is that both Pathname and Integer (for example) use C code, and somehow wnd up being Kernel methods:

irb> Object.method(:Pathname)
  => #<Method: #<Class:Object>(Kernel)#Pathname(_)>
irb> Object.method(:Integer)
  => #<Method: #<Class:Object>(Kernel)#Integer(*)>
irb> Object.method(:Foo)
(irb):10:in `method': undefined method `Foo' for class `#<Class:Object>' (NameError)
Did you mean?  for

So this appears to be something provided by the C/.so extension code.

However, can it be done simply with native Ruby code without resorting to a kludge like the following?

irb> Object.define_method(:Foo) { |*args| Foo.new(*args) }
  => :Foo
irb> Foo(1,2,3)
  => #<Foo:0x00007f330a3b9018 @args=[1, 2, 3]>

Solution

  • Those global methods are so-called module functions defined in Kernel. You can define your own global function this way: (using the Ruby 3 shorthand syntax for defining methods)

    module Kernel
      def Foo(...) = Foo.new(...)
      module_function :Foo
    end
    

    The above defines a method Kernel#Foo which forwards all arguments to Foo.new. Since Object includes Kernel, this allows to create a Foo instances from within objects via:

    Foo(1, 2, 3) #=> #<Foo:0x000000010633e120 @args=[1, 2, 3]>
    

    In addition, module_function makes the method available as a class method:

    Kernel.Foo(1, 2, 3) #=> #<Foo:0x0000000108b00398 @args=[1, 2, 3]>
    

    it also marks the instance method as being private so you can't call it with any other explicit receiver:

    :hello.Foo(1, 2, 3)
    # NoMethodError: private method `Foo' called for an instance of Symbol
    

    This is also how Integer, Pathname etc. are integrated.

    Note that the built-in global methods which correspond to a class are intended primarily for converting values to that class, e.g. Integer("123") #=> 123. They don't just pass along the arguments. (Integer doesn't even respond to new)