Skip to content

Instantly share code, notes, and snippets.

@justinweiss
Last active November 11, 2016 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinweiss/86af7c8265242d6c1f62e5ca61ac377b to your computer and use it in GitHub Desktop.
Save justinweiss/86af7c8265242d6c1f62e5ca61ac377b to your computer and use it in GitHub Desktop.
Sample code from my "Metaprogramming? Not Good Enough!" RubyConf 2016 talk.
# We'll start by inheriting from a BasicObject, with our two properties: state and behavior.
class TinyObject < BasicObject
attr_accessor :state
attr_accessor :behavior
end
# `add_method`, for adding a method implementation to a behavior / class's state.
behavior_add_method = lambda do |behavior, method_name, method|
behavior.state[:methods][method_name] = method
end
# `lookup`, for finding a method from an inheritance chain.
behavior_lookup = lambda do |behavior, method_name|
# Look up a method by name in the object's methods list
method = behavior.state[:methods][method_name]
# If a method with that name exists, return it
# Otherwise, if you have a parent class...
if !method && behavior.state[:parent]
# call this method recursively, using the parent instead
method = behavior_lookup.call(behavior.state[:parent], method_name)
end
method
end
# `build_object`, for creating a new object with a specific behavior.
behavior_build_object = lambda do |behavior|
# Allocate an object
obj = TinyObject.new
obj.state = {}
# Set the object's behavior (or class)
# to the class that called this method
obj.behavior = behavior
# Return the object
obj
end
# `delegate`, for creating a subclass or sub-behavior.
behavior_delegate = lambda do |parent_class|
parent_class_behavior = parent_class && parent_class.behavior
subclass = behavior_build_object.call(parent_class_behavior)
# Set the subclass's parent to us
subclass.state[:parent] = parent_class
# Initialize the object's state with an empty set of methods
subclass.state[:methods] ||= {}
subclass
end
# Bootstrap the object model
# Create the default behavior, to hold the default versions of `add_method`, `lookup`, etc.
default_behavior = behavior_delegate.call(nil)
default_behavior.behavior = default_behavior
# Create our root Object class
root_object_class = behavior_delegate.call(nil)
root_object_class.behavior = default_behavior
# Point the parent of the default behavior to the Object class
default_behavior.state[:parent] = root_object_class
# Add default implementations of the core methods to the default behavior
behavior_add_method.call(default_behavior, "lookup", behavior_lookup)
behavior_add_method.call(default_behavior, "add_method", behavior_add_method)
behavior_add_method.call(default_behavior, "build_object", behavior_build_object)
behavior_add_method.call(default_behavior, "delegate", behavior_delegate)
# `object_send`, for finding and calling the correct method implementations (along with its helper, `find_method`)
find_method = nil
object_send = lambda do |object, method_name, *args|
method = find_method.call(object, method_name)
if method
method.call(object, *args)
else
raise "No method #{method_name} found on object"
end
end
find_method = lambda do |object, method_name|
if (object == default_behavior && method_name == "lookup")
behavior_lookup.call(default_behavior, "lookup")
else
object_send.call(object.behavior, "lookup", method_name)
end
end
# Now, for a simple example using this model!
# Subclass Object to create a `greeter_class`, and add a `hello` method to it:
greeter_class = object_send.call(root_object_class, "delegate")
object_send.call(greeter_class, "add_method", "hello", lambda { |_| puts "Hello, world!" })
# Build a `greeter` object, and call the `hello` method on it:
greeter = object_send.call(greeter_class, "build_object")
object_send.call(greeter, "hello") # => "Hello, world!"
# Use `method_missing` to clean the syntax up. This will translate
# unknown method calls on an object into `object_send` calls:
class TinyObject
def method_missing(name, *args)
object_send = nil
behavior = self.behavior
# Crawl our ancestors for an "object_send" implementation
while !object_send && behavior
object_send = behavior.state[:methods]["object_send"]
behavior = behavior.state[:parent]
end
# Call object_send on ourselves
object_send.call(self, name.to_s, *args)
end
end
behavior_add_method.call(root_object_class, "object_send", object_send)
# Let's try this out, with different state on the different objects,
# using a new `hello_name` method:
greeter_class.add_method("hello_name",
lambda { |object| puts "Hello, #{object.state[:name]}!" })
alice = greeter_class.build_object
bob = greeter_class.build_object
alice.state[:name] = "Alice"
bob.state[:name] = "Bob"
alice.hello_name # => "Hello, Alice!"
bob.hello_name # => "Hello, Bob!"
# ---------------------------------------
# Example: Create a new behavior for intercepting method calls:
# Subclass the default behavior, so we can override the `lookup` method
intercepting_behavior = default_behavior.delegate
# Define a new `lookup` method, that wraps methods with an `interceptor_method`
intercepting_behavior.add_method(
"lookup",
lambda do |sender, method_name|
# Get the old lookup method from the parent of our class's behavior
super_behavior = sender.behavior.state[:parent]
lookup = super_behavior.lookup("lookup")
# Call it to find the real method implementation
method = lookup.call(sender, method_name)
# Wrap the method and return it
if method
original_method = method
interceptor = sender.state[:interceptor_method]
method = lambda do |*args|
interceptor.call(method_name, original_method, *args)
end
end
method
end)
# Create a setter for interceptor methods.
intercepting_behavior.add_method(
"set_interceptor",
lambda do |behavior, interceptor_method|
behavior.state[:interceptor_method] = interceptor_method
end)
# Let's try it out, by creating a class that uses an interceptor function to log method calls:
person_class = root_object_class.delegate
person_class.behavior = intercepting_behavior
# Define an interceptor function to wrap method calls with some logging:
method_logger = lambda do |method_name, method, *args|
puts "> Calling #{method_name}... "
method.call(*args)
puts "> Done!"
end
person_class.set_interceptor(method_logger)
person_class.add_method "name", lambda { |_| puts "Justin" }
person_class.add_method "location", lambda { |_| puts "Cincinnati, OH" }
# And try it out:
person = person_class.build_object
person.name
# > Calling name...
# Justin
# > Done!
person.location
# > Calling location...
# Cincinnati, OH
# > Done!
# Let's write another method to retry flaky method calls, until they succeed!
person_class.add_method("flaky_method", lambda do |_|
if rand(3) == 0
puts "Success!"
true
else
false
end
end)
# Retry a method until it returns `true`:
retry_interceptor = lambda do |method_name, method, *args|
until method.call(*args)
puts "Method #{method_name} failed, retrying..."
sleep 0.5
end
end
person_class.set_interceptor(retry_interceptor)
person.flaky_method
# Method flaky_method failed, retrying...
# Method flaky_method failed, retrying...
# Method flaky_method failed, retrying...
# Method flaky_method failed, retrying...
# Success!
# ---------------------------------------
# Example: Prototype inheritance
# Once it's built, we can write code that looks like this:
# object = prototype_behavior.build_object
# object.set("a", 1)
#
# derived_object = object.extend
# derived_object.a # => 1
#
# derived_object.set("a", 2)
# derived_object.a # => 2
#
# The derived object pulls its properties from the parent object, but can override them and add new properties if it wants to.
# Subclass the default behavior, so we can override `object_send`, and add the new methods `set` and `extend`
prototype_behavior = default_behavior.delegate
# Define two methods:
# `set` sets a property value to an object's state.
prototype_behavior.add_method("set", lambda do |object, property_name, value|
object.state[property_name] = value
end)
# `extend` creates a new object that points to the original object
prototype_behavior.add_method("extend", lambda do |object|
child_object = prototype_behavior.build_object
child_object.state[:__prototype__] = object
child_object
end)
# Find a property, just like how `find_method` finds a method:
prototype_find_property = lambda do |object, property_name|
if value = object.state[property_name]
value
elsif prototype = object.state[:__prototype__]
prototype_find_property.call(prototype, property_name)
end
end
# Override `object_send` to look for properties as well as methods:
prototype_object_send = lambda do |object, name, *args|
value = prototype_find_property.call(object, name)
return value if value
method = find_method.call(object, name)
if method
method.call(object, *args)
else
raise "No method or property #{key} found on object"
end
end
# Override `object_send` with our new one
prototype_behavior.add_method("object_send", prototype_object_send)
# Try it out!
person = prototype_behavior.build_object
person.set("name", "Justin")
person.set("location", "Seattle, WA")
puts person.name # => "Justin"
clone = person.extend
clone.set("location", "Cincinnati, OH")
puts clone.name # => "Justin"
puts clone.location # => "Cincinnati, OH"
# ---------------------------------------
# Example: Multiple Inheritance
# As in, a class can have multiple parents. If it doesn't implement a method, it searches all of its parents for it.
# In code, that would look something like this:
# person_class = root_object_class.delegate
# person_class.add_method "name", lambda { |_| puts "Justin" }
# greeter_class = root_object_class.delegate
# greeter_class.add_method "greeting", lambda { |_| puts "Hello! Hello! Hello!" }
# greeter_person_class = person_class.delegate
# greeter_person_class.add_parent(greeter_class)
# justin = greeter_person_class.build_object
# puts justin.name # => "Justin"
# puts justin.greeting # => "Hello! Hello! Hello!"
# Subclass the default behavior, so we can override `lookup`
multiple_parents_behavior = default_behavior.delegate
# Write the `add_parent` method, to add a new parent to a class
multiple_parents_behavior.add_method("add_parent",
lambda do |behavior, new_parent|
# Initialize `parents` with the original parent
unless behavior.state[:parents]
behavior.state[:parents] = []
parent = behavior.state[:parent]
behavior.state[:parents] << parent if parent
end
# Append the new parent to `parents`
behavior.state[:parents] << new_parent
end
)
# Override `lookup`, to search through all of the object's parents
multiple_parents_behavior.add_method(
"lookup",
lambda do |behavior, method_name|
method = behavior.state[:methods][method_name]
parents = behavior.state[:parents] ||
[behavior.state[:parent]]
if !method && parents
parents.each do |parent|
break if method = parent.lookup(method_name)
end
end
method
end)
# Try it out!
# Create two normal classes
person_class = root_object_class.delegate
person_class.add_method "name", lambda { |_| puts "Justin" }
greeter_class = root_object_class.delegate
greeter_class.add_method "greeting", lambda { |_| puts "Hello! Hello! Hello!" }
# Inherit from both of them
social_person_class = person_class.delegate
social_person_class.behavior = multiple_parents_behavior
social_person_class.add_parent(greeter_class)
# The methods come from both objects!
justin = social_person_class.build_object
justin.name # => "Justin"
justin.greeting # => "Hello! Hello! Hello!"
# ---------------------------------------
# What else can you build? Some ideas:
# - Pattern Matching, like Elixir
# - Skip a generation -- if your method is missing, go to
# your grandparent instead of your parent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment