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)