Reflexive Metaprogramming in Ruby

36
Reflexive Reflexive Metaprogramming Metaprogramming in Ruby in Ruby H. Conrad Cunningham H. Conrad Cunningham Computer and Information Science Computer and Information Science University of Mississippi University of Mississippi

description

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. - PowerPoint PPT Presentation

Transcript of Reflexive Metaprogramming in Ruby

Page 1: Reflexive Metaprogramming  in Ruby

Reflexive Metaprogramming Reflexive Metaprogramming in Rubyin Ruby

H. Conrad CunninghamH. Conrad CunninghamComputer and Information ScienceComputer and Information Science

University of MississippiUniversity of Mississippi

Page 2: Reflexive Metaprogramming  in Ruby

22

MetaprogrammingMetaprogramming

MetaprogrammingMetaprogramming: writing programs : writing programs that write or manipulate programs as that write or manipulate programs as data data

Reflexive metaprogramming: Reflexive metaprogramming: writing writing programs that manipulate themselves as programs that manipulate themselves as data data

Page 3: Reflexive Metaprogramming  in Ruby

33

Reflexive Metaprogramming Reflexive Metaprogramming LanguagesLanguages

EarlyEarly– LispLisp– SmalltalkSmalltalk

More recentMore recent– RubyRuby– PythonPython– GroovyGroovy

Page 4: Reflexive Metaprogramming  in Ruby

44

Basic Characteristics of RubyBasic Characteristics of Ruby(1 of 2)(1 of 2)

InterpretedInterpreted Purely object-orientedPurely object-oriented Single inheritance with mixinsSingle inheritance with mixins Garbage collectedGarbage collected Dynamically, but strongly typedDynamically, but strongly typed ““Duck typed”Duck typed” Message passing (to methods) Message passing (to methods)

Page 5: Reflexive Metaprogramming  in Ruby

55

Basic Characteristics of Ruby Basic Characteristics of Ruby (2 of 2)(2 of 2)

Flexible syntaxFlexible syntax– optional parentheses on method callsoptional parentheses on method calls– variable number of argumentsvariable number of arguments– two block syntax alternativestwo block syntax alternatives– symbol data typesymbol data type

String manipulation facilitiesString manipulation facilities– regular expressionsregular expressions– string interpolationstring interpolation

Array and hash data structuresArray and hash data structures

Page 6: Reflexive Metaprogramming  in Ruby

66

Why Ruby Supportive of Reflexive Why Ruby Supportive of Reflexive Metaprogramming (1 of 2)Metaprogramming (1 of 2)

Open classesOpen classes Executable declarationsExecutable declarations Dynamic method definition, removal, Dynamic method definition, removal,

hiding, and aliasinghiding, and aliasing Runtime callbacks forRuntime callbacks for– program changes (e.g. program changes (e.g. method_addedmethod_added))– missing methods (missing methods (missing_methodmissing_method))

Page 7: Reflexive Metaprogramming  in Ruby

77

Why Ruby Supportive of Reflexive Why Ruby Supportive of Reflexive Metaprogramming (2 of 2)Metaprogramming (2 of 2)

Dynamic evaluation of strings as codeDynamic evaluation of strings as code– at module level for declarations (at module level for declarations (class_evalclass_eval))

– at object level for computation (at object level for computation (instance_evalinstance_eval)) Reflection (e.g. Reflection (e.g. kind_of?, methods)kind_of?, methods) Singleton classes/methods for objectsSingleton classes/methods for objects Mixin modules (e.g. Mixin modules (e.g. EnumerableEnumerable)) Blocks and closuresBlocks and closures ContinuationsContinuations

Page 8: Reflexive Metaprogramming  in Ruby

88

Employee Class HierarchyEmployee Class HierarchyInitializationInitialization

class Employeeclass Employee @@nextid@@nextid = 1 = 1

def def initializeinitialize(first,last,dept,boss) (first,last,dept,boss) @fname@fname = first.to_s = first.to_s @lname = last.to_s@lname = last.to_s @deptid = dept @deptid = dept @supervisor = boss @supervisor = boss @empid = @@nextid @empid = @@nextid @@nextid = @@nextid + 1@@nextid = @@nextid + 1 endend

Page 9: Reflexive Metaprogramming  in Ruby

99

Employee Class HierarchyEmployee Class HierarchyWriter MethodsWriter Methods

def def deptid=(dept)deptid=(dept) # deptid = dept # deptid = dept @deptid = dept@deptid = dept endend

def supervisor=(boss)def supervisor=(boss) @supervisor = boss@supervisor = boss endend

Page 10: Reflexive Metaprogramming  in Ruby

1010

Employee Class HierarchyEmployee Class HierarchyReader MethodsReader Methods

def def name name # not an attribute# not an attribute @lname + ", " + @fname@lname + ", " + @fname endend

def def empid;empid; @empid; end @empid; end

def deptid; @deptid; enddef deptid; @deptid; end

def supervisordef supervisor @supervisor@supervisor end end

Page 11: Reflexive Metaprogramming  in Ruby

1111

Employee Class HierarchyEmployee Class HierarchyString Conversion ReaderString Conversion Reader

def to_sdef to_s @empid.to_s + " : " + name [email protected]_s + " : " + name + " : " + @deptid.to_s + " (" + " : " + @deptid.to_s + " (" + @supervisor.to_s + ")" @supervisor.to_s + ")" endend

end # Employee end # Employee

Page 12: Reflexive Metaprogramming  in Ruby

1212

Employee Class HierarchyEmployee Class HierarchyAlternate InitializationAlternate Initialization

class Employeeclass Employee

@@nextid = 1@@nextid = 1

attr_accessor :deptid, :supervisorattr_accessor :deptid, :supervisor attr_reader :empidattr_reader :empid

def initialize(first,last,dept,boss) def initialize(first,last,dept,boss) # as before# as before endend

Page 13: Reflexive Metaprogramming  in Ruby

1313

Employee Class HierarchyEmployee Class HierarchyOtherOther Reader MethodsReader Methods

def namedef name @lname + ", " + @fname@lname + ", " + @fname end end

def to_sdef to_s @empid.to_s + " : " + name [email protected]_s + " : " + name + " : " + @deptid.to_s + " (" + " : " + @deptid.to_s + " (" + @supervisor.to_s + ")" @supervisor.to_s + ")" endend

end # Employee end # Employee

Page 14: Reflexive Metaprogramming  in Ruby

1414

Employee Class HierarchyEmployee Class HierarchyStaff SubclassStaff Subclass

class class Staff < EmployeeStaff < Employee attr_accessor :titleattr_accessor :title

def initialize(first,last,dept,def initialize(first,last,dept, boss,title) boss,title) supersuper(first,last,dept,boss)(first,last,dept,boss) @title = title @title = title endend

def to_s def to_s supersuper.to_s + ", " + @title.to_s.to_s + ", " + @title.to_s end end end # Staffend # Staff

Page 15: Reflexive Metaprogramming  in Ruby

1515

Employee Class HierarchyEmployee Class HierarchyUsing Employee ClassesUsing Employee Classes

class TestEmployee class TestEmployee def def TestEmployee.do_testTestEmployee.do_test @s1 = @s1 = Staff.newStaff.new("Robert", "Khayat",("Robert", "Khayat", "Law", nil, "Chancellor")"Law", nil, "Chancellor") @s2 = Staff.new("Carolyn", "Staton",@s2 = Staff.new("Carolyn", "Staton", "Law", @s1,"Provost")"Law", @s1,"Provost") puts "s1.class ==> " + @s1.puts "s1.class ==> " + @s1.classclass.to_s.to_s puts "s1.to_s ==> " + @s1.to_sputs "s1.to_s ==> " + @s1.to_s puts "s2.to_s ==> " + @s2.to_sputs "s2.to_s ==> " + @s2.to_s @s1.deptid [email protected] = "Chancellor" "Chancellor" puts "s1.to_s ==> " + @s1.to_sputs "s1.to_s ==> " + @s1.to_s puts "s1.methods ==> " + puts "s1.methods ==> " + @[email protected](", ").join(", ") endendend # TestEmployeeend # TestEmployee

Page 16: Reflexive Metaprogramming  in Ruby

1616

Employee Class HierarchyEmployee Class HierarchyTestEmployee.do_testTestEmployee.do_test Output Output

irbirbirb(main):001:0> load "Employee.rb"irb(main):001:0> load "Employee.rb"=> true=> trueirb(main):002:0> TestEmployee.do_testirb(main):002:0> TestEmployee.do_tests1.class ==> Staffs1.class ==> Staffs1.to_s ==> 1 : Khayat, Robert : Law (), s1.to_s ==> 1 : Khayat, Robert : Law (),

ChancellorChancellors2.to_s ==> 2 : Staton, Carolyn : Law (1 : s2.to_s ==> 2 : Staton, Carolyn : Law (1 :

Khayat, Robert : Law (), Chancellor), ProvostKhayat, Robert : Law (), Chancellor), Provosts1.to_s ==> 1 : Khayat, Robert : Chancellor (), s1.to_s ==> 1 : Khayat, Robert : Chancellor (),

ChancellorChancellors1.methods ==> to_a, respond_to?, display, s1.methods ==> to_a, respond_to?, display,

deptid, type, protected_methods, require, deptid, type, protected_methods, require, deptid=, title, … kind_of?deptid=, title, … kind_of?

=> nil=> nil

Page 17: Reflexive Metaprogramming  in Ruby

1717

Ruby MetaprogrammingRuby MetaprogrammingClass MacrosClass Macros

Every class has Every class has ClassClass object where object where instance methods resideinstance methods reside

Class definition is executable Class definition is executable Class Class ClassClass extends class extends class ModuleModule Instance methods of class Instance methods of class ModuleModule

available during definition of classesavailable during definition of classes Result is essentially “class macros”Result is essentially “class macros”

Page 18: Reflexive Metaprogramming  in Ruby

1818

Ruby MetaprogrammingRuby MetaprogrammingCodeCode String EvaluationString Evaluation

class_eval class_eval instance method ofinstance method of classclass Module Module– evaluates string as Ruby code evaluates string as Ruby code

– using context of class using context of class ModuleModule– enabling definition of new methods and constantsenabling definition of new methods and constants

instance_evalinstance_eval instance method of class instance method of class ObjectObject– evaluates string as Ruby codeevaluates string as Ruby code

– using context of the objectusing context of the object

– enabling statement execution and state changesenabling statement execution and state changes

Page 19: Reflexive Metaprogramming  in Ruby

1919

Ruby MetaprogrammingRuby MetaprogrammingImplementingImplementing attr_reader attr_reader

# Not really implemented this way# Not really implemented this wayclass Moduleclass Module # add to system class # add to system class defdef attr_reader attr_reader(*syms)(*syms) syms.each do |sym|syms.each do |sym| class_evalclass_eval %{def #{sym}%{def #{sym} @#{sym}@#{sym} end}end} end # syms.eachend # syms.each end # attr_readerend # attr_readerend # Moduleend # Module

Page 20: Reflexive Metaprogramming  in Ruby

2020

Ruby MetaprogrammingRuby MetaprogrammingImplementingImplementing attr_writer attr_writer

# Not really implemented this way# Not really implemented this wayclass Module # add to system classclass Module # add to system class def attr_writer(*syms)def attr_writer(*syms) syms.each do |sym|syms.each do |sym| class_eval %{def #{sym}class_eval %{def #{sym}=(val)=(val) @#{sym} @#{sym} = val= val end}end} endend end # attr_writerend # attr_writerend # Moduleend # Module

Page 21: Reflexive Metaprogramming  in Ruby

2121

Ruby MetaprogrammingRuby MetaprogrammingRuntime CallbacksRuntime Callbacks

class Employee # class definitions executableclass Employee # class definitions executable def def Employee.inheritedEmployee.inherited(sub) # class method(sub) # class method puts "New subclass: #{sub}" # of Classputs "New subclass: #{sub}" # of Class endend class Faculty < Employeeclass Faculty < Employee endend class Chair < Facultyclass Chair < Faculty endend

OUTPUTSOUTPUTS

New subclass: FacultyNew subclass: Faculty New subclass: Chair New subclass: Chair

Page 22: Reflexive Metaprogramming  in Ruby

2222

Ruby MetaprogrammingRuby MetaprogrammingRuntime CallbacksRuntime Callbacks

class Employeeclass Employee def def method_missing(meth,*args) method_missing(meth,*args) # instance method# instance method mstr = meth.to_s # of Objectmstr = meth.to_s # of Object last = mstr[-1,1]last = mstr[-1,1] base = mstr[0..-2]base = mstr[0..-2] if last == "="if last == "=" class_eval("attr_writer :#{base}")class_eval("attr_writer :#{base}") elseelse class_eval("attr_reader :#{mstr}")class_eval("attr_reader :#{mstr}") endend endendendend

Page 23: Reflexive Metaprogramming  in Ruby

2323

Domain Specific Languages Domain Specific Languages (DSL)(DSL)

Programming or description language Programming or description language designed for particular family of problemsdesigned for particular family of problems

Specialized syntax and semanticsSpecialized syntax and semantics Alternative approachesAlternative approaches– External language with specialized interpreterExternal language with specialized interpreter

– Internal (embedded) language by tailoring a Internal (embedded) language by tailoring a general purpose languagegeneral purpose language

Page 24: Reflexive Metaprogramming  in Ruby

2424

Martin Fowler DSL ExampleMartin Fowler DSL ExampleInput Data FileInput Data File

#123456789012345678901234567890123456#123456789012345678901234567890123456

SVCLFOWLER 10101MS0120050313SVCLFOWLER 10101MS0120050313

SVCLHOHPE 10201DX0320050315SVCLHOHPE 10201DX0320050315

SVCLTWO x10301MRP220050329SVCLTWO x10301MRP220050329

USGE10301TWO x50214..7050329USGE10301TWO x50214..7050329

Page 25: Reflexive Metaprogramming  in Ruby

2525

Martin Fowler DSL ExampleMartin Fowler DSL ExampleText Data DescriptionText Data Description

mapping SVCL dsl.ServiceCallmapping SVCL dsl.ServiceCall 4-18: CustomerName4-18: CustomerName 19-23: CustomerID19-23: CustomerID 24-27 : CallTypeCode24-27 : CallTypeCode 28-35 : DateOfCallString28-35 : DateOfCallString

mapping USGE dsl.Usagemapping USGE dsl.Usage 4-8 : CustomerID4-8 : CustomerID 9-22: CustomerName9-22: CustomerName 30-30: Cycle30-30: Cycle 31-36: ReadDate31-36: ReadDate

Page 26: Reflexive Metaprogramming  in Ruby

2626

Martin Fowler DSL ExampleMartin Fowler DSL ExampleXML Data DescriptionXML Data Description

<ReaderConfiguration><ReaderConfiguration> <Mapping Code = "SVCL" TargetClass = "dsl.ServiceCall"><Mapping Code = "SVCL" TargetClass = "dsl.ServiceCall"> <Field name = "CustomerName" start = "4" <Field name = "CustomerName" start = "4" end = "18"/>end = "18"/> <Field name = "CustomerID" start = "19" end = "23"/><Field name = "CustomerID" start = "19" end = "23"/> <Field name = "CallTypeCode" start = "24" <Field name = "CallTypeCode" start = "24" end = "27"/>end = "27"/> <Field name = "DateOfCallString" start = "28" <Field name = "DateOfCallString" start = "28" end = "35"/>end = "35"/> </Mapping></Mapping> <Mapping Code = "USGE" TargetClass = "dsl.Usage"><Mapping Code = "USGE" TargetClass = "dsl.Usage"> <Field name = "CustomerID" start = "4" end = "8"/><Field name = "CustomerID" start = "4" end = "8"/> <Field name = "CustomerName" start = "9" <Field name = "CustomerName" start = "9" end = "22"/>end = "22"/> <Field name = "Cycle" start = "30" end = "30"/><Field name = "Cycle" start = "30" end = "30"/> <Field name = "ReadDate" start = "31" end = "36"/><Field name = "ReadDate" start = "31" end = "36"/> </Mapping></Mapping></ReaderConfiguration></ReaderConfiguration>

Page 27: Reflexive Metaprogramming  in Ruby

2727

Martin Fowler DSL ExampleMartin Fowler DSL ExampleRuby Data DescriptionRuby Data Description

mapping('SVCL', ServiceCall) domapping('SVCL', ServiceCall) do extract 4..18, 'customer_name'extract 4..18, 'customer_name'extract 19..23, 'customer_ID'extract 19..23, 'customer_ID'extract 24..27, 'call_type_code'extract 24..27, 'call_type_code'extract 28..35, 'date_of_call_string'extract 28..35, 'date_of_call_string'

endendmapping('USGE', Usage) domapping('USGE', Usage) doextract 9..22, 'customer_name'extract 9..22, 'customer_name'extract 4..8, 'customer_ID'extract 4..8, 'customer_ID'extract 30..30, 'cycle'extract 30..30, 'cycle'extract 31..36, 'read_date‘extract 31..36, 'read_date‘

endend

Page 28: Reflexive Metaprogramming  in Ruby

2828

Martin Fowler DSL ExampleMartin Fowler DSL ExampleRuby DSL Class (1)Ruby DSL Class (1)

require 'ReaderFramework'require 'ReaderFramework'

class BuilderRubyDSLclass BuilderRubyDSL

def initialize(filename)def initialize(filename) @rb_dsl_file = filename@rb_dsl_file = filename endend

def configure(reader)def configure(reader) @reader = reader@reader = reader rb_file = File.new(@rb_dsl_file)rb_file = File.new(@rb_dsl_file) instance_eval(rb_file.readinstance_eval(rb_file.read, @rb_dsl_file), @rb_dsl_file) rb_file.close rb_file.close endend

Page 29: Reflexive Metaprogramming  in Ruby

2929

Martin Fowler DSL ExampleMartin Fowler DSL ExampleRuby DSL Class (2 of 3)Ruby DSL Class (2 of 3)

def mapping(code,target)def mapping(code,target) @cur_mapping = ReaderFramework::ReaderStrategy.new(@cur_mapping = ReaderFramework::ReaderStrategy.new( code,target)code,target) @reader.add_strategy(@cur_mapping)@reader.add_strategy(@cur_mapping) yieldyield endend

def extract(range,field_name)def extract(range,field_name) begin_col = range.beginbegin_col = range.begin end_col = range.endend_col = range.end end_col -= 1 if range.exclude_end? end_col -= 1 if range.exclude_end? @cur_mapping.add_field_extractor(@cur_mapping.add_field_extractor( begin_col,end_col,field_name)begin_col,end_col,field_name) endend

end#BuilderRubyDSLend#BuilderRubyDSL

Page 30: Reflexive Metaprogramming  in Ruby

3030

Martin Fowler DSL ExampleMartin Fowler DSL ExampleRuby DSL Class (3 of 3)Ruby DSL Class (3 of 3)

class ServiceCall; endclass ServiceCall; end

class Usage; endclass Usage; end

class TestRubyDSLclass TestRubyDSL def TestRubyDSL.rundef TestRubyDSL.run rdr = ReaderFramework::Reader.newrdr = ReaderFramework::Reader.new cfg = cfg = BuilderRubyDSL.newBuilderRubyDSL.new("dslinput.rb")("dslinput.rb") cfg.cfg.configureconfigure(rdr)(rdr) inp = File.new("fowlerdata.txt")inp = File.new("fowlerdata.txt") res = rdr.process(inp)res = rdr.process(inp) inp.closeinp.close res.each {|o| puts o.inspect}res.each {|o| puts o.inspect} endendendend

Page 31: Reflexive Metaprogramming  in Ruby

3131

Using Blocks and Iterators Using Blocks and Iterators Inverted Index (1)Inverted Index (1)

class InvertedIndexclass InvertedIndex @@wp = /(\w+([-'.]\w+)*)/ @@wp = /(\w+([-'.]\w+)*)/ DEFAULT_STOPS = {"the" => true, "a" => true, DEFAULT_STOPS = {"the" => true, "a" => true, "an" => true}"an" => true}

def initialize(*args)def initialize(*args) @files_indexed = []@files_indexed = [] @index = Hash.new@index = Hash.new @stops = Hash.new@stops = Hash.new if args.size == 1if args.size == 1 args[0].each {|w| @stops[w] = true}args[0].each {|w| @stops[w] = true} elseelse @stops = DEFAULT_STOPS@stops = DEFAULT_STOPS endend endend

Page 32: Reflexive Metaprogramming  in Ruby

3232

Using Blocks and Iterators Using Blocks and Iterators Inverted Index (2)Inverted Index (2)

def index_file(filename)def index_file(filename) unless @files_indexed.index(filename) == nilunless @files_indexed.index(filename) == nil STDERR.puts("#{filename} already indexed.")STDERR.puts("#{filename} already indexed.") returnreturn endend unless File.exist? Filenameunless File.exist? Filename STDERR.puts("#{filename} does not exist.")STDERR.puts("#{filename} does not exist.") returnreturn endend unless File.readable? Filenameunless File.readable? Filename STDERR. puts("#{filename} is not readable.")STDERR. puts("#{filename} is not readable.") returnreturn endend @files_indexed << filename@files_indexed << filename

Page 33: Reflexive Metaprogramming  in Ruby

3333

Using Blocks and Iterators Using Blocks and Iterators Inverted Index (3)Inverted Index (3)

inf = File.new(filename)inf = File.new(filename) lineno = 0lineno = 0 inf.each do |s|inf.each do |s| lineno += 1lineno += 1 words = s.scan(@@wp).map {|a| a[0].downcase}words = s.scan(@@wp).map {|a| a[0].downcase} words = words.reject {|w| @stops[w]}words = words.reject {|w| @stops[w]} words = words.map {|w| words = words.map {|w| [w,[filename,[lineno]]]}[w,[filename,[lineno]]]} words.each do |p| words.each do |p| @index[p[0]] = [] unless@index[p[0]] = [] unless @index.has_key? p[0]@index.has_key? p[0] @index[p[0]] = @index[p[0]].push(p[1])@index[p[0]] = @index[p[0]].push(p[1]) endend endend inf.closeinf.close

Page 34: Reflexive Metaprogramming  in Ruby

3434

Using Blocks and Iterators Using Blocks and Iterators Inverted Index (4)Inverted Index (4)

@index.each do |k,v| # k => v is hash [email protected] do |k,v| # k => v is hash entry @index[k] = v.sort {|a,b| a[0] <=> b[0]}@index[k] = v.sort {|a,b| a[0] <=> b[0]} endend @index.each do |k,v|@index.each do |k,v| @index[k] =@index[k] = v.slice(1...v.length).inject([v[0]]) v.slice(1...v.length).inject([v[0]]) do |acc, e|do |acc, e| if acc[-1][0] == e[0]if acc[-1][0] == e[0] acc[-1][1] = acc[-1][1] + e[1]acc[-1][1] = acc[-1][1] + e[1] elseelse acc = acc + [e]acc = acc + [e] endend accacc endend end#@index.each's blockend#@index.each's block selfself end#index_fileend#index_file

Page 35: Reflexive Metaprogramming  in Ruby

3535

Using Blocks and Iterators Using Blocks and Iterators Inverted Index (5)Inverted Index (5)

def lookup(word)def lookup(word) if @index[word]if @index[word] @index[word].map {|f| @index[word].map {|f| [f[0].clone, f[1].clone] } [f[0].clone, f[1].clone] } elseelse nil nil endend endend # …# …endend

Page 36: Reflexive Metaprogramming  in Ruby

3636

QuestionsQuestions