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