360 likes | 478 Views
Reflexive Metaprogramming in Ruby. H. Conrad Cunningham Computer and Information Science University of Mississippi. Metaprogramming. Metaprogramming : writing programs that write or manipulate programs as data Reflexive metaprogramming: writing programs that manipulate themselves as data.
E N D
Reflexive Metaprogramming in Ruby H. Conrad Cunningham Computer and Information Science University of Mississippi
Metaprogramming Metaprogramming: writing programs that write or manipulate programs as data Reflexive metaprogramming: writing programs that manipulate themselves as data
Reflexive Metaprogramming Languages • Early • Lisp • Smalltalk • More recent • Ruby • Python • Groovy
Basic Characteristics of Ruby(1 of 2) • Interpreted • Purely object-oriented • Single inheritance with mixins • Garbage collected • Dynamically, but strongly typed • “Duck typed” • Message passing (to methods)
Basic Characteristics of Ruby (2 of 2) • Flexible syntax • optional parentheses on method calls • variable number of arguments • two block syntax alternatives • symbol data type • String manipulation facilities • regular expressions • string interpolation • Array and hash data structures
Why Ruby Supportive of Reflexive Metaprogramming (1 of 2) • Open classes • Executable declarations • Dynamic method definition, removal, hiding, and aliasing • Runtime callbacks for • program changes (e.g. method_added) • missing methods (missing_method)
Why Ruby Supportive of Reflexive Metaprogramming (2 of 2) • Dynamic evaluation of strings as code • at module level for declarations (class_eval) • at object level for computation (instance_eval) • Reflection (e.g. kind_of?, methods) • Singleton classes/methods for objects • Mixin modules (e.g. Enumerable) • Blocks and closures • Continuations
Employee Class HierarchyInitialization class Employee @@nextid = 1 def initialize(first,last,dept,boss) @fname = first.to_s @lname = last.to_s @deptid = dept @supervisor = boss @empid = @@nextid @@nextid = @@nextid + 1 end
Employee Class HierarchyWriter Methods def deptid=(dept) # deptid = dept @deptid = dept end def supervisor=(boss) @supervisor = boss end
Employee Class HierarchyReader Methods def name # not an attribute @lname + ", " + @fname end def empid; @empid; end def deptid; @deptid; end def supervisor @supervisor end
Employee Class HierarchyString Conversion Reader def to_s @empid.to_s + " : " + name + " : " + @deptid.to_s + " (" + @supervisor.to_s + ")" end end # Employee
Employee Class HierarchyAlternate Initialization class Employee @@nextid = 1 attr_accessor :deptid, :supervisor attr_reader :empid def initialize(first,last,dept,boss) # as before end
Employee Class HierarchyOtherReader Methods def name @lname + ", " + @fname end def to_s @empid.to_s + " : " + name + " : " + @deptid.to_s + " (" + @supervisor.to_s + ")" end end # Employee
Employee Class HierarchyStaff Subclass class Staff < Employee attr_accessor :title def initialize(first,last,dept, boss,title) super(first,last,dept,boss) @title = title end def to_s super.to_s + ", " + @title.to_s end end # Staff
Employee Class HierarchyUsing Employee Classes class TestEmployee def TestEmployee.do_test @s1 = Staff.new("Robert", "Khayat", "Law", nil, "Chancellor") @s2 = Staff.new("Carolyn", "Staton", "Law", @s1,"Provost") puts "s1.class ==> " + @s1.class.to_s puts "s1.to_s ==> " + @s1.to_s puts "s2.to_s ==> " + @s2.to_s @s1.deptid = "Chancellor" puts "s1.to_s ==> " + @s1.to_s puts "s1.methods ==> " + @s1.methods.join(", ") end end # TestEmployee
Employee Class HierarchyTestEmployee.do_test Output irb irb(main):001:0> load "Employee.rb" => true irb(main):002:0> TestEmployee.do_test s1.class ==> Staff s1.to_s ==> 1 : Khayat, Robert : Law (), Chancellor s2.to_s ==> 2 : Staton, Carolyn : Law (1 : Khayat, Robert : Law (), Chancellor), Provost s1.to_s ==> 1 : Khayat, Robert : Chancellor (), Chancellor s1.methods ==> to_a, respond_to?, display, deptid, type, protected_methods, require, deptid=, title, … kind_of? => nil
Ruby MetaprogrammingClass Macros • Every class has Class object where instance methods reside • Class definition is executable • Class Class extends class Module • Instance methods of class Module available during definition of classes • Result is essentially “class macros”
Ruby MetaprogrammingCodeString Evaluation class_eval instance method ofclass Module • evaluates string as Ruby code • using context of class Module • enabling definition of new methods and constants instance_eval instance method of class Object • evaluates string as Ruby code • using context of the object • enabling statement execution and state changes
Ruby MetaprogrammingImplementing attr_reader # Not really implemented this way class Module # add to system class def attr_reader(*syms) syms.each do |sym| class_eval%{def #{sym} @#{sym} end} end # syms.each end # attr_reader end # Module
Ruby MetaprogrammingImplementing attr_writer # Not really implemented this way class Module # add to system class def attr_writer(*syms) syms.each do |sym| class_eval %{def #{sym}=(val) @#{sym} = val end} end end # attr_writer end # Module
Ruby MetaprogrammingRuntime Callbacks class Employee # class definitions executable def Employee.inherited(sub) # class method puts "New subclass: #{sub}" # of Class end class Faculty < Employee end class Chair < Faculty end OUTPUTS New subclass: Faculty New subclass: Chair
Ruby MetaprogrammingRuntime Callbacks class Employee def method_missing(meth,*args) # instance method mstr = meth.to_s # of Object last = mstr[-1,1] base = mstr[0..-2] if last == "=" class_eval("attr_writer :#{base}") else class_eval("attr_reader :#{mstr}") end end end
Domain Specific Languages (DSL) • Programming or description language designed for particular family of problems • Specialized syntax and semantics • Alternative approaches • External language with specialized interpreter • Internal (embedded) language by tailoring a general purpose language
Martin Fowler DSL ExampleInput Data File #123456789012345678901234567890123456 SVCLFOWLER 10101MS0120050313 SVCLHOHPE 10201DX0320050315 SVCLTWO x10301MRP220050329 USGE10301TWO x50214..7050329
Martin Fowler DSL ExampleText Data Description mapping SVCL dsl.ServiceCall 4-18: CustomerName 19-23: CustomerID 24-27 : CallTypeCode 28-35 : DateOfCallString mapping USGE dsl.Usage 4-8 : CustomerID 9-22: CustomerName 30-30: Cycle 31-36: ReadDate
Martin Fowler DSL ExampleXML Data Description <ReaderConfiguration> <Mapping Code = "SVCL" TargetClass = "dsl.ServiceCall"> <Field name = "CustomerName" start = "4" end = "18"/> <Field name = "CustomerID" start = "19" end = "23"/> <Field name = "CallTypeCode" start = "24" end = "27"/> <Field name = "DateOfCallString" start = "28" end = "35"/> </Mapping> <Mapping Code = "USGE" TargetClass = "dsl.Usage"> <Field name = "CustomerID" start = "4" end = "8"/> <Field name = "CustomerName" start = "9" end = "22"/> <Field name = "Cycle" start = "30" end = "30"/> <Field name = "ReadDate" start = "31" end = "36"/> </Mapping> </ReaderConfiguration>
Martin Fowler DSL ExampleRuby Data Description mapping('SVCL', ServiceCall) do extract 4..18, 'customer_name' extract 19..23, 'customer_ID' extract 24..27, 'call_type_code' extract 28..35, 'date_of_call_string' end mapping('USGE', Usage) do extract 9..22, 'customer_name' extract 4..8, 'customer_ID' extract 30..30, 'cycle' extract 31..36, 'read_date‘ end
Martin Fowler DSL ExampleRuby DSL Class (1) require 'ReaderFramework' class BuilderRubyDSL def initialize(filename) @rb_dsl_file = filename end def configure(reader) @reader = reader rb_file = File.new(@rb_dsl_file) instance_eval(rb_file.read, @rb_dsl_file) rb_file.close end
Martin Fowler DSL ExampleRuby DSL Class (2 of 3) def mapping(code,target) @cur_mapping = ReaderFramework::ReaderStrategy.new( code,target) @reader.add_strategy(@cur_mapping) yield end def extract(range,field_name) begin_col = range.begin end_col = range.end end_col -= 1 if range.exclude_end? @cur_mapping.add_field_extractor( begin_col,end_col,field_name) end end#BuilderRubyDSL
Martin Fowler DSL ExampleRuby DSL Class (3 of 3) class ServiceCall; end class Usage; end class TestRubyDSL def TestRubyDSL.run rdr = ReaderFramework::Reader.new cfg = BuilderRubyDSL.new("dslinput.rb") cfg.configure(rdr) inp = File.new("fowlerdata.txt") res = rdr.process(inp) inp.close res.each {|o| puts o.inspect} end end
Using Blocks and Iterators Inverted Index (1) class InvertedIndex @@wp = /(\w+([-'.]\w+)*)/ DEFAULT_STOPS = {"the" => true, "a" => true, "an" => true} def initialize(*args) @files_indexed = [] @index = Hash.new @stops = Hash.new if args.size == 1 args[0].each {|w| @stops[w] = true} else @stops = DEFAULT_STOPS end end
Using Blocks and Iterators Inverted Index (2) def index_file(filename) unless @files_indexed.index(filename) == nil STDERR.puts("#{filename} already indexed.") return end unless File.exist? Filename STDERR.puts("#{filename} does not exist.") return end unless File.readable? Filename STDERR. puts("#{filename} is not readable.") return end @files_indexed << filename
Using Blocks and Iterators Inverted Index (3) inf = File.new(filename) lineno = 0 inf.each do |s| lineno += 1 words = s.scan(@@wp).map {|a| a[0].downcase} words = words.reject {|w| @stops[w]} words = words.map {|w| [w,[filename,[lineno]]]} words.each do |p| @index[p[0]] = [] unless @index.has_key? p[0] @index[p[0]] = @index[p[0]].push(p[1]) end end inf.close
Using Blocks and Iterators Inverted Index (4) @index.each do |k,v| # k => v is hash entry @index[k] = v.sort {|a,b| a[0] <=> b[0]} end @index.each do |k,v| @index[k] = v.slice(1...v.length).inject([v[0]]) do |acc, e| if acc[-1][0] == e[0] acc[-1][1] = acc[-1][1] + e[1] else acc = acc + [e] end acc end end#@index.each's block self end#index_file
Using Blocks and Iterators Inverted Index (5) def lookup(word) if @index[word] @index[word].map {|f| [f[0].clone, f[1].clone] } else nil end end # … end