Last active
November 11, 2016 18:38
-
-
Save justinweiss/86af7c8265242d6c1f62e5ca61ac377b to your computer and use it in GitHub Desktop.
Sample code from my "Metaprogramming? Not Good Enough!" RubyConf 2016 talk.
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
# 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