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"
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"
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"
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"
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"
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"
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"]
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!"
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"
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"
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
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
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
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"
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"
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)