terça-feira, 30 de dezembro de 2014

Metaprogramming Ruby

I'm reading Metaprogramming Ruby 2: Program Like the Ruby Pros. I started to read it, because I guess that metaprogramming could make pieces of my code simpler, but I discovered that my problems are about abstractions. I just need better abstractions, nevertheless the concepts exposed in the book are very good and made me feel excited.

The first time I read about the Ruby object model I feel very impressed. Ruby is the first language that I learned with two concepts very new to me: dynamic typing and pure object oriented. So, understand how all the pieces fit together and get comfortable with the notion that everything is object, including classes, take some time to click.

I learn best by doing and now I'm experiencing an opportunity to apply metaprogramming (that I expect to blog in a near future). By doing and trying to understand what I was doing I can say that the Ruby object model clicked in my head, but one aspect that still confuses me a bit, and IMO isn't explained with great details in this book, is about method lookup.

How Ruby goes from the receiver of the calling to the location in the hierarchy of ancestors? One source of confusion was this picture:


I perceveid the object model in a very organized form: for each class, you have a singleton class and it's superclass is the singleton class of the class superclass. After you get it, it's not that complicated, but what I didn't perceived, was that in the middle of all this you can have modules!

This way, I was struggling in how I could add class methods to an existing class and also override some of it's methods. For example, for the following code:

module OneModule
  def my_method
    puts "OneModule.my_method"
  end
end

class MyClass
  class << self
    include OneModule
  end
end

How can I tweak my_method behavior? The first difficult that I had was map this code to the diagram above, since I didn't understand where the code was living.

After reading and re-reading the first chapters of Perrotta book, and tinkering with irb, I understand that it was going to the singleton class of MyClass.

This code provide insightful output:

p MyClass.ancestors
p MyClass.singleton_class.ancestors
p MyClass.methods(false) == MyClass.singleton_class.methods(false)
====
[MyClass, Object, Kernel, BasicObject]
[#, OneModule, #, #, Class, Module, Object, Kernel, BasicObject]
true

When you define a class method, it's stored in the singleton class. So if I would like to change a class method,  I have to change the singleton class, but this also puzzled me: how can I add it without removing the actual method?

From the output it's easy to see the solution, but it take me a while to realize that it was possible. I guess that the module methods where inserted in the singleton class, but the module is put as an ancestor of the singleton class.
With this knowledge in mind you can write the following:

module OtherModule
  def my_method
    puts "OtherModule.my_method"
    super
  end
end

class MyClass
  class << self
    include OtherModule
  end
end

p MyClass.my_method
p MyClass.singleton_class.ancestors
====
OtherModule.my_method
OneModule.my_method
[#, OtherModule, OneModule, #, #, Class, Module, Object, Kernel, BasicObject]

You can also call "super" in "OtherModule" and Ruby will chain the calls correctly.

After all, I still can't describe how method lookup works, and luckily someone blogged about it: Ruby's method lookup path, Part 1.

The algorithm can be resumed to the following:
  1. Methods defined in the object’s singleton class (i.e. the object itself)
  2. Modules mixed into the singleton class in reverse order of inclusion
  3. Methods defined by the object’s class
  4. Modules included into the object’s class in reverse order of inclusion
  5. Methods defined by the object’s superclass.
Armed with this, I'm by far better prepared to experiment and answer questions when they pop out. So nice, so good!

Nenhum comentário:

Postar um comentário