3

I'm learning ruby, and noticed that I cannot create a class method called puts:

class Printer
    def initialize(text="")
        @text = text
    end
    def puts
        puts @text
    end
end

The error is:

`puts': wrong number of arguments (given 1, expected 0)

My expectation was that I could use the code like this:

p = Printer.new("hello")
p.puts

It's not just because puts is a built-in method, though. For instance, this code also gives a syntax error:

def my_puts(text)
    puts text 
end

class Printer
    def initialize(text="")
        @text = text
    end
    def my_puts
        my_puts @name
    end
end
Cam
  • 14,930
  • 16
  • 77
  • 128
  • 2
    This has nothing do with class methods. You have defined an instance method `Printer#puts`. When you invoke `p = Printer.new("hello")`, and then invoke `p.puts`, your method `puts` is invoked and executes `puts @text`. The latter method `puts` has no explicit receiver, so the receiver defaults to `self`, which is `p`. Ruby therefore looks for the instance method `Printer#puts`. Finding it she notes that it is defined with no arguments but you have called it with one argument. Hence, the error message. Had you written `def puts; puts; end` you'd terminate with a *stack level too deep* error. – Cary Swoveland Jun 08 '20 at 18:47
  • The fix is simple: when naming custom methods avoid using the names of core methods, unless there is a good reason for doing so. – Cary Swoveland Jun 08 '20 at 18:56
  • @Cam : The error message says that `puts` does not expect parameters, and indeed, you defined your method `puts` without parameters, but you in your recursive invocation, you pass a parameter. The same applies with your second example, where you have `my_puts`. Of course, your usage of recursion is already by itself nonsense, but Ruby already aborts on the first invocation, because the number of the parameters in the call does not match the number of the parameters in the method definition. – user1934428 Jun 09 '20 at 07:09

2 Answers2

2

tldr; within the scope of the instance, the puts resolves to self.puts (which then resolves to the locally defined method, and not Kernel#puts). This method overriding is a form of shadowing.

Ruby has an 'implicit self' which is the basis for this behavior and is also how the bare puts is resolved - it comes from Kernel, which is mixed into every object.

The Kernel module is included by class Object, so its methods [like Kernel#puts] are available in every Ruby object. These methods are called without a receiver and thus can be called in functional form [such as puts, except when they are overridden].

To call the original same-named method here, the super keyword can be used. However, this doesn't work in the case where X#another_method calls X#puts with arguments when it expects to be calling Kernel#puts. To address that case, see Calling method in parent class from subclass methods in Ruby (either use an alias or instance_method on the appropriate type).

class X
  def puts
    super "hello!"
  end
end

X.new.puts

P.S. The second example should trivially fail, as my_puts clearly does not take any parameters, without any confusion of there being another "puts". Also, it's not a syntax error as it occurs at run-time after any language parsing.

user2864740
  • 60,010
  • 15
  • 145
  • 220
1

To add to the previous answer (https://stackoverflow.com/a/62268877/13708583), one way to solve this is to create an alias of the original puts which you use in your new puts method.

class Printer
  alias_method :original_puts, :puts
  attr_reader :text

  def initialize(text="")
    @text = text
  end

  def puts
    original_puts text
  end
end

Printer.new("Hello World").puts

You might be confused from other (static) programming languages in which you can overwrite a method by creating different signatures.

For instance, this will only create one puts method in Ruby (in Java you would have two puts methods (disclaimer: not a Java expert).

def puts(value)
end

def puts
end

If you want to have another method with the same name but accepting different parameters, you need to use optional method parameters like this:

def value(value = "default value")
end