Metaprogramming Ruby cheatcheat
Today I've Learned postThis 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
Entire blog website and all the articles can be forked from this Github Repo