Skip to content

Instantly share code, notes, and snippets.

@BrianSigafoos
Last active March 8, 2023 14:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save BrianSigafoos/ed1b4247d1ace1c7742d2b41a363f11a to your computer and use it in GitHub Desktop.
Save BrianSigafoos/ed1b4247d1ace1c7742d2b41a363f11a to your computer and use it in GitHub Desktop.
Metaprogramming in Ruby

Dynamic Method

# Decide how to define a method at runtime
class C
end

C.class_eval do
  define_method :my_method do
    'a dynamic method'
  end
end

obj = C.new
obj.my_method # => "a dynamic method"

Ghost Method

# powered by BasicObject#method_missing and BasicObject#respond_to_missing?
class Hello
  PEOPLE = %w[Abe Brian Chia].freeze

  def method_missing(name, *args, &block)
    person = name.to_s.capitalize

    # super if not one of the methods/names in const PEOPLE
    super unless PEOPLE.include? person

    "Hello #{person}!"
  end

  def respond_to_missing?(method, include_private = false)
    person = method.to_s.capitalize

    # super if not one of the methods/names in const PEOPLE
    PEOPLE.include?(person) || super
  end
end

hello = Hello.new
hello.brian # => "Hello Brian!"
hello.dave # NoMethodError: undefined method `dave' for #<Hello:...>

hello.respond_to? :brian # => true
hello.respond_to? :dave # => false

Blank Slate

# A skinny class with a minimal number of methods by inheriting directly
# from Basic Object
#
# Alternatives
#   Module#undef_method removes any method, including inherited ones
#   Module#remove_method removes method from receiver, not inherited ones
# 
# inherits from Object by default
class C
  def method_missing(_name, *_args)
    'a Ghost Method'
  end
end

# inherits from BasicObject explicitly to set a "blank slate"
# BasicObject.instance_methods # =>
# [:!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
class D < BasicObject
  def method_missing(_name, *_args)
    'a Ghost Method'
  end
end

obj = C.new
obj.to_s # => "#<C:0x007fbd9b025958>"

blank_slate = D.new # => "a Ghost Method"
blank_slate.to_s # =>  "a Ghost Method"

Kernel Method

# define a method in module Kernel to make the method available to *all* objects
module Kernel
  def a_method
    'a kernel method'
  end
end

a_method # => "a kernel method"

Context Probe

# Execute a block to access information in an object's context
class C
  def initialize
    @x = 'a private instance variable'
  end
end

obj = C.new
obj.instance_eval { @x } # => "a private instance variable"

Deferred Evaluation

# Store a piece of code and its context in a proc or lambda
# for evaluation later
class C
  def store(&block)
    @my_code_capsule = block
  end

  def execute
    @my_code_capsule.call
  end
end

obj = C.new
obj.store { $X = 1 }
$X = 0

obj.execute
$X # => 1

Reference: Blocks vs Procs vs Lambdas

# 1. Procs are objects, blocks are not
# 2. At most one block can appear in an argument list
# 3. Lambdas check the number of arguments, while procs do not
# 4. Lambdas and procs treat the 'return' keyword differently

# Converting a block to a proc
# Ruby provides standard library class Proc, to turn a block into an object
# Different ways to create a proc
# inc = Proc.new { |x| x + 1 } # same as preferred...
inc = proc { |x| x + 1 }
inc.class # => Proc
inc.call(1) # => 2

# dec = lambda { |x| x - 1 } # same as preferred with "stabby lambda"...
dec = ->(x) { x - 1 }
dec.class # => Proc
dec.call(1) # => 0

Singleton Method

# Define a method on a single object
obj = 'abc'

class << obj
  def my_singleton_method
    'x'
  end
end

obj.my_singleton_method

# Another example
str = 'any string'

def str.title?
  # self.upcase == self # self.upcase's self is redundant, so...
  upcase == self
end

str.title? # => false
str.methods.grep(/title?/) # => [:title?]
str.singleton_methods # => [:title?]

Class Macro

# Use a class method in a class definition
class C; end

class << C
  def my_macro(arg)
    "my_macro(#{arg} called"
  end
end

class C
  my_macro :x # => "my_macro(x) called"
end

# Simple

class Book
  def title
    # ...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :get_title, :title
end

Class Extension

# Use Object#extend as a shortcut to 
# include a module in a receiver's singleton class
#
# this will NOT work...
# module MyModule
#   def self.my_class_method; end
# end
#
# class MyClass
#   include MyModule
# end
# 
class C; end

module M
  # becomes a class method when included as an instance method
  # on the singleton class of class C
  def my_method
    'a class method'
  end
end

class << C
  include M
end
# or use "extend" to do the same...
class C
  extend M
end

C.my_method # => 'a class method'

Object Extension

module N
  def my_method
    'a singleton method'
  end
end

obj = Object.new

class << obj
  include N
end
# or use "extend" to do the same...
obj.extend N

obj.my_method # => 'a singleton method'

3 ways to wrap a method

Around Alias

# Call the previous, aliased version of a method from a redifined method.
class String
  # rubocop:disable Alias
  alias_method :old_reverse, :reverse
  # rubocop:enable Alias

  def reverse
    "x#{old_reverse}x"
  end
end

'abc'.reverse # => "xcbax"

Refinement Wrapper

# Call an unrefined method from its refinement
module StringRefinement
  refine String do
    def reverse
      "x#{super}x"
    end
  end
end

using StringRefinement
'abc'.reverse # => "xcbax"

Prepended Wrapper

# Call a method from its prepended override
module M
  def reverse
    "x#{super}x"
  end
end

String.class_eval do
  prepend M
end

'abc'.reverse # => "xcbax"

Code snippets from or inspired by Metaprogramming Ruby 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment