Upload
confoo
View
2.255
Download
2
Tags:
Embed Size (px)
Citation preview
Metaprogrammingin Ruby
Things I Wish I KnewWhen I Started Ruby
Who?
@jjJoshua Hull
https://github.com/joshbuddy
What?
What?
Writing programs that write programs.
What?
Writing programs that write programs.NOT code generation!
Why?We all do it.
Why?We all do it.
Ruby attr_accessor :my_attribute
Why?We all do it.
Ruby attr_accessor :my_attribute
def my_attribute @my_attributeend
def my_attribute=(my_attribute) @my_attribute = my_attributeend
Why?We all do it.
Ruby
public int getAttribute() { return attribute;}
public void setAttribute(int newAttribute) { attribute = newAttribute;}
Java
attr_accessor :my_attribute
Why?We all do it.
Ruby
Java
Winner
public int getAttribute() { return attribute;}
public void setAttribute(int newAttribute) { attribute = newAttribute;}
attr_accessor :my_attribute
Why?def age @age || 'not set'end
def gender @gender || 'not set'end
def name @name || 'not set'end
Why?
[:age, :gender, :name].each do |attr| define_method(attr) do instance_variable_get(:"@#{attr}") || 'not set' endend
def age @age || 'not set'end
def gender @gender || 'not set'end
def name @name || 'not set'end
Drawbacks
Drawbacks
You can write some difficult-to-understand code.
Drawbacks
You can write some difficult-to-understand code.
Method dispatch
Method dispatch
class MyObject attr_accessor :name
def say_hello puts "hello" puts name endend
Method dispatch
class MyObject attr_accessor :name
def say_hello puts "hello" puts name endend This is a
method call,but who receives it?
Method dispatch
class MyObject attr_accessor :name
def say_hello puts "hello" puts self.name endend
Method dispatch
class MyObject attr_accessor :name
def say_hello puts "hello" puts self.name endend
self is always theimplied receiver!
Method dispatch
class MyObject attr_accessor :name
def say_hello puts "hello" puts name endend
Method dispatch
class MyObject self.attr_accessor :name
def say_hello puts "hello" puts name endend
Method dispatch
Who is self here?
class MyObject self.attr_accessor :name
def say_hello puts "hello" puts name endend
Method dispatch
Who is self here?
class MyObject self.attr_accessor :name
def say_hello puts "hello" puts name endend
The class is!
Method dispatch
Who is self here?
The class is!
Module#attr
Ruby classes
Ruby classesclass NewClass def hey puts 'hello!' endend
Ruby classesclass NewClass def hey puts 'hello!' endend
This is normalcode!
{
Ruby classesclass NewClass def hey puts 'hello!' endend
NewClass = Class.new do def hey puts 'hello!' endend
is the same as
Ruby classes
class ParsingError < RuntimeErrorend
Ruby classes
ParsingError = Class.new(RuntimeError)
class ParsingError < RuntimeErrorend
is the same as
Ruby classes
def class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend
Ruby classes
def class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend
Returns a new class!
Ruby classes
def class_with_accessors(*attributes) Class.new do attr_accessor *attributes endend
Returns a new class!
class Person < class_with_accessors(:name, :age, :sex) # ...end
eval, instance_eval, class_eval
eval, instance_eval, class_eval
eval
instance_eval
class_eval
Method
eval, instance_eval, class_eval
eval
instance_eval
class_eval
your current context
the object
the object’s class
Method Context
eval, instance_eval, class_eval
eval "puts 'hello'"eval
# hello
eval, instance_eval, class_eval
class MyClassend
MyClass.instance_eval("def hi; 'hi'; end")
instance_eval
eval, instance_eval, class_eval
instance_eval
MyClass.hi# 'hi'
class MyClassend
MyClass.instance_eval("def hi; 'hi'; end")
eval, instance_eval, class_eval
class MyClassend
MyClass.instance_eval("def hi; 'hi' end")
instance_eval
obj = MyClass.new# <MyClass:0x10178aff8> obj.hi# NoMethodError: undefined method `hi' for #<MyClass>
eval, instance_eval, class_eval
class MyClassend
MyClass.class_eval("def hi; 'hi' end")
class_eval
eval, instance_eval, class_eval
class MyClassend
MyClass.class_eval("def hi; 'hi' end")
class_eval
MyClass.hi# NoMethodError: undefined method `hi' for MyClass:Class
eval, instance_eval, class_eval
class MyClassend
MyClass.class_eval("def hi; 'hi' end")
class_eval
obj = MyClass.new# <MyClass:0x101849688> obj.hi# "hi"
eval, instance_eval, class_eval
class_eval
class MyClassend
MyClass.class_eval("def hi; 'hi'; end")
obj = MyClass.new# <MyClass:0x101849688> obj.hi# "hi"
instance_eval
MyClass.hi# 'hi'
class MyClassend
MyClass.instance_eval("def hi; 'hi'; end")
eval, instance_eval, class_eval
Ninja’s will attack you if ...
you don’t use __FILE__, __LINE__
eval, instance_eval, class_eval
class HiThereend
HiThere.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, instance_eval, class_eval
class HiThereend
HiThere.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
my_file.rb:5:in `hi_with_niceness': unhandled exception from my_file.rb:7
eval, instance_eval, class_eval
class HiThereend
HiThere.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
my_file.rb:5:in `hi_with_niceness': unhandled exception from my_file.rb:7
So nice. <3
eval, instance_eval, class_eval
Implement attr_accessor!
eval, instance_eval, class_eval
class Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endend
eval, instance_eval, class_eval
class Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endend
class M create_attr :hiend
eval, instance_eval, class_eval
class Module def create_attr(attribute) class_eval("def #{attribute}; @#{attribute}; end") endend
class M create_attr :hiend
def M def hi @hi endend
Defining methods
Defining methodsFor an object
o = Object.newo.instance_eval("def just_this_object; end")o.just_this_object
Defining methodsFor an object
Object.new.just_this_object# NoMethodError: undefined method `just_this_object'
o = Object.newo.instance_eval("def just_this_object; end")o.just_this_object
Defining methodsFor an object
Object.new.just_this_object# NoMethodError: undefined method `just_this_object'
o = Object.newo.instance_eval("def just_this_object; end")o.just_this_object
o = Object.newo.instance_eval { def just_this_object end}
Defining methodsFor an object
o = Object.newo.extend(Module.new { def just_this_object end})
Defining methodsFor a class
MyClass = Class.new
class MyClass def new_method endend
MyClass.new.respond_to?(:new_method) # true
Defining methodsFor a class
MyClass = Class.new
MyClass.class_eval " def new_method end"
MyClass.class_eval do def new_method endend
MyClass.send(:define_method, :new_method) { # your method body}
Defining methodsAliasing
Module#alias_method
Scoping
Scoping
module Project class Main def run # ... end endend
Scoping
module Project class Main def run # ... end endend
Class definitionsModule definitionsMethod definitions
Scopinga = 'hello'
module Project class Main def run puts a end endend
Project::Main.new.run
Scopinga = 'hello'
module Project class Main def run puts a end endend
Project::Main.new.run
# undefined local variable or method `a' for #<Project::Main> (NameError)
Scopingmodule Project class Main endend
a = 'hello'
Project::Main.class_eval do define_method(:run) do puts a endend
Project::Main.new.run # => hello
ScopingExample: Connection Sharing
module 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
ScopingExample: Connection Sharing
module 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
Client = Class.newAddConnections.add_connection_methods(Client, 'localhost', 8080)
Client.new.get_connection # Getting connection for localhost:8080Client.new.host # localhost Client.new.port # 8080
ScopingKernel#bindingLet’s you leak the current “bindings”
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
connection = nilcreate_connection(binding)connection # => I am a connection
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
connection = nilcreate_connection(binding)connection # => I am a connection
Callswith thecurrentstate
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
connection = nilcreate_connection(binding)connection # => I am a connection
Callswith thecurrentstate MAGIC!
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
# connection = nilcreate_connection(binding)connection# undefined local variable or method `connection'
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
# connection = nilcreate_connection(binding)connection# undefined local variable or method `connection'
You can’t add to the local variables via binding
ScopingKernel#bindingLet’s you leak the current “bindings”
def create_connection(bind) eval ' connection = "I am a connection" ', bindend
eval "connection = nil"create_connection(binding)connection# undefined local variable or method `connection'
You can’t add to the local variables via eval
ScopingKernel#binding
TOPLEVEL_BINDING
ScopingKernel#binding
TOPLEVEL_BINDING
a = 'hello'
module Program class Main def run puts eval("a", TOPLEVEL_BINDING) end endend
Program::Main.new.run # => hello
Interception!(aka lazy magic)
Interception!method_missing(method, *args, &blk)
Interception!method_missing(method, *args, &blk)
class MethodMissing def method_missing(m, *args, &blk) puts "method_missing #{m} #{args.inspect} #{blk.inspect}" super endend
Interception!method_missing(method, *args, &blk)
class MethodMissing def method_missing(m, *args, &blk) puts "method_missing #{m} #{args.inspect} #{blk.inspect}" super endend
mm = 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>
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
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
class SlowClass include MethodsWithTiming def slow sleep 1 endend
sc = SlowClass.newsc.slow
sc.slow_with_timing# Method slow_with_timing took 0.000000 0.000000 0.000000 ( 1.000088)
Interception!Example: Proxyclass Proxy def initialize(backing) @backing = backing end
def method_missing(m, *args, &blk) @backing.send(m, *args, &blk) endend
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
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 endend
nc = NameCollector.newnc.joshnc.bobnc.janenc.names.join(' ') # => Josh Bob Jane
Interception!Object#respond_to?(sym)
Interception!Object#respond_to?(sym)Example: Timing
module 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
Interception!Object#respond_to?(sym)Example: Timing
module 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
It gets
better!
Interception!Object#respond_to_missing?(sym) (1.9 only)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
def respond_to_missing?(sym) (timed_method = sym.to_s[/^(.*)_with_timing$/, 1]) ? respond_to?(timed_method.to_sym) : super endend
Interception!const_missing(sym)
Interception!const_missing(sym)MyClass::MyOtherClass
# MyClass.const_missing(:MyOtherClass)
Interception!const_missing(sym)
Example: Loader
MyClass::MyOtherClass
# MyClass.const_missing(:MyOtherClass)
class 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 "can't find #{file}, sorry!" super end endend
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 "can't find #{file}, sorry!" super end endend
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 "can't find #{file}, sorry!" super end endend
Loader::Auto# can't find ./auto.rb, sorry!# NameError: uninitialized constant Loader::Auto
# or, if you have an ./auto.rbLoader::Auto# => Auto
Callbacks
CallbacksModule#method_added
CallbacksModule#method_added
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
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
defining my methodadding my_methoddone defining my method
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 isn't documented" : "This action doesn't exist" end
desc "Start server" def start end
def stop endend
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 isn't documented" : "This action doesn't exist" end
desc "Start server" def start end
def stop endend
Record the description
When a method is added,record the description associatedwith that method
Provide the description for a method, or, if not found, some default string.
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 isn't documented" : "This action doesn't exist" end
desc "Start server" def start end
def stop endend
Record the description
When a method is added,record the description associatedwith that method
Provide the description for a method, or, if not found, some default string.
Described!
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 isn't documented" : "This action doesn't exist" end
desc "Start server" def start end
def stop endend
puts Tasks.method_description(:start)# => Start serverputs Tasks.method_description(:stop)# => This action isn't documentedputs Tasks.method_description(:restart)# => This action doesn't exist
Query your methods!
CallbacksObject#singleton_method_added
CallbacksObject#singleton_method_added
class ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end
def self.another endend
CallbacksObject#singleton_method_added
class ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end
def self.another endend
# ADDING! singleton_method_added# ADDING! another
CallbacksObject#singleton_method_added
class ClassWithMethods def self.singleton_method_added(m) puts "ADDING! #{m}" end
def self.another endend
# ADDING! singleton_method_added# ADDING! another
Holy meta!
CallbacksModule#includedmodule Logger def self.included(m) puts "adding logging to #{m}" endend
class Server include Loggerend
# adding logging to Server
CallbacksModule#includedExample: ClassMethods pattern
module Logger def self.included(m) puts "adding logging to #{m}" end
def self.log(message) puts "LOG: #{message}" endend
class Server include Logger
def self.create log("Creating server!") endend
Server.create# `create': undefined method `log' for Server:Class (NoMethodError)
CallbacksModule#includedExample: ClassMethods pattern
class Server include Logger
def self.create log("Creating server!") endend
module Logger def self.included(m) m.extend(ClassMethods) end
module ClassMethods def log(message) puts "LOG: #{message}" end endend
Server.create# LOG: Creating server!
CallbacksModule#extended
module One def self.extended(obj) puts "#{self} has been extended by #{obj}" endend
Object.new.extend(One)
# One has been extended by #<Object:0x1019614a8>
CallbacksClass#inherited
class Parent def self.inherited(o) puts "#{self} was inherited by #{o}" endend
class Child < Parentend
# Parent was inherited by Child
CallbacksGuarding callbacks
Module#append_featuresModule#extend_object
includeextend
def self.extend_object(o) super end
def self.append_features(o) super end
CallbacksGuarding callbacks
Module#append_featuresModule#extend_object
includeextend
def self.append_features(o) o.instance_method(:<=>) ? super : warn('you no can uze')end
CallbacksKernel#callerdef one twoend
def two threeend
def three p callerend
# ["method.rb:156:in `two'", "method.rb:152:in `one'", "method.rb:163"]
file name linemethod name
(optional)
https://github.com/joshbuddy/callsite
CallbacksModule#nesting
module A module B module C p Module.nesting end endend
# [A::B::C, A::B, A]
There and back again, a parsing tale
gem install ruby_parsergem install sexp_processor
Let’s go!
There and back again, a parsing tale
gem install ruby2ruby
There and back again, a parsing tale
require 'rubygems'require 'ruby_parser'RubyParser.new.process("'string'")
s(:str, "string")
Type Arguments...
Parsing
There and back again, a parsing tale
require 'rubygems'require 'ruby_parser'RubyParser.new.process("'string'")
s(:str, "string")[:str, "string"] # Sexp
Sexp.superclass# Array
Parsing
There and back again, a parsing tale
RubyParser.new.process("'string' + 'string'")
s(:call, s(:str, "string"), :+, s(:arglist, s(:str, "string")))
Methodcall Receiver Method
name Arguments
Parsing
There and back again, a parsing tale
RubyParser.new.process("'string' + 'string'")
Methodcall
Receiver Methodname Arguments
s(:call, nil, :puts, s(:arglist, s(:str, "hello world")))
Parsing
There and back again, a parsing tale
And, back again...
require 'rubygems'require 'ruby2ruby'
Ruby2Ruby.new.process [:str, "hello"] # => "hello"
There and back again, a parsing tale
And, back again...
require 'rubygems'require 'ruby2ruby'
Ruby2Ruby.new.process [:str, "hello"] # => "hello"Ruby2Ruby.new.process [:lit, :symbol] # => :symbol
There and back again, a parsing tale
Roundtrip
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) endend
There and back again, a parsing tale
Roundtripclass 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) endend
ast = RubyParser.new.process('puts "hello"')Ruby2Ruby.new.process(JarJarify.new.process(ast))# => puts("YOUZA GONNA SAY hello")
There and back again, a parsing tale
Roundtripclass 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) endend
ast = RubyParser.new.process('puts "hello"')Ruby2Ruby.new.process(JarJarify.new.process(ast))# => puts("YOUZA GONNA SAY hello")
Process type :str
Consume the current sexpReturn a new one
IT’S OVER!