Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active October 7, 2024 02:15
Show Gist options
  • Save Integralist/a29212a8eb10bc8154b7 to your computer and use it in GitHub Desktop.
Save Integralist/a29212a8eb10bc8154b7 to your computer and use it in GitHub Desktop.
Ruby Meta Programming
  • Dynamic Dispatch
  • Dynamic Method
  • Ghost Methods
  • Dynamic Proxies
  • Blank Slate
  • Kernel Method
  • Flattening the Scope (aka Nested Lexical Scopes)
  • Context Probe
  • Class Eval (not really a 'spell' more just a demonstration of its usage)
  • Class Macros
  • Around Alias
  • Hook Methods
  • Class Extension Mixin
  • Module Namespace Interpolation
# Dynamic Dispatch
# Allows us to send messages to even private methods
# object.send(message, *arguments)
1.send(:+, 2) # => 3
# Dynamic Method
# Allows us to dynamically create methods
# define_method :method_name { block that becomes method body }
class Foo
define_method :bar do
puts "This is a dynamic method"
end
end
Foo.new.bar # => "This is a dynamic method"
# Dynamic Method
# Alternative example
class Bar
# we have to define this method on `self` (see below comment)
def self.create_method(method)
define_method "my_#{method}" do
puts "Dynamic method called 'my_#{method}'"
end
end
# these methods are executed within the definition of the Bar class
create_method :foo
create_method :bar
create_method :baz
end
Bar.new.my_foo # => "Dynamic method called 'my_foo'"
Bar.new.my_bar # => "Dynamic method called 'my_bar'"
Bar.new.my_baz # => "Dynamic method called 'my_baz'"
# Dynamic Method
# Parse another class for data
class AnotherClass
def get_foo_stuff; end
def get_bar_stuff; end
def get_baz_stuff; end
end
class Baz
def initialize(a_class)
a_class.methods.grep(/^get_(.*)_stuff$/) { Baz.create_method $1 }
end
def self.create_method(method)
define_method "my_#{method}" do
puts "Dynamic method called 'my_#{method}'"
end
end
end
another_class = AnotherClass.new
Baz.new(another_class).my_foo # => "Dynamic method called 'my_foo'"
Baz.new(another_class).my_bar # => "Dynamic method called 'my_bar'"
Baz.new(another_class).my_baz # => "Dynamic method called 'my_baz'"
class Foo
def initialize(bar)
self.class.send(:define_method, bar) { p "hello #{bar}!" }
end
end
foo = Foo.new("world")
foo.world # => "hello world!"
# Ghost Methods
# Utilises `method_missing`
class Hai
def method_missing(method, *args)
puts "You called: #{method}(#{args.join(', ')})"
puts "You also passed a block" if block_given?
end
end
Hai.new.yolo # => You called: yolo()
Hai.new.yolo "a", 123, :c # => You called: yolo(a, 123, c)
Hai.new.yolo(:a, :b, :c) { puts "a block" } # => You called: yolo(a, b, c)
# => You also passed a block
# Dynamic Proxies
# Catching "Ghost Methods" and forwarding them onto another method
# Whilst possibly adding logic around the call.
#
# For example,
# You can provide imaginary methods by utilising `method_missing` to parse
# the incoming message (e.g. `get_name`, `get_age`) and to delegate off to
# another method such as `get(:data_type)` where `:data_type` is `:name` or `:age`.
def method_missing(message, *args, &block)
return get($1.to_sym, *args, &block) if message.to_s =~ /^get_(.*)/
super # if we don't find a match then we'll call the top level `BasicObject#method_missing`
end
# If (after analysis) you discover a performance issue with using `method_missing`
# you can utilise the "Dynamic Method" technique to create a real method after
# the message has been received by `method_missing` the first time.
# Blank Slate
# Prevents issues when using "Dynamic Proxies"
#
# e.g. user calls a method that exists higher up the inheritance chain
# so your `method_missing` doesn't fire because the method does exist.
#
# To work around this issue, make sure your class starts with a "Blank Slate"
# So you remove any methods you don't want to appear at all in the inheritance chain
# by using `undef_method` (there is also `remove_method` which doesn't remove the named
# method from the inheritance chain but just the current class, but that doesn't help us
# fix the "Dynamic Proxy" scenario so we use `undef_method` instead).
#
# For "Dynamic Proxy" we use the parent `method_missing` so we keep that,
# we also might use `respond_to?` so we keep that (although you can remove it if you don't).
# Also the `__` in the below regex pattern is to prevent Ruby from displaying a warning
# about removing 'reserved' methods such as `__id__` and `__send__`
class ImBlank
instance_methods.each do |m|
undef_method m unless m.to_s =~ /^__|method_missing|respond_to?/
end
# rest of your code (such as your "Dynamic Proxy" implementation)
end
# Kernel Method
# Add a method that gives the illusion it's a language keyword
# But really it's just added to the `Kernel` module which all other objects inherit from.
# At the top level of a Ruby program `self` is == `main`.
# `self.class` == `Object` and the `Kernel` sits above it in the hierarchy.
# You can see this by running the following code:
class Foo; end
Foo.ancestors # => [Foo, Object, Kernel, BasicObject]
# So we can see we can add what looks to be a language provided feature like so:
module Kernel
def foobar
puts "I'm not a language keyword, I'm just a fake"
end
end
# Now from any where in our program we can call
foobar # => I'm not a language keyword, I'm just a fake
# Flattening the Scope (aka Nested Lexical Scopes)
# Where you change the code in such a way that it's easier for you to pass variables through "Scope Gates".
# A scope gate is any block, where normally when you enter its scope the variables outside of it become unreachable.
# This happens in: Class definitions, Module definitions, Method definitions
# I'm not sure what the real life examples are of this, but if you ever wonder why some code does the following,
# then maybe it was that they wanted to flatten the scope so they could more easily pass around variables.
# I guess it's better to do it this way than to define a global variable?
#
# In the following code we want to access `my_var` from inside the method (inner scope gate) that's
# inside the class (outer scope gate).
my_var = "abc"
class OuterScopeGate
puts my_var
def inner_scope_gate
puts my_var
end
end
# We fix this by flattening the code into method calls (method *calls* aren't scope gates)
# So we turn the class keyword into a method call using `Class.new`
# We also turn the method inside the class from a keyword into a method call using `define_method`
my_var = "abc"
MyClass = Class.new do
puts "Here is 'my_var' inside my class definition: #{my_var}"
define_method :my_method do
puts "Here is 'my_var' inside my class instance method: #{my_var}"
end
end # => Here is 'my_var' inside my class definition: abc
MyClass.new.my_method # => Here is 'my_var' inside my class instance method: abc
# Context Probe
# Execute a code block in the context of another object using `instance_eval`
class Foo
def initialize
@z = 1
end
end
foo = Foo.new
foo.instance_eval do
puts self # => #<Foo:0x7d15e891>
puts @z # => 1
end
new_value = 2
foo.instance_eval { @z = new_value }
foo.instance_eval { puts @z } # => 2
# There is also `instance_exec` which works the same way but allows passing arguments to the block
class Foo
def initialize
@x, @y = 1, 2
end
end
Foo.new.instance_exec(3) { |arg| (@x + @y) * arg } # => 9
# Evaluate a block in the context of a class
# Similar to re-opening a class but more flexible in that it
# works on any variable that references a class, where as re-opening
# a class requires defining a constant.
# Classic class re-opening style
class String
def m; puts "hello!" end
end
# Class eval style
# The extra code is used to make the example a bit more re-usable/abstracted
def add_method_to_class(the_class)
the_class.class_eval do
def m; puts "hello!" end
end
end
add_method_to_class String
"abc".m # => hello!
# Class Macros are just regular class methods that are only used in a class definition
# e.g. not used from a new instance of the class (only at the time the class is defined)
#
# Below is an example of a Class Macro that alerts users of a publically available class
# that the methods they've been using are now deprecated and they should use the renamed version.
#
# It uses "Dynamic Method" to help performance by creating the old methods again and delegating off
# to the new methods (rather than using `method_missing` which can be quite slow as it has to spend
# time looking up the inheritance chain)
class Foo
def get_a; puts "I'm an A" end
def get_b; puts "I'm an B" end
def get_c; puts "I'm an C" end
# Defining our Class Macro
def self.deprecate(old_method, new_method)
define_method(old_method) do |*args, &block|
puts "Warning: #{old_method} is deprecated! Use #{new_method} instead"
send(new_method, *args, &block) # `self` is assumed when calling `send`
end
end
# Using our Class Macro
deprecate :a, :get_a
deprecate :b, :get_b
deprecate :c, :get_c
end
# Around Alias uses the `alias` keyword to store a copy of the original method under a new name,
# allowing you to redefine the original method name and to delegate off to the previous method implementation
class String
alias :orig_length :length
def length
"Length of string '#{self}' is: #{orig_length}"
end
end
"abc".length
#=> "Length of string 'abc' is: 3"
# Hook Methods are provided by the Ruby language and let you know about certain events
# such as when a class inherits from another class or when a method has been added to an object.
class String
  def self.inherited(subclass)
    puts "#{self} was inherited by #{subclass}"
  end
end
class MyString < String; end # => String was inherited by MyString

There are quite a few hooks which I've listed below.

Method-related hooks

method_missing
method_added
singleton_method_added
method_removed
singleton_method_removed
method_undefined
singleton_method_undefined

Class & Module Hooks

inherited
append_features
included
extend_object
extended
initialize_copy
const_missing

Marshalling Hooks

marshal_dump
marshal_load

Coercion Hooks

coerce
induced_from
to_xxx
# Class Extension Mixin allows you to both `include` and `extend` a class
module MyMixin
def self.included(base) # Hook Method
base.extend(ClassMethods)
end
def a
puts "I'm A (an instance method)"
end
module ClassMethods # "ClassMethods" is a recognised naming pattern
def x
puts "I'm X (a class method)"
end
end
end
class Foo
include MyMixin
end
Foo.x # => I'm X (a class method)
Foo.new.a # => I'm A (an instance method)
type = "baz"
Foo::Bar.const_get(type.capitalize).new # => new instance of Foo::Bar::Baz
@apchamberlain
Copy link

"Dynamic dispatch" doesn't mean what you think it does.

http://en.wikipedia.org/wiki/Dynamic_dispatch

@uzbekjon
Copy link

Ruby 2.0 also introduces prepend hook method that behaves similar to include method, but inserts your module before your class in its inheritance tree. I explained it in my "Ruby Metaprogramming" video course in more details.

BTW, I also applied $50 OFF coupon to the link above as a give back to GitHub community ;)

@cesartalves
Copy link

cesartalves commented Sep 11, 2019

This is great!

I was doing a list of my own when I stumbled on this doing a web search. Very useful.
Here's my humble little repo: https://github.com/cesartalves/ruby_metaprogramming_as_spec
I used RSpec to generated a doc-like structure, so I could reference it whenever I was in doubt about anything.

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