Metaprogramming in Ruby is a powerful feature that allows you to write code that writes code. This can make your programs more flexible and reduce redundancy. In this section, we will explore the key concepts of metaprogramming, provide practical examples, and offer exercises to help you master this advanced topic.

Key Concepts

  1. Dynamic Methods: Creating methods at runtime.
  2. Method Missing: Handling calls to undefined methods.
  3. Class Macros: Using methods to define other methods.
  4. Eval: Executing Ruby code stored in strings.
  5. Open Classes: Modifying existing classes.

Dynamic Methods

Dynamic methods allow you to define methods at runtime. This can be useful for creating methods based on user input or other runtime data.

Example

class DynamicMethodExample
  def self.create_method(name)
    define_method(name) do
      puts "Method #{name} called"
    end
  end
end

DynamicMethodExample.create_method(:hello)
example = DynamicMethodExample.new
example.hello  # Output: Method hello called

Explanation

  • define_method is used to create a method with the given name.
  • The method is defined within the class DynamicMethodExample.
  • When example.hello is called, it outputs "Method hello called".

Method Missing

method_missing is a powerful way to handle calls to methods that do not exist.

Example

class MethodMissingExample
  def method_missing(name, *args)
    puts "You tried to call #{name} with arguments #{args.join(', ')}"
  end
end

example = MethodMissingExample.new
example.some_method(1, 2, 3)  # Output: You tried to call some_method with arguments 1, 2, 3

Explanation

  • method_missing is overridden to handle calls to undefined methods.
  • It takes the method name and any arguments passed to it.
  • In this example, it simply prints out the method name and arguments.

Class Macros

Class macros are methods that define other methods. They are often used in libraries to provide a DSL (Domain-Specific Language).

Example

class MacroExample
  def self.attr_accessor_with_history(attr_name)
    attr_name = attr_name.to_s
    attr_reader attr_name
    attr_reader "#{attr_name}_history"

    define_method("#{attr_name}=") do |value|
      instance_variable_set("@#{attr_name}", value)
      @history ||= []
      @history << value
      instance_variable_set("@#{attr_name}_history", @history)
    end
  end
end

class Person < MacroExample
  attr_accessor_with_history :name
end

person = Person.new
person.name = "Alice"
person.name = "Bob"
puts person.name_history.inspect  # Output: ["Alice", "Bob"]

Explanation

  • attr_accessor_with_history is a class macro that creates an accessor with history tracking.
  • It defines a getter for the attribute and its history.
  • It also defines a setter that updates the history each time the attribute is set.

Eval

eval allows you to execute Ruby code stored in strings. Use it with caution as it can introduce security risks.

Example

code = "puts 'Hello from eval'"
eval(code)  # Output: Hello from eval

Explanation

  • eval takes a string containing Ruby code and executes it.
  • In this example, it prints "Hello from eval".

Open Classes

Ruby allows you to reopen and modify existing classes. This can be useful for adding methods to built-in classes.

Example

class String
  def shout
    self.upcase + "!!!"
  end
end

puts "hello".shout  # Output: HELLO!!!

Explanation

  • The String class is reopened, and a new method shout is added.
  • This method converts the string to uppercase and adds exclamation marks.

Practical Exercises

Exercise 1: Dynamic Methods

Create a class DynamicGreeter that dynamically defines a method greet_<name> for each name in an array.

class DynamicGreeter
  def self.create_greet_methods(names)
    names.each do |name|
      define_method("greet_#{name}") do
        puts "Hello, #{name}!"
      end
    end
  end
end

DynamicGreeter.create_greet_methods(["Alice", "Bob", "Charlie"])
greeter = DynamicGreeter.new
greeter.greet_Alice  # Output: Hello, Alice!
greeter.greet_Bob    # Output: Hello, Bob!
greeter.greet_Charlie  # Output: Hello, Charlie!

Solution

  • Use define_method within a loop to create methods for each name.
  • Call the dynamically created methods to verify they work.

Exercise 2: Method Missing

Create a class FlexibleCalculator that can handle basic arithmetic operations (add, subtract, multiply, divide) using method_missing.

class FlexibleCalculator
  def method_missing(name, *args)
    if name.to_s =~ /^(add|subtract|multiply|divide)_(\d+)_and_(\d+)$/
      operation, num1, num2 = $1, $2.to_i, $3.to_i
      case operation
      when "add"
        num1 + num2
      when "subtract"
        num1 - num2
      when "multiply"
        num1 * num2
      when "divide"
        num1 / num2
      else
        super
      end
    else
      super
    end
  end
end

calculator = FlexibleCalculator.new
puts calculator.add_2_and_3      # Output: 5
puts calculator.subtract_5_and_2 # Output: 3
puts calculator.multiply_3_and_4 # Output: 12
puts calculator.divide_10_and_2  # Output: 5

Solution

  • Use a regular expression to match method names and extract numbers.
  • Perform the corresponding arithmetic operation based on the method name.

Conclusion

Metaprogramming in Ruby allows you to write more flexible and dynamic code. By understanding and utilizing dynamic methods, method_missing, class macros, eval, and open classes, you can create powerful and concise programs. Practice these concepts with the provided exercises to reinforce your understanding and prepare for more advanced Ruby programming.

© Copyright 2024. All rights reserved