Metaprogramming? Not good enough!

Ac54c2b179cd4c54305846de2cb22ba1?s=47 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.

Ac54c2b179cd4c54305846de2cb22ba1?s=128

Justin Weiss

November 12, 2016
Tweet

Transcript

  1. Metaprogramming? Not good enough! Justin Weiss @justinweiss | #tinyobj

  2. Metaprogramming? Not good enough! Justin Weiss @justinweiss | #tinyobj

  3. @justinweiss | #tinyobj

  4. @justinweiss | #tinyobj

  5. @justinweiss | #tinyobj

  6. @justinweiss | #tinyobj

  7. → 2005: Ruby @justinweiss | #tinyobj

  8. → 2005: Ruby → 2006: JavaScript Ruby @justinweiss | #tinyobj

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

    Ruby @justinweiss | #tinyobj
  10. → 2005: Ruby → 2006: JavaScript Ruby → 2007: Erlang

    Ruby → ... → 2015: Swift Ruby @justinweiss | #tinyobj
  11. What makes Ruby, Ruby? @justinweiss | #tinyobj

  12. @justinweiss | #tinyobj

  13. Erlang = Actors @justinweiss | #tinyobj

  14. Erlang = Actors Haskell = Monads @justinweiss | #tinyobj

  15. Erlang = Actors Haskell = Monads Lisp = Parentheses @justinweiss

    | #tinyobj
  16. Erlang = Actors Haskell = Monads Lisp = Parentheses Macros

    @justinweiss | #tinyobj
  17. Erlang = Actors Haskell = Monads Lisp = Parentheses Macros

    Ruby = Metaprogramming? @justinweiss | #tinyobj
  18. @justinweiss | #tinyobj

  19. Metaprogramming → !? @justinweiss | #tinyobj

  20. @justinweiss | #tinyobj

  21. @justinweiss | #tinyobj

  22. @justinweiss | #tinyobj

  23. @justinweiss | #tinyobj

  24. @justinweiss | #tinyobj

  25. A brand new object model (on top of Ruby) @justinweiss

    | #tinyobj
  26. Object model The concepts, data structures, and methods you use

    to build things in your language @justinweiss | #tinyobj
  27. @justinweiss | #tinyobj

  28. I. Piumarta and A. Warth Open, Extensible Object Models http://piumarta.com/software/cola/

    objmodel2.pdf @justinweiss | #tinyobj
  29. When you have a method name and arguments, which code

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

    do you use? @justinweiss | #tinyobj
  31. 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
  32. @justinweiss | #tinyobj

  33. @justinweiss | #tinyobj

  34. What is behavior? @justinweiss | #tinyobj

  35. @justinweiss | #tinyobj

  36. @justinweiss | #tinyobj

  37. @justinweiss | #tinyobj

  38. @justinweiss | #tinyobj

  39. @justinweiss | #tinyobj

  40. @justinweiss | #tinyobj

  41. @justinweiss | #tinyobj

  42. 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
  43. Object add_method lookup @justinweiss | #tinyobj

  44. How do we get an object from a class? @justinweiss

    | #tinyobj
  45. Object add_method lookup build_object @justinweiss | #tinyobj

  46. What about new classes, sub-classes, or sub- behaviors? @justinweiss |

    #tinyobj
  47. Object add_method lookup build_object delegate @justinweiss | #tinyobj

  48. How do you call a method? @justinweiss | #tinyobj

  49. Object add_method lookup build_object delegate send @justinweiss | #tinyobj

  50. Let's build it in Ruby! @justinweiss | #tinyobj

  51. class TinyObject < BasicObject attr_accessor :state attr_accessor :behavior end @justinweiss

    | #tinyobj
  52. class TinyObject < BasicObject attr_accessor :state attr_accessor :behavior end @justinweiss

    | #tinyobj
  53. Create the core methods @justinweiss | #tinyobj

  54. behavior_add_method = lambda do |behavior, method_name, method| behavior.state[:methods][method_name] = method

    end @justinweiss | #tinyobj
  55. behavior_add_method = lambda do |behavior, method_name, method| # -------^ behavior.state[:methods][method_name]

    = method end @justinweiss | #tinyobj
  56. behavior_lookup = lambda do |behavior, method_name| # ... end @justinweiss

    | #tinyobj
  57. 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
  58. 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
  59. 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
  60. 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
  61. behavior_delegate = lambda do |parent_class| # ... end @justinweiss |

    #tinyobj
  62. @justinweiss | #tinyobj

  63. subclass = behavior_build_object.call(parent_class.behavior) @justinweiss | #tinyobj

  64. # 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
  65. 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
  66. 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
  67. @justinweiss | #tinyobj

  68. default_behavior = behavior_delegate.call(nil) # ... @justinweiss | #tinyobj

  69. default_behavior = behavior_delegate.call(nil) default_behavior.behavior = default_behavior @justinweiss | #tinyobj

  70. root_object_class = behavior_delegate.call(nil) root_object_class.behavior = default_behavior @justinweiss | #tinyobj

  71. root_object_class = behavior_delegate.call(nil) root_object_class.behavior = default_behavior @justinweiss | #tinyobj

  72. # ... default_behavior.state[:parent] = root_object_class @justinweiss | #tinyobj

  73. default_behavior = behavior_delegate.call(nil) default_behavior.behavior = default_behavior root_object_class = behavior_delegate.call(nil) root_object_class.behavior

    = default_behavior default_behavior.state[:parent] = root_object_class @justinweiss | #tinyobj
  74. 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) @justinweiss | #tinyobj
  75. @justinweiss | #tinyobj

  76. @justinweiss | #tinyobj

  77. 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
  78. find_method = lambda do |object, method_name| object_send.call(object.behavior, "lookup", method_name) end

    @justinweiss | #tinyobj
  79. 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
  80. 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
  81. greeter_class = object_send.call(root_object_class, "delegate") @justinweiss | #tinyobj

  82. greeter_class = object_send.call(root_object_class, "delegate") object_send.call(greeter_class, "add_method", "hello", lambda { |object|

    puts "Hello, world!" }) @justinweiss | #tinyobj
  83. greeter = object_send.call(greeter_class, "build_object") object_send.call(greeter, "hello") # => @justinweiss |

    #tinyobj
  84. greeter = object_send.call(greeter_class, "build_object") object_send.call(greeter, "hello") # => "Hello, world!"

    @justinweiss | #tinyobj
  85. @justinweiss | #tinyobj

  86. greeter = object_send.call(greeter_class, "build_object") object_send.call(greeter, "hello") # => "Hello, world!"

    @justinweiss | #tinyobj
  87. @justinweiss | #tinyobj

  88. 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
  89. object_send.call(greeter, "hello") @justinweiss | #tinyobj

  90. object_send.call(greeter, "hello") ⬇ greeter.hello @justinweiss | #tinyobj

  91. behavior_add_method.call(root_object_class, "object_send", object_send) @justinweiss | #tinyobj

  92. greeter_class.add_method("hello_name", lambda { |object| puts "Hello, #{object.state[:name]}!" }) # ----^

    @justinweiss | #tinyobj
  93. 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
  94. 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
  95. Now what? @justinweiss | #tinyobj

  96. Log method calls! @justinweiss | #tinyobj

  97. 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
  98. method_logger = lambda do |method_name, method, *args| puts "> Calling

    #{method_name}... " method.call(*args) puts "> Done!" end @justinweiss | #tinyobj
  99. @justinweiss | #tinyobj

  100. intercepting_behavior = default_behavior.delegate @justinweiss | #tinyobj

  101. 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
  102. # Get the old lookup method # from the parent

    of our class's behavior super_behavior = ? lookup = super_behavior.lookup("lookup") @justinweiss | #tinyobj
  103. super_behavior = sender.behavior.state[:parent] lookup = super_behavior.lookup("lookup") @justinweiss | #tinyobj

  104. # 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
  105. # 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
  106. @justinweiss | #tinyobj

  107. person_class = root_object_class.delegate # ... @justinweiss | #tinyobj

  108. person_class = root_object_class.delegate person_class.behavior = intercepting_behavior @justinweiss | #tinyobj

  109. person_class = root_object_class.delegate # ... @justinweiss | #tinyobj

  110. person_class = root_object_class.delegate person_class.behavior = intercepting_behavior @justinweiss | #tinyobj

  111. 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
  112. person_class.add_method "name", lambda { |_| puts "Justin" } person_class.add_method "location",

    lambda { |_| puts "Cincinnati, OH" } @justinweiss | #tinyobj
  113. person = person_class.build_object person.name # # # person.location # #

    # @justinweiss | #tinyobj
  114. person = person_class.build_object person.name # > Calling name... # Justin

    # > Done! person.location # > Calling location... # Cincinnati, OH # > Done! @justinweiss | #tinyobj
  115. @justinweiss | #tinyobj

  116. person_class.behavior = default_behavior person.name # @justinweiss | #tinyobj

  117. person_class.behavior = default_behavior person.name # Justin @justinweiss | #tinyobj

  118. Retry method calls! @justinweiss | #tinyobj

  119. person_class.add_method("flaky_method", lambda do |_| if rand(3) == 0 puts "Success!"

    true else false end end) @justinweiss | #tinyobj
  120. 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
  121. person_class.state[:interceptor_method] = retry_interceptor person.flaky_method @justinweiss | #tinyobj

  122. person_class.state[:interceptor_method] = retry_interceptor person.flaky_method # Method flaky_method failed, retrying... @justinweiss

    | #tinyobj
  123. person_class.state[:interceptor_method] = retry_interceptor person.flaky_method # Method flaky_method failed, retrying... #

    Method flaky_method failed, retrying... @justinweiss | #tinyobj
  124. 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
  125. 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
  126. 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
  127. @justinweiss | #tinyobj

  128. Multiple Inheritance @justinweiss | #tinyobj

  129. @justinweiss | #tinyobj

  130. @justinweiss | #tinyobj

  131. @justinweiss | #tinyobj

  132. @justinweiss | #tinyobj

  133. multiple_parents_behavior = default_behavior.delegate @justinweiss | #tinyobj

  134. 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
  135. 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
  136. 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
  137. 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
  138. social_person_class = person_class.delegate social_person_class.behavior = multiple_parents_behavior social_person_class.add_parent(greeter_class) @justinweiss | #tinyobj

  139. justin = social_person_class.build_object justin.name # => justin.greeting # => @justinweiss

    | #tinyobj
  140. justin = social_person_class.build_object justin.name # => "Justin" justin.greeting # =>

    "Hello! Hello! Hello!" @justinweiss | #tinyobj
  141. → Single inheritance → Interception → Multiple Inheritance @justinweiss |

    #tinyobj
  142. → Object → add_method → lookup → build_object → delegate

    → send @justinweiss | #tinyobj
  143. Modules? method_missing? BasicObject? delegation? @justinweiss | #tinyobj

  144. Intent @justinweiss | #tinyobj

  145. What's normal? @justinweiss | #tinyobj

  146. @justinweiss | #tinyobj

  147. @justinweiss | #tinyobj

  148. What? Why!? @justinweiss | #tinyobj

  149. @justinweiss | #tinyobj

  150. The best way to understand a system is to break

    it. @justinweiss | #tinyobj
  151. 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
  152. @justinweiss | #tinyobj

  153. @justinweiss | #tinyobj

  154. Try it out @justinweiss | #tinyobj

  155. Thank you! Justin Weiss @justinweiss https://www.avvo.com justin@justinweiss.com https://www.justinweiss.com/ rubyconf-2016 @justinweiss

    | #tinyobj
  156. Thank you! Justin Weiss @justinweiss https://www.avvo.com justin@justinweiss.com https://www.justinweiss.com/ rubyconf-2016 @justinweiss

    | #tinyobj
  157. 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