BoxScript: A Component-Oriented Language for Teaching

Download Report

Transcript BoxScript: A Component-Oriented Language for Teaching

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
2
Reflexive Metaprogramming
Languages

Early
–
–

Lisp
Smalltalk
More recent
–
–
–
Ruby
Python
Groovy
3
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)
4
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
5
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)
6
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
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
@@nextid = @@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
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
end # Employee
13
Employee Class Hierarchy
Staff 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
14
Employee Class Hierarchy
Using 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
15
Employee Class Hierarchy
TestEmployee.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
16
Ruby Metaprogramming
Class 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”

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}
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
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:
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
end
22
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
23
Martin Fowler DSL Example
Input Data File
#123456789012345678901234567890123456
SVCLFOWLER
10101MS0120050313
SVCLHOHPE
10201DX0320050315
SVCLTWO
x10301MRP220050329
USGE10301TWO
x50214..7050329
24
Martin Fowler DSL Example
Text 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
25
Martin Fowler DSL Example
XML 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>
26
Martin Fowler DSL Example
Ruby 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
27
Martin Fowler DSL Example
Ruby 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
28
Martin Fowler DSL Example
Ruby 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
29
Martin Fowler DSL Example
Ruby DSL Class (3 of 3)
class ServiceCall;
class Usage;
end
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
30
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
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
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
end#@index.each's block
self
end#index_file
34
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
35
Questions
36