Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Metaprogramming? Not good enough!

Justin Weiss
November 12, 2016

Metaprogramming? Not good enough!

If you know how to metaprogram in Ruby, you can create methods and objects on the fly, build Domain Specific Languages, or just save yourself a lot of typing. But can you change how methods are dispatched? Can you decide that the normal inheritance rules don't apply to some object?

In order to change those core parts of the language, there can't be much difference between how a language is implemented and how it's used. In this talk, you'll make that difference smaller, building a totally extensible object model on top of Ruby, using less than a dozen new classes and methods.

Justin Weiss

November 12, 2016
Tweet

More Decks by Justin Weiss

Other Decks in Programming

Transcript

  1. → 2005: Ruby → 2006: JavaScript Ruby → 2007: Erlang

    Ruby → ... → 2015: Swift Ruby @justinweiss | #tinyobj
  2. Erlang = Actors Haskell = Monads Lisp = Parentheses Macros

    Ruby = Metaprogramming? @justinweiss | #tinyobj
  3. Object model The concepts, data structures, and methods you use

    to build things in your language @justinweiss | #tinyobj
  4. When you have a method name and arguments, which code

    do you use? @justinweiss | #tinyobj
  5. When you have a method name and arguments, which code

    do you use? @justinweiss | #tinyobj
  6. def lookup(object, method_name) # Look up a method by name

    in object's methods list # If it exists, return it # Otherwise, if you have a parent class, recurse end @justinweiss | #tinyobj
  7. Object = a thing Class = a thing that builds

    objects, holds methods, has a parent Behavior = a thing that holds methods like lookup and add_method @justinweiss | #tinyobj
  8. 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] # ... method end @justinweiss | #tinyobj
  9. behavior_lookup = lambda do |behavior, method_name| # ... # If

    you can't find it, but 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 @justinweiss | #tinyobj
  10. 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 you can't find it, but 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 @justinweiss | #tinyobj
  11. behavior_build_object = lambda do |behavior| # Allocate an object obj

    = TinyObject.new obj.state = {} # set the object's class / behavior # to yourself (the class that called this method) obj.behavior = behavior # return the object obj end @justinweiss | #tinyobj
  12. # set the object's parent (in its state) to ourselves

    subclass.state[:parent] = parent_class # initialize the object's state with an empty set of methods subclass.state[:methods] ||= {} subclass @justinweiss | #tinyobj
  13. behavior_delegate = lambda do |parent_class| parent_class_behavior = parent_class && parent_class.behavior

    subclass = behavior_build_object.call(parent_class_behavior) # set the object's parent (in its state) to ourselves subclass.state[:parent] = parent_class # initialize the object's state with an empty set of methods subclass.state[:methods] ||= {} subclass end @justinweiss | #tinyobj
  14. TinyObject: Our object structure, containing behavior and state behavior_add_method: Add

    a method to a class behavior_lookup: Find an implementation, from a method name behavior_build_object: Like .new, build an object from a class / behavior behavior_delegate: Inherit from a class / behavior @justinweiss | #tinyobj
  15. 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 @justinweiss | #tinyobj
  16. 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 @justinweiss | #tinyobj
  17. behavior_add_method: Add a method to a class behavior_lookup: Find an

    implementation, from a method name behavior_build_object: Like .new, build an object from a class / behavior behavior_delegate: Inherit from a class / behavior object_send: Call a method, by name, on an object @justinweiss | #tinyobj
  18. class TinyObject < BasicObject attr_accessor :state attr_accessor :behavior 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 @justinweiss | #tinyobj
  19. alice = greeter_class.build_object bob = greeter_class.build_object alice.state[:name] = "Alice" bob.state[:name]

    = "Bob" alice.hello_name # => bob.hello_name # => @justinweiss | #tinyobj
  20. 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!" @justinweiss | #tinyobj
  21. def lookup # Get the old lookup method from the

    parent of our behavior # Call it to find the real method implementation # Wrap the method and return it end @justinweiss | #tinyobj
  22. method_logger = lambda do |method_name, method, *args| puts "> Calling

    #{method_name}... " method.call(*args) puts "> Done!" end @justinweiss | #tinyobj
  23. intercepting_behavior.add_method( "lookup", lambda do |sender, method_name| # Get the old

    lookup method from the parent of our class's behavior lookup = object_send.call(sender.behavior.state[:parent], "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 { |*args| interceptor.call(method_name, original_method, *args) } end method end ) @justinweiss | #tinyobj
  24. # Get the old lookup method # from the parent

    of our class's behavior super_behavior = ? lookup = super_behavior.lookup("lookup") @justinweiss | #tinyobj
  25. # 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 lookup to find the real method implementation method = lookup.call(sender, method_name) @justinweiss | #tinyobj
  26. # 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 @justinweiss | #tinyobj
  27. method_logger = lambda do |method_name, method, *args| puts "> Calling

    #{method_name}... " method.call(*args) puts "> Done!" end person_class.state[:interceptor_method] = method_logger @justinweiss | #tinyobj
  28. person = person_class.build_object person.name # > Calling name... # Justin

    # > Done! person.location # > Calling location... # Cincinnati, OH # > Done! @justinweiss | #tinyobj
  29. retry_interceptor = lambda do |method_name, method, *args| until method.call(*args) puts

    "Method #{method_name} failed, retrying..." sleep 0.5 end end @justinweiss | #tinyobj
  30. person_class.state[:interceptor_method] = retry_interceptor person.flaky_method # Method flaky_method failed, retrying... #

    Method flaky_method failed, retrying... # Method flaky_method failed, retrying... @justinweiss | #tinyobj
  31. person_class.state[:interceptor_method] = 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... @justinweiss | #tinyobj
  32. person_class.state[:interceptor_method] = 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! @justinweiss | #tinyobj
  33. 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 ) @justinweiss | #tinyobj
  34. 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 ) @justinweiss | #tinyobj
  35. 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| # v---- -----^ break if method = parent.lookup(method_name) end end method end ) @justinweiss | #tinyobj
  36. 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!" } @justinweiss | #tinyobj
  37. symbols.each do |symbol| klass = Class.new(ActiveRecord::Base) do def method_missing(name, *args)

    # ... end end Object.const_set(symbol.to_s.capitalize, klass) end @justinweiss | #tinyobj
  38. The Ruby logo is Copyright © 2006, Yukihiro Matsumoto. It

    is licensed under the terms of the Creative Commons Attribution- ShareAlike 2.5 License agreement. "Happy Programmers" by Jesper Rønn-Jensen, used under CC BY- SA 2.0 / Resized from original "Laptop Stickers" by Nate Angell, used under CC BY 2.0 / Resized from original "No Brain" by Pierre-Olivier Carles, used under CC BY 2.0 @justinweiss | #tinyobj