Ruby Metaprogramming: part 1
Tags: ruby, rails
Published: 2019-12-24 09:41:33

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](#chapter-1)
[2. Dynamic method: write code that define method](#chapter-2)
[3. Calling method using string](#chapter-3)
[4. Calling class using string](#chapter-4)
[5. Method missing](#chapter-5)
<a name="chapter-1"></a>
## 1. Open Classes
Let say you have this method:
```ruby
def remove_leading_zeros(str)
  str.sub!(/^0*/, '')
end

str = '0090'
puts remove_leading_zeros(str)
# => "90"
```
You can just put the method inside String class
```ruby
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"
```
Be careful of **Monkeypatching**: you redefine existing method in a class.
```ruby
str = 'Ruby Language'

# Normal
puts str.upcase
# => "RUBY LANGUAGE"

# Monkeypatching
class String
  def upcase
    downcase
  end
end
puts str.upcase
# => "ruby language"
```
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.
<a name="chapter-2"></a>
## 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:
```ruby
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"
```
Pretty repetitive if you have 5 countries, right. Let's add metaprogramming:
```ruby
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"
```
You can pass arguments, options, and blocks too!

Well, these codes will explain better:
```ruby
class Bar
  define_method(:foo) do |arg=nil|
    puts arg
  end
end

Bar.new.foo
# => nil

Bar.new.foo("baz")
# => "baz"
```
```ruby
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"]
```
```ruby
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!"
```
<a name="chapter-3"></a>
## 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)
```ruby
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"
```
The point is, use `send()`. To make it clear:
```ruby
str = 'Ruby Languange'

# Normal calling method
str.upcase
# => "RUBY LANGUAGE"

# Call method using string
str.send('upcase')
# => "RUBY LANGUAGE"

```
There are `send()` and `public_send()`. The code below will explain what is the difference. It'll also explain how to use argument.
```ruby
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
```
<a name="chapter-4"></a>
## 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.
```ruby
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

```
<a name="chapter-5"></a>
## 5. Method missing
Let's back to our `WhereAmI` class (#2). What if we want to call undefined method?
```ruby
i_am = WhereAmI.new
i_am.in_mars
# => NoMethodError
```
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:
```ruby
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"
```
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`!
```ruby
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"
```
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!