- 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
Last active
October 7, 2024 02:15
-
-
Save Integralist/a29212a8eb10bc8154b7 to your computer and use it in GitHub Desktop.
Ruby Meta Programming
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Dynamic Dispatch | |
# Allows us to send messages to even private methods | |
# object.send(message, *arguments) | |
1.send(:+, 2) # => 3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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!" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type = "baz" | |
Foo::Bar.const_get(type.capitalize).new # => new instance of Foo::Bar::Baz |
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 ;)
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
"Dynamic dispatch" doesn't mean what you think it does.
http://en.wikipedia.org/wiki/Dynamic_dispatch