Metaprogramming in Ruby
Upcoming SlideShare
Loading in...5
×
 

Like this? Share it with your network

Share

Metaprogramming in Ruby

on

  • 2,566 views

 

Statistics

Views

Total Views
2,566
Views on SlideShare
2,566
Embed Views
0

Actions

Likes
6
Downloads
64
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Metaprogramming in Ruby Presentation Transcript

  • 1. Metaprogramming in Ruby
  • 2. Things I Wish I KnewWhen I Started Ruby
  • 3. Who?Joshua Hull@jjhttps://github.com/joshbuddy
  • 4. What?
  • 5. What?Writing programs that write programs.
  • 6. What?Writing programs that write programs. NOT code generation!
  • 7. Why?We all do it.
  • 8. Why? We all do it.Ruby attr_accessor :my_attribute
  • 9. Why? We all do it.Ruby attr_accessor :my_attribute def my_attribute @my_attribute end def my_attribute=(my_attribute) @my_attribute = my_attribute end
  • 10. Why? We all do it.Ruby attr_accessor :my_attributeJava public int getAttribute() { return attribute; } public void setAttribute(int newAttribute) { attribute = newAttribute; }
  • 11. Why? We all do it. ner in RubyW attr_accessor :my_attribute Java public int getAttribute() { return attribute; } public void setAttribute(int newAttribute) { attribute = newAttribute; }
  • 12. Why?def age @age || not setenddef gender @gender || not setenddef name @name || not setend
  • 13. Why? def age @age || not set end def gender @gender || not set end def name @name || not set end[:age, :gender, :name].each do |attr| define_method(attr) do instance_variable_get(:"@#{attr}") || not set endend
  • 14. Drawbacks
  • 15. DrawbacksYou can write some difficult-to-understand code.
  • 16. DrawbacksYou can write some difficult-to-understand code.
  • 17. Method dispatch
  • 18. Method dispatch class MyObject attr_accessor :name def say_hello puts "hello" puts name end end
  • 19. Method dispatch class MyObject attr_accessor :name def say_hello puts "hello" puts name end end This is a method call, but who receives it?
  • 20. Method dispatch class MyObject attr_accessor :name def say_hello puts "hello" puts self.name end end
  • 21. Method dispatch class MyObject attr_accessor :name def say_hello puts "hello" puts self.name end end self is always the implied receiver!
  • 22. Method dispatch class MyObject attr_accessor :name def say_hello puts "hello" puts name end end
  • 23. Method dispatch class MyObject self.attr_accessor :name def say_hello puts "hello" puts name end end
  • 24. Method dispatch class MyObjectWho is self self.attr_accessor :name here? def say_hello puts "hello" puts name end end
  • 25. Method dispatch class MyObjectWho is self self.attr_accessor :name here? def say_hello puts "hello"The class puts name end is! end
  • 26. Method dispatch Module#attrWho is self here?The class is!
  • 27. Ruby classes
  • 28. Ruby classesclass NewClass def hey puts hello! endend
  • 29. Ruby classes class NewClass { def hey puts hello! end endThis is normal code!
  • 30. Ruby classesclass NewClass def hey puts hello! endend is the same asNewClass = Class.new do def hey puts hello! endend
  • 31. Ruby classesclass ParsingError < RuntimeErrorend
  • 32. Ruby classesclass ParsingError < RuntimeErrorend is the same asParsingError = Class.new(RuntimeError)
  • 33. Ruby classesdef class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend
  • 34. Ruby classesdef class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend Returns a new class!
  • 35. Ruby classesdef class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend Returns a new class!class Person < class_with_accessors(:name, :age, :sex) # ...end
  • 36. eval, instance_eval, class_eval
  • 37. eval, instance_eval, class_evalMethodevalinstance_evalclass_eval
  • 38. eval, instance_eval, class_evalMethod Contexteval your current contextinstance_eval the objectclass_eval the object’s class
  • 39. eval, instance_eval, class_evalevaleval "puts hello"# hello
  • 40. eval, instance_eval, class_evalinstance_evalclass MyClassendMyClass.instance_eval("def hi; hi; end")
  • 41. eval, instance_eval, class_evalinstance_evalclass MyClassendMyClass.instance_eval("def hi; hi; end")MyClass.hi# hi
  • 42. eval, instance_eval, class_evalinstance_evalclass MyClassendMyClass.instance_eval("def hi; hi end")obj = MyClass.new# <MyClass:0x10178aff8>obj.hi# NoMethodError: undefined method `hi for #<MyClass>
  • 43. eval, instance_eval, class_evalclass_evalclass MyClassendMyClass.class_eval("def hi; hi end")
  • 44. eval, instance_eval, class_evalclass_evalclass MyClassendMyClass.class_eval("def hi; hi end")MyClass.hi# NoMethodError: undefined method `hi for MyClass:Class
  • 45. eval, instance_eval, class_evalclass_evalclass MyClassendMyClass.class_eval("def hi; hi end")obj = MyClass.new# <MyClass:0x101849688>obj.hi# "hi"
  • 46. eval, instance_eval, class_evalclass_eval instance_evalclass MyClass class MyClassend endMyClass.class_eval("def hi; hi; end") MyClass.instance_eval("def hi; hi; end")obj = MyClass.new MyClass.hi# <MyClass:0x101849688> # hiobj.hi# "hi"
  • 47. eval, instance_eval, class_evalNinja’s will attack you if ...you don’t use __FILE__, __LINE__
  • 48. eval, instance_eval, class_evalclass HiThereendHiThere.class_eval "def hi; raise; end"HiThere.class_eval "def hi_with_niceness; raise; end", __FILE__, __LINE__HiThere.new.hiHiThere.new.hi_with_niceness
  • 49. eval, instance_eval, class_evalclass HiThereendHiThere.class_eval "def hi; raise; end"HiThere.class_eval "def hi_with_niceness; raise; end", __FILE__, __LINE__HiThere.new.hiHiThere.new.hi_with_niceness(eval):1:in `hi: unhandled exception from my_file.rb:7my_file.rb:5:in `hi_with_niceness: unhandled exception from my_file.rb:7
  • 50. eval, instance_eval, class_evalclass HiThereendHiThere.class_eval "def hi; raise; end"HiThere.class_eval "def hi_with_niceness; raise; end", __FILE__, __LINE__HiThere.new.hiHiThere.new.hi_with_niceness(eval):1:in `hi: unhandled exception from my_file.rb:7 So nice. <3my_file.rb:5:in `hi_with_niceness: unhandled exception from my_file.rb:7
  • 51. eval, instance_eval, class_eval Implement attr_accessor!
  • 52. eval, instance_eval, class_evalclass Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endend
  • 53. eval, instance_eval, class_evalclass Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endendclass M create_attr :hiend
  • 54. eval, instance_eval, class_evalclass Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endend def Mclass M def hi create_attr :hi @hiend end end
  • 55. Defining methods
  • 56. Defining methods For an objecto = Object.newo.instance_eval("def just_this_object; end")o.just_this_object
  • 57. Defining methods For an objecto = Object.newo.instance_eval("def just_this_object; end")o.just_this_objectObject.new.just_this_object# NoMethodError: undefined method `just_this_object
  • 58. Defining methods For an objecto = Object.newo.instance_eval("def just_this_object; end")o.just_this_objectObject.new.just_this_object# NoMethodError: undefined method `just_this_objecto = Object.newo.instance_eval { def just_this_object end}
  • 59. Defining methods For an object o = Object.new o.extend(Module.new { def just_this_object end })
  • 60. Defining methods For a classMyClass = Class.newclass MyClass def new_method endendMyClass.new.respond_to?(:new_method) # true
  • 61. Defining methods For a classMyClass = Class.newMyClass.class_eval " MyClass.class_eval do def new_method def new_method end end" endMyClass.send(:define_method, :new_method) { # your method body}
  • 62. Defining methods AliasingModule#alias_method
  • 63. Scoping
  • 64. Scopingmodule Project class Main def run # ... end endend
  • 65. Scoping module Project class Main def run # ... end end end Class definitionsModule definitionsMethod definitions
  • 66. Scopinga = hellomodule Project class Main def run puts a end endendProject::Main.new.run
  • 67. Scoping a = hello module Project class Main def run puts a end end end Project::Main.new.run# undefined local variable or method `a for #<Project::Main> (NameError)
  • 68. Scoping module Project class Main end end a = hello Project::Main.class_eval do define_method(:run) do puts a end endProject::Main.new.run # => hello
  • 69. ScopingExample: Connection Sharingmodule AddConnections def self.add_connection_methods(cls, host, port) cls.class_eval do define_method(:get_connection) do puts "Getting connection for #{host}:#{port}" end define_method(:host) { host } define_method(:port) { port } end endend
  • 70. ScopingExample: Connection Sharingmodule AddConnections def self.add_connection_methods(cls, host, port) cls.class_eval do define_method(:get_connection) do puts "Getting connection for #{host}:#{port}" end define_method(:host) { host } define_method(:port) { port } end endendClient = Class.newAddConnections.add_connection_methods(Client, localhost, 8080)Client.new.get_connection # Getting connection for localhost:8080Client.new.host # localhostClient.new.port # 8080
  • 71. ScopingKernel#bindingLet’s you leak the current “bindings”
  • 72. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end connection = nil create_connection(binding) connection # => I am a connection
  • 73. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end Calls connection = nilwith the create_connection(binding)current connection # => I am a connection state
  • 74. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end Calls connection = nilwith the create_connection(binding)current connection # => I am a connection state MAGIC!
  • 75. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end # connection = nil create_connection(binding) connection # undefined local variable or method `connection
  • 76. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end # connection = nil create_connection(binding) connection # undefined local variable or method `connection You can’t add to the local variables via binding
  • 77. ScopingKernel#bindingLet’s you leak the current “bindings” def create_connection(bind) eval connection = "I am a connection" , bind end eval "connection = nil" create_connection(binding) connection # undefined local variable or method `connection You can’t add to the local variables via eval
  • 78. ScopingKernel#bindingTOPLEVEL_BINDING
  • 79. ScopingKernel#bindingTOPLEVEL_BINDING a = hello module Program class Main def run puts eval("a", TOPLEVEL_BINDING) end end end Program::Main.new.run # => hello
  • 80. Interception!(aka lazy magic)
  • 81. Interception!method_missing(method, *args, &blk)
  • 82. Interception!method_missing(method, *args, &blk)class MethodMissing def method_missing(m, *args, &blk) puts "method_missing #{m} #{args.inspect} #{blk.inspect}" super endend
  • 83. Interception!method_missing(method, *args, &blk)class MethodMissing def method_missing(m, *args, &blk) puts "method_missing #{m} #{args.inspect} #{blk.inspect}" super endendmm = MethodMissing.newmm.i_dont_know_this(1, 2, 3)# method_missing i_dont_know_this [1, 2, 3] nil# NoMethodError: undefined method `i_dont_know_this for #<MethodMissing>
  • 84. Interception!Example: Timingmodule MethodsWithTiming def method_missing(m, *args, &blk) if timed_method = m.to_s[/^(.*)_with_timing$/, 1] and respond_to?(timed_method) respond = nil measurement = Benchmark.measure { respond = send(timed_method, *args, &blk) } puts "Method #{m} took #{measurement}" respond else super end endend
  • 85. Interception! Example: Timing module MethodsWithTiming def method_missing(m, *args, &blk) if timed_method = m.to_s[/^(.*)_with_timing$/, 1] and respond_to?(timed_method) respond = nil measurement = Benchmark.measure { respond = send(timed_method, *args, &blk) } puts "Method #{m} took #{measurement}" respond else super end end end sc = SlowClass.newclass SlowClass sc.slow include MethodsWithTiming def slow sleep 1 sc.slow_with_timing end # Method slow_with_timing took 0.000000 0.000000 0.000000 ( 1.000088)end
  • 86. Interception!Example: Proxyclass Proxy def initialize(backing) @backing = backing end def method_missing(m, *args, &blk) @backing.send(m, *args, &blk) endend
  • 87. Interception!Example: Proxyclass LoggingProxy def initialize(backing) @backing = backing end def method_missing(m, *args, &blk) puts "Calling method #{m} with #{args.inspect}" @backing.send(m, *args, &blk) endend
  • 88. Interception!Example: Simple DSL class NameCollector attr_reader :names def initialize @names = [] end def method_missing(method, *args, &blk) args.empty? ? @names.push(method.to_s.capitalize) : super end end nc = NameCollector.new nc.josh nc.bob nc.jane nc.names.join( ) # => Josh Bob Jane
  • 89. Interception!Object#respond_to?(sym)
  • 90. Interception! Object#respond_to?(sym) Example: Timingmodule MethodsWithTiming alias_method :original_respond_to?, :respond_to? def method_missing(m, *args, &blk) if timed_method = m.to_s[/^(.*)_with_timing$/, 1] and original_respond_to?(timed_method) respond = nil measurement = Benchmark.measure { respond = send(timed_method, *args, &blk) } puts "Method #{m} took #{measurement}" respond else super end end def respond_to?(sym) (timed_method = sym.to_s[/^(.*)_with_timing$/, 1]) ? original_respond_to?(timed_method.to_sym) : original_respond_to?(sym) endend
  • 91. Interception! Object#respond_to?(sym) Example: Timingmodule MethodsWithTiming ge ts alias_method :original_respond_to?, :respond_to? It def method_missing(m, *args, &blk) r! if timed_method = m.to_s[/^(.*)_with_timing$/, 1] and original_respond_to?(timed_method) te respond = nil et measurement = Benchmark.measure { b respond = send(timed_method, *args, &blk) } puts "Method #{m} took #{measurement}" respond else super end end def respond_to?(sym) (timed_method = sym.to_s[/^(.*)_with_timing$/, 1]) ? original_respond_to?(timed_method.to_sym) : original_respond_to?(sym) endend
  • 92. Interception! Object#respond_to_missing?(sym) (1.9 only) Example: Timingmodule MethodsWithTiming def method_missing(m, *args, &blk) if timed_method = m.to_s[/^(.*)_with_timing$/, 1] and respond_to?(timed_method) respond = nil measurement = Benchmark.measure { respond = send(timed_method, *args, &blk) } puts "Method #{m} took #{measurement}" respond else super end end def respond_to_missing?(sym) (timed_method = sym.to_s[/^(.*)_with_timing$/, 1]) ? respond_to?(timed_method.to_sym) : super endend
  • 93. Interception!const_missing(sym)
  • 94. Interception!const_missing(sym)MyClass::MyOtherClass# MyClass.const_missing(:MyOtherClass)
  • 95. Interception!const_missing(sym)MyClass::MyOtherClass# MyClass.const_missing(:MyOtherClass)Example: Loaderclass Loader def self.const_missing(sym) file = File.join(File.dirname(__FILE__), "#{sym.to_s.downcase}.rb") if File.exist?(file) require file Object.const_defined?(sym) ? Object.const_get(sym) : super else puts "cant find #{file}, sorry!" super end endend
  • 96. Interception!Example: Loaderclass Loader def self.const_missing(sym) file = File.join(File.dirname(__FILE__), "#{sym.to_s.downcase}.rb") if File.exist?(file) require file Object.const_defined?(sym) ? Object.const_get(sym) : super else puts "cant find #{file}, sorry!" super end endend
  • 97. Interception!Example: Loaderclass Loader def self.const_missing(sym) file = File.join(File.dirname(__FILE__), "#{sym.to_s.downcase}.rb") if File.exist?(file) require file Object.const_defined?(sym) ? Object.const_get(sym) : super else puts "cant find #{file}, sorry!" super end endendLoader::Auto# cant find ./auto.rb, sorry!# NameError: uninitialized constant Loader::Auto# or, if you have an ./auto.rbLoader::Auto# => Auto
  • 98. Callbacks
  • 99. CallbacksModule#method_added
  • 100. CallbacksModule#method_added
  • 101. CallbacksModule#method_addedclass MyClass def self.method_added(m) puts "adding #{m}" end puts "defining my method" def my_method two end puts "done defining my method"end
  • 102. CallbacksModule#method_addedclass MyClass defining my method def self.method_added(m) adding my_method puts "adding #{m}" done defining my method end puts "defining my method" def my_method two end puts "done defining my method"end
  • 103. CallbacksModule#method_addedExample: Thor!class Tasks def self.desc(desc) @desc = desc end def self.method_added(m) (@method_descs ||= {})[m] = @desc @desc = nil end def self.method_description(m) method_defined?(m) ? @method_descs[m] || "This action isnt documented" : "This action doesnt exist" end desc "Start server" def start end def stop endend
  • 104. CallbacksModule#method_addedExample: Thor! Record the descriptionclass Tasks def self.desc(desc) @desc = desc When a method is added, end record the description associated def self.method_added(m) with that method (@method_descs ||= {})[m] = @desc @desc = nil end Provide the description for a def self.method_description(m) method, or, if not found, some method_defined?(m) ? default string. @method_descs[m] || "This action isnt documented" : "This action doesnt exist" end desc "Start server" def start end def stop endend
  • 105. CallbacksModule#method_addedExample: Thor! Record the descriptionclass Tasks def self.desc(desc) @desc = desc When a method is added, end record the description associated def self.method_added(m) with that method (@method_descs ||= {})[m] = @desc @desc = nil end Provide the description for a def self.method_description(m) method, or, if not found, some method_defined?(m) ? default string. @method_descs[m] || "This action isnt documented" : "This action doesnt exist" end desc "Start server" def start end Described! def stop endend
  • 106. CallbacksModule#method_addedExample: Thor! Query your methods!class Tasks puts Tasks.method_description(:start) def self.desc(desc) # => Start server @desc = desc puts Tasks.method_description(:stop) end # => This action isnt documented def self.method_added(m) puts Tasks.method_description(:restart) (@method_descs ||= {})[m] = @desc # => This action doesnt exist @desc = nil end def self.method_description(m) method_defined?(m) ? @method_descs[m] || "This action isnt documented" : "This action doesnt exist" end desc "Start server" def start end def stop endend
  • 107. CallbacksObject#singleton_method_added
  • 108. CallbacksObject#singleton_method_addedclass ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end def self.another endend
  • 109. CallbacksObject#singleton_method_addedclass ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end def self.another endend# ADDING! singleton_method_added# ADDING! another
  • 110. CallbacksObject#singleton_method_addedclass ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end def self.another endend Holy meta!# ADDING! singleton_method_added# ADDING! another
  • 111. CallbacksModule#includedmodule Logger def self.included(m) puts "adding logging to #{m}" endendclass Server include Loggerend# adding logging to Server
  • 112. CallbacksModule#includedExample: ClassMethods patternmodule Logger class Server def self.included(m) include Logger puts "adding logging to #{m}" end def self.create log("Creating server!") def self.log(message) end puts "LOG: #{message}" end endendServer.create# `create: undefined method `log for Server:Class (NoMethodError)
  • 113. CallbacksModule#includedExample: ClassMethods patternmodule Logger class Server def self.included(m) include Logger m.extend(ClassMethods) end def self.create log("Creating server!") module ClassMethods end def log(message) end puts "LOG: #{message}" end endendServer.create# LOG: Creating server!
  • 114. CallbacksModule#extendedmodule One def self.extended(obj) puts "#{self} has been extended by #{obj}" endendObject.new.extend(One)# One has been extended by #<Object:0x1019614a8>
  • 115. CallbacksClass#inheritedclass Parent def self.inherited(o) puts "#{self} was inherited by #{o}" endendclass Child < Parentend# Parent was inherited by Child
  • 116. CallbacksGuarding callbacksModule#append_features includeModule#extend_object extend def self.extend_object(o) super end def self.append_features(o) super end
  • 117. CallbacksGuarding callbacksModule#append_features includeModule#extend_object extenddef self.append_features(o) o.instance_method(:<=>) ? super : warn(you no can uze)end
  • 118. CallbacksKernel#callerdef one twoenddef two method name three file name line (optional)enddef three p callerend# ["method.rb:156:in `two", "method.rb:152:in `one", "method.rb:163"] https://github.com/joshbuddy/callsite
  • 119. CallbacksModule#nestingmodule A module B module C p Module.nesting end endend# [A::B::C, A::B, A]
  • 120. There and back again, a parsing tale
  • 121. There and back again, a parsing tale gem install ruby_parser gem install sexp_processor gem install ruby2ruby Let’s go!
  • 122. There and back again, a parsing taleParsing require rubygems require ruby_parser RubyParser.new.process("string") s(:str, "string") Type Arguments...
  • 123. There and back again, a parsing taleParsing require rubygems require ruby_parser RubyParser.new.process("string") s(:str, "string") [:str, "string"] # Sexp Sexp.superclass # Array
  • 124. There and back again, a parsing taleParsing RubyParser.new.process("string + string")s(:call, s(:str, "string"), :+, s(:arglist, s(:str, "string"))) Method Method Receiver Arguments call name
  • 125. There and back again, a parsing taleParsing RubyParser.new.process("string + string") s(:call, nil, :puts, s(:arglist, s(:str, "hello world"))) Method Receiver Method Arguments call name
  • 126. There and back again, a parsing taleAnd, back again... require rubygems require ruby2ruby Ruby2Ruby.new.process [:str, "hello"] # => "hello"
  • 127. There and back again, a parsing taleAnd, back again... require rubygems require ruby2ruby Ruby2Ruby.new.process [:str, "hello"] # => "hello" Ruby2Ruby.new.process [:lit, :symbol] # => :symbol
  • 128. There and back again, a parsing taleRoundtrip require sexp_processor require ruby2ruby require ruby_parser class JarJarify < SexpProcessor def initialize self.strict = false super end def process_str(str) new_string = "YOUZA GONNA SAY #{str[-1]}" str.clear s(:str, new_string) end end
  • 129. There and back again, a parsing taleRoundtrip class JarJarify < SexpProcessor def initialize self.strict = false super end def process_str(str) new_string = "YOUZA GONNA SAY #{str[-1]}" str.clear s(:str, new_string) end end ast = RubyParser.new.process(puts "hello") Ruby2Ruby.new.process(JarJarify.new.process(ast)) # => puts("YOUZA GONNA SAY hello")
  • 130. There and back again, a parsing taleRoundtrip class JarJarify < SexpProcessor def initialize self.strict = false Process type :str super end def process_str(str) new_string = "YOUZA GONNA SAY #{str[-1]}" str.clear s(:str, new_string) Consume the current sexp end end Return a new one ast = RubyParser.new.process(puts "hello") Ruby2Ruby.new.process(JarJarify.new.process(ast)) # => puts("YOUZA GONNA SAY hello")
  • 131. IT’S OVER!