Reflexive Metaprogramming in Ruby H Conrad Cunningham Computer
Reflexive Metaprogramming in Ruby H. Conrad Cunningham Computer and Information Science University of Mississippi
Metaprogramming: writing programs that write or manipulate programs as data Reflexive metaprogramming: writing programs that manipulate themselves as data 2
Reflexive Metaprogramming Languages n Early – Lisp – Smalltalk n More recent – – – Ruby Python Groovy 3
Basic Characteristics of Ruby (1 of 2) Interpreted n Purely object-oriented n Single inheritance with mixins n Garbage collected n Dynamically, but strongly typed n “Duck typed” n Message passing (to methods) n 4
Basic Characteristics of Ruby (2 of 2) n Flexible syntax – – n optional parentheses on method calls variable number of arguments two block syntax alternatives symbol data type String manipulation facilities – regular expressions – string interpolation n Array and hash data structures 5
Why Ruby Supportive of Reflexive Metaprogramming (1 of 2) Open classes n Executable declarations n Dynamic method definition, removal, hiding, and aliasing n Runtime callbacks for n – program changes (e. g. method_added) – missing methods (missing_method) 6
Why Ruby Supportive of Reflexive Metaprogramming (2 of 2) n 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) n Singleton classes/methods for objects n Mixin modules (e. g. Enumerable) n Blocks and closures n Continuations n 7
Employee Class Hierarchy Initialization class Employee @@nextid = 1 def initialize(first, last, dept, boss) @fname = first. to_s @lname = last. to_s @deptid = dept @supervisor = boss @empid = @@nextid + 1 end 8
Employee Class Hierarchy Writer Methods def deptid=(dept) # deptid = dept @deptid = dept end def supervisor=(boss) @supervisor = boss end 9
Employee Class Hierarchy Reader Methods def name # not an attribute @lname + ", " + @fname end def empid; @empid; end def deptid; @deptid; end def supervisor @supervisor end 10
Employee Class Hierarchy String Conversion Reader def to_s @empid. to_s + " : " + name + " : " + @deptid. to_s + " (" + @supervisor. to_s + ")" end # Employee 11
Employee Class Hierarchy Alternate Initialization class Employee @@nextid = 1 attr_accessor : deptid, : supervisor attr_reader : empid def initialize(first, last, dept, boss) # as before end 12
Employee Class Hierarchy Other Reader Methods def name @lname + ", " + @fname end def to_s @empid. to_s + " : " + name + " : " + @deptid. to_s + " (" + @supervisor. to_s + ")" end # Employee 13
Employee Class Hierarchy Staff Subclass 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 # Staff 14
Employee Class Hierarchy Using Employee Classes class Test. Employee def Test. Employee. do_test @s 1 = Staff. new("Robert", "Khayat", "Law", nil, "Chancellor") @s 2 = Staff. new("Carolyn", "Staton", "Law", @s 1, "Provost") puts "s 1. class ==> " + @s 1. class. to_s puts "s 1. to_s ==> " + @s 1. to_s puts "s 2. to_s ==> " + @s 2. to_s @s 1. deptid = "Chancellor" puts "s 1. to_s ==> " + @s 1. to_s puts "s 1. methods ==> " + @s 1. methods. join(", ") end # Test. Employee 15
Employee Class Hierarchy Test. Employee. do_test Output irb(main): 001: 0> load "Employee. rb" => true irb(main): 002: 0> Test. Employee. do_test s 1. class ==> Staff s 1. to_s ==> 1 : Khayat, Robert : Law (), Chancellor s 2. to_s ==> 2 : Staton, Carolyn : Law (1 : Khayat, Robert : Law (), Chancellor), Provost s 1. to_s ==> 1 : Khayat, Robert : Chancellor (), Chancellor s 1. methods ==> to_a, respond_to? , display, deptid, type, protected_methods, require, deptid=, title, … kind_of? => nil 16
Ruby Metaprogramming Class Macros n Every class has Class object where instance methods reside n Class definition is executable n Class extends class Module n Instance methods of class Module available during definition of classes n Result is essentially “class macros” 17
Ruby Metaprogramming Code String Evaluation class_eval instance method of class 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 18
Ruby Metaprogramming Implementing 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 # syms. each end # attr_reader end # Module 19
Ruby Metaprogramming Implementing 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 # attr_writer end # Module 20
Ruby Metaprogramming Runtime 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 Chair 21
Ruby Metaprogramming Runtime 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 22
Domain Specific Languages (DSL) n n n 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 23
Martin Fowler DSL Example Input Data File #12345678901234567890123456 SVCLFOWLER 10101 MS 0120050313 SVCLHOHPE 10201 DX 0320050315 SVCLTWO x 10301 MRP 220050329 USGE 10301 TWO x 50214. . 7050329 24
Martin Fowler DSL Example Text Data Description mapping SVCL dsl. Service. Call 4 -18: Customer. Name 19 -23: Customer. ID 24 -27 : Call. Type. Code 28 -35 : Date. Of. Call. String mapping USGE dsl. Usage 4 -8 : Customer. ID 9 -22: Customer. Name 30 -30: Cycle 31 -36: Read. Date 25
Martin Fowler DSL Example XML Data Description <Reader. Configuration> <Mapping Code = "SVCL" Target. Class = "dsl. Service. Call"> <Field name = "Customer. Name" start = "4" end = "18"/> <Field name = "Customer. ID" start = "19" end = "23"/> <Field name = "Call. Type. Code" start = "24" end = "27"/> <Field name = "Date. Of. Call. String" start = "28" end = "35"/> </Mapping> <Mapping Code = "USGE" Target. Class = "dsl. Usage"> <Field name = "Customer. ID" start = "4" end = "8"/> <Field name = "Customer. Name" start = "9" end = "22"/> <Field name = "Cycle" start = "30" end = "30"/> <Field name = "Read. Date" start = "31" end = "36"/> </Mapping> </Reader. Configuration> 26
Martin Fowler DSL Example Ruby Data Description mapping('SVCL', Service. Call) 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 27
Martin Fowler DSL Example Ruby DSL Class (1) require 'Reader. Framework' class Builder. Ruby. DSL 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 28
Martin Fowler DSL Example Ruby DSL Class (2 of 3) def mapping(code, target) @cur_mapping = Reader. Framework: : Reader. Strategy. new( code, target) @reader. add_strategy(@cur_mapping) yield end def extract(range, field_name) begin_col = range. begin end_col = range. end_col -= 1 if range. exclude_end? @cur_mapping. add_field_extractor( begin_col, end_col, field_name) end#Builder. Ruby. DSL 29
Martin Fowler DSL Example Ruby DSL Class (3 of 3) class Service. Call; class Usage; end class Test. Ruby. DSL def Test. Ruby. DSL. run rdr = Reader. Framework: : Reader. new cfg = Builder. Ruby. DSL. new("dslinput. rb") cfg. configure(rdr) inp = File. new("fowlerdata. txt") res = rdr. process(inp) inp. close res. each {|o| puts o. inspect} end 30
Using Blocks and Iterators Inverted Index (1) class Inverted. Index @@wp = /(w+([-'. ]w+)*)/ DEFAULT_STOPS = {"the" => 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 31
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 32
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 33 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#@index. each's block self end#index_file 34
Using Blocks and Iterators Inverted Index (5) def lookup(word) if @index[word]. map {|f| [f[0]. clone, f[1]. clone] } else nil end # … end 35
Questions 36
- Slides: 36