Could someone explain please how the code works

Viewed 83

There is some code:

def func
   def func
      1
   end
end

then I try the following in irb:

func
func.func
func

and get the result:

:func
1
1

Could anyone please explain what's going on? I kinda understand the first output but not the latter. Thanks!

2 Answers

You define a method inside a method in a global scope. Method definition returns a symbol with it's name.

  1. When you call func for the first time, it's redefined, by the inner func. That's why subsequent calls to func return 1.
  2. Method definition returns a symbol and you can call any globally-defined method on the symbol, that's why you can call func.func. Try to define other method and you'll be able to call it on any symbol:
def func
   def func
      1
   end
end
def a
  'a'
end
func.a
# 'a'
:asd.a
# 'a'

This is complicated. This really shouldn't work in Ruby, but it does.

def func at the top-level defines a global method. But how does that work in Ruby? It actually uses two hacks:

  1. It adds the method to Object so that you can call it from anywhere (because self is always an Object)
  2. It makes the method private, because the rule for private methods is that they can't have an "explicit receiver" i.e. you have to call them without putting an object and a dot before them. This is to prevent mistakes like if I think that a class Foo has its own puts method but it actually doesn't, Foo.new.puts will raise an error, rather than call the global puts.

So if your code were just

def func
  1
end

1.func

It would crash because you're trying to call a private method on the object 1.

But here's where it gets weird. If you define a method inside another method, it defines a normal instance method as if it were not nested at all

class A
  def outer
    def inner
      3
    end
  end
end

x = A.new
y = A.new
x.outer
y.inner # calls the method defined by x

This wasn't really an intentional design in Ruby, but more of a fallback for a weird situation. The lead designer doesn't like that it's even possible

...the current behavior of nested method definition is useless. It should be made obsolete to open up the future possibility (I'd vote for warning).

Your code behaves so strangely because you're doing it at the top level where self is just an Object:

def func # normal private method in Object
  def func # adds a normal instance method to the current class i.e. Object
    1
  end
end

Object.private_methods.include?(:func) # true
func # returns :func, but also now re-defines func to be a normal method on Object
Object.private_methods.include?(:func) # false
func.func # same as 1.func, which is OK because it's not private
Related