DEV Community

K Putra
K Putra

Posted on • Edited on

Ruby Metaprogramming: part 1

Metaprogramming is, write code that writes code. There are many articles out there which explain the fundamental of ruby metaprogramming, but I just want to cover how to use metaprogramming.

Do I need to learn metaprogramming? Yes, you do! Ruby Metaprogramming is a powerful tool to refactor your code (besides design pattern).

Let's start our first journey!

Table of Content
1. Open Classes
2. Dynamic method: write code that define method
3. Calling method using string
4. Calling class using string
5. Method missing

1. Open Classes

Let say you have this method:

def remove_leading_zeros(str) str.sub!(/^0*/, '') end str = '0090' puts remove_leading_zeros(str) # => "90" 
Enter fullscreen mode Exit fullscreen mode

You can just put the method inside String class

class String def remove_leading_zeros sub!(/^0*/, '') end end str = '0090' puts str.remove_leading_zeros # => "90" arr = ('0090'..'0100').to_a arr.map! { |a| a.remove_leading_zeros } puts arr.join(' ') # => "90 91 92 93 94 95 96 97 98 99 100" 
Enter fullscreen mode Exit fullscreen mode

Be careful of Monkeypatching: you redefine existing method in a class.

str = 'Ruby Language' # Normal puts str.upcase # => "RUBY LANGUAGE" # Monkeypatching class String def upcase downcase end end puts str.upcase # => "ruby language" 
Enter fullscreen mode Exit fullscreen mode

This is not ilegal, just be careful, because may be you will get un-notice bugs in the future. Always check for existing method before adding new method to a class.

2. Dynamic method: write code that define method

We know how to define method in ruby. But let's write code that define a method.
Let's say we have this kind of class:

class WhereAmI def in_indonesia puts 'I am in Indonesia' end def in_america puts 'I am in America' end end i_am = WhereAmI.new i_am.in_indonesia i_am.in_america # => "I am in Indonesia" # => "I am in America" 
Enter fullscreen mode Exit fullscreen mode

Pretty repetitive if you have 5 countries, right. Let's add metaprogramming:

class WhereAmI ['indonesia', 'america', 'england', 'germany', 'japan'].each do |method| define_method "in_#{method}" do puts "I am in #{method.capitalize}" end end end i_am = WhereAmI.new i_am.in_indonesia i_am.in_america i_am.in_england # => "I am in Indonesia" # => "I am in America" # => "I am in England" 
Enter fullscreen mode Exit fullscreen mode

You can pass arguments, options, and blocks too!

Well, these codes will explain better:

class Bar define_method(:foo) do |arg=nil| puts arg end end Bar.new.foo # => nil Bar.new.foo("baz") # => "baz" 
Enter fullscreen mode Exit fullscreen mode
class Bar define_method(:foo) do |arg1, arg2, *args| puts arg1 + ' ' + arg2 + ' ' + args end end Bar.new.foo # => wrong number of arguments Bar.new.foo("one", "two", "three", "four", "five") # => "one two ["three", "four", "five"] 
Enter fullscreen mode Exit fullscreen mode
class Bar define_method(:foo) do |arg, *args, **options, &block| puts arg puts args puts options puts 'Hey you use block!' if block_given? end end Bar.new.foo("one", "two", 3, 4, this_is: 'awesome', yes: 'it is') do 'foobar' end # => "one" # => ["two", 3, 4] # => {:this_is => "awesome", :yes => "it is"} # => "Hey you use block!" 
Enter fullscreen mode Exit fullscreen mode

3. Calling method using string

Okay, but what if I want to call all the method in previous class (#2) in one line? We call method using string (because it is String, you can use string interpolation)

i_am = WhereAmI.new countries = ['indonesia', 'america', 'england', 'germany', 'japan'] countries.each { |country| i_am.send("in_#{country}") } # => "I am in Indonesia" # => "I am in America" # => "I am in England" # => "I am in Germany" # => "I am in Japan" 
Enter fullscreen mode Exit fullscreen mode

The point is, use send(). To make it clear:

str = 'Ruby Languange' # Normal calling method str.upcase # => "RUBY LANGUAGE" # Call method using string str.send('upcase') # => "RUBY LANGUAGE" 
Enter fullscreen mode Exit fullscreen mode

There are send() and public_send(). The code below will explain what is the difference. It'll also explain how to use argument.

class MyClass private def priv_method(var) puts "Private #{var}" end end # This will throw error, because we call for private method MyClass.new.priv_method('Ruby') # => NoMethodError # This won't throw error MyClass.new.send('priv_method', 'Ruby') # => "Private Ruby" # This will throw error, because public_send calls public method only MyClass.new.public_send('priv_method', 'Ruby') # => NoMethodError 
Enter fullscreen mode Exit fullscreen mode

4. Calling class using string

What if I want to create a class instance using string? Yes you can!(Note: because it is String, you can use string interpolation)

There is Rails's way, and there is Ruby's way. Let's use WhereAmI class (#2) again, and I add new class inside a module.

module Foo class Bar end end # Normal i_am = WhereAmI.new foo = Foo::Bar.new # Rails's way, you can only use this in Ruby on Rails i_am = 'WhereAmI'.constantize.new foo = 'Foo::Bar'.constantize.new # Ruby's way i_am = Object.const_get('WhereAmI').new foo = ('Foo::Bar'.split('::').inject(Object) {|o,c| o.const_get c}).new 
Enter fullscreen mode Exit fullscreen mode

5. Method missing

Let's back to our WhereAmI class (#2). What if we want to call undefined method?

i_am = WhereAmI.new i_am.in_mars # => NoMethodError 
Enter fullscreen mode Exit fullscreen mode

Yes, we get NoMethodError. WhereAmI does not know how to handle a method that it does not have! What if I don't want to get NoMethodError? So let's add method_missing to our class:

class WhereAmI # Code in #2 is copied to here def method_missing(method, *args, &block) puts "You called method #{method} using argument #{args.join(', ')}" puts "--You also using block" if block_given? end end i_am = WhereAmI.new i_am.in_mars i_am.in_mars('with', 'elon musk') i_am.in_mars('with', 'ariel tatum') { "foobar" } # => "You called in_mars using argument " # => "You called in_mars using argument with, elon musk" # => "You called in_mars using argument with, ariel tatum" # => "--You also using block" 
Enter fullscreen mode Exit fullscreen mode

Now, probably you don't really know what to do with this function. I'll give you one. Let's upgrade our WhereAmI class in #2.

Before, we can only call 5 countries as given in the arrays. What if I want to able to call all countries and all planet and also all known-stars in the universe? Let's combine define_method and method_missing!

class WhereAmI def method_missing(method, *args, &block) return super method, *args, &block unless method.to_s =~ /^in_\w+/ self.class.send(:define_method, method) do puts "I am in " + method.to_s.gsub(/^in_/, '').capitalize end self.send(method, *args, &block) end end i_am = WhereAmI.new i_am.in_indonesia i_am.in_mars i_am.in_betelgeuse # => "I am in Indonesia" # => "I am in Mars" # => "I am in Betelgeuse" 
Enter fullscreen mode Exit fullscreen mode

Still don't know when to use method_missing? Easy, you will figure it out soon.

That's all for Part 1. I still don't know what to write in other Parts. But, see you in other Parts!

Top comments (0)