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)