This is a collection of Metaprogramming Ruby copy-paste examples.

Metaprogramming is gentle art of writing code that defines/writes other code.

Article was published 2018-08-23 and examples were tried under Ruby version 2.5.1

Please be aware that metaprogramming is handy but also dangerous. Right amount may make your library/gem awesome but overuse may lead your project hard to understand or debug.

Define method

define_method is usually used when you want to dynamically define methods. E.g.:

class Account attr_accessor :state (1..99).each do |i| define_method("credit_#{i}".to_sym) do self.state = state + i end define_method("debit_#{i}".to_sym) do self.state = state - i end end def initialize(state) @state = state end end account = Account.new(20) account.credit_31 account.state # => 51 account.debit_45 account.state # => 6

Methods will also appear on the public_methods list:

account.public_methods(false) # [ ... , :credit_83, :debit_83, :credit_84, :debit_84, ...] account.respond_to?(:debit_19) # => true method = account.public_method(:debit_19) # => #<Method: Account#debit_19>

Variations of the define_method :

(Based on this SO answer)

With arguments

class Bar define_method(:foo) do |arg1, arg2| arg1 + arg2 end end a = Bar.new a.foo #=> ArgumentError (wrong number of arguments (given 0, expected 2)) a.foo 1, 2 # => 3

Optional argument

class Bar define_method(:foo) do |arg=nil| arg end end a = Bar.new a.foo #=> nil a.foo 1 # => 1

As many arguments as you want

class Bar define_method(:foo) do |*arg| arg end end a = Bar.new a.foo #=> [] a.foo 1 # => [1] a.foo 1, 2 , 'AAA' # => [1, 2, 'AAA']

Keyword arguments:

class Bar define_method(:foo) do |option1: 'default value', option2: nil| "#{option1} #{option2}" end end bar = Bar.new bar.foo option2: 'hi' # => "default value hi" bar.foo option2: 'hi',option1: 'ahoj' # => "ahoj hi"

As many keyword arguments as you want:

class Bar define_method(:foo) do |**keyword_args| keyword_args end end bar = Bar.new bar.foo option1: 'hi', option2: 'ahoj', option3: 'ola' # => {:option1=>"hi", :option2=>"ahoj", :option3=>"ola"}

All of them

class Bar define_method(:foo) do |variable1, variable2, *arg, **keyword_args, &block| p variable1 p variable2 p arg p keyword_args p block.call end end bar = Bar.new bar.foo :one, 'two', :three, 4, 5, foo: 'bar', car: 'dar' do 'six' end ## will print: :one "two" [:three, 4, 5] { foo: "bar", car: "dar" } 'six'

method missing

Simmilar to define_method , method_missing is usually used when you want to dynamically define methods. E.g.:

class Account attr_accessor :state def initialize(state) @state = state end def method_missing(method_name, *args, **keyword_args, &block) if result = method_name.match(%r(\Adebit_\d*\z)) self.state = state - extract_number(result) elsif result = method_name.match(%r(\Acredit_\d*\z)) self.state = state + extract_number(result) else super end end private def extract_number(matched_result) matched_result[0].split('_')[1].to_i end end account = Account.new(20) account.debit_12 account.state # => 8 account.credit_999 account.state # => 1007

Reason why to define_method might be a better choice doh is that method missing dosn’t register them to public_methods :

account.public_methods(false) # => [:state=, :state, :method_missing]

Therfore you cannot do method operations on it:

account.respond_to?(:debit_19) # => false method = account.public_method(:debit_19) # => NameError (undefined method `debit_19' for class `Account')

Argument passing to method_missing apply similar way as for define_method :

def method_missing(method_name, *args, **keyword_args, &block) # ... end

Include / Extend (modules & mixins)

module Debit module ClassMethods def is_awesome? "is awesome" end end def self.included(base) base.extend(ClassMethods) end def transaction(amount) self.state = state - amount end end class Account include Debit attr_accessor :state def initialize(state) @state = state end end Account.is_awesome? # => "is Awesome" account = Account.new(20) account.state # => 20 account.transaction(4) account.state # => 16

Ruby on Rails framework introduced ActiveSupport::Concerns that will make this even easier:

# require 'active_support/concern' module Debit extend ActiveSupport::Concern included do # scope :approved, -> { where(approved: true) } end class_methods do def is_awesome? "is awesome" end end def transaction(amount) self.state = state - amount end end class Account include Debit attr_accessor :state def initialize(state) @state = state end end Account.is_awesome? # => "is Awesome" account = Account.new(20) account.state # => 20 account.transaction(4) account.state # => 16

You may want to use extend to widen your class methods:

module Bar def bar "bar" end end class Foo extend Bar end Foo.bar # => "bar"

But you may use same module for both extend and include

module Bar extend self # `self` in this case is `Bar`, therefore you are doing equivalent of `extend Bar` def bar "bar" end end class Foo include Bar end Bar.bar # => "bar" Foo.new.bar # => "bar"

Eval

class Account attr_accessor :state def initialize(state) @state = state end end Account.instance_eval do def is_awesome? "it is truly awesome" end end Account.class_eval do def debit(amount) self.state = state - amount end end Account.is_awesome? # => "it is truly awesome" account = Account.new(20) account.debit(3) account.state # => 17 account.instance_eval do def credit(amount) self.state = state + amount end end account.credit(35) account.state # => 52 # BUT ! If I initialize new Account instance, this method will not be there account = Account.new(6) account.credit(15) # NoMethodError (undefined method `credit' for #<Account:0x0000000000d259b0 @state=6>) # `instance_eval` is simmilar of doing: def account.other_version_of_credit(amount) self.state = state + amount end account.other_version_of_credit(5) # => 11 def Account.other_awesome "still awesome" end Account.other_awesome # => "still awesome"

Now there also possibility to use kernel eval but I highly recommend to avoid it (security) unless you really know what you are doing.

meaning_of_life = 42 eval("def answer; #{meaning_of_life}; end") answer # => 42

you will be able to achieve same results with other technique mentioned in this post

Singleton Class object extend

Inspired by article by Benedikt Deicke - Changing the Way Ruby Creates Objects

module Debit def transaction(amount) self.state = state - amount end end module Credit def transaction(amount) self.state = state + amount end end class Account attr_accessor :state def initialize(state) @state = state end end account = Account.new(20) account.state # => 20 account.singleton_class.include(Debit) account.transaction(4) account.state # => 16 account.singleton_class.include(Credit) account.transaction(5) account.state # => 21

Method re-binding

Be aware! This is still experimental feature in Ruby and too fast.

I’ve already wrote article about this Method re-binding in Ruby if you want to learn more.

class Account attr_accessor :state def initialize(state) @state = state end end module Debit def transaction(amount) self.state = state - amount end end module Credit def transaction(amount) self.state = state + amount end end account = Account.new(100) account.state # => 100 puts account.public_methods(false) # => [:state, :state=] debit = Debit.instance_method(:transaction) credit = Credit.instance_method(:transaction) # Lets do debit transactions transaction = debit.bind(account) transaction.call(6) account.state # => 94 account.public_methods(false) # => [:state, :state=] # Lets do credit transactions transaction = credit.bind(account) transaction.call(15) account.state # => 109 account.public_methods(false) # => [:state, :state=]

Related articles

Discussion

I’ll try to add more in next couple of days as they pop to my mind. But if I forgot to add your favorite one pls ping me a comment