#from http://weblog.raganwald.com/2007/03/approach-to-composing-domain-specific.html
#!/usr/bin/env ruby
#
# Created by Reginald Braithwaite on 2007-03-11.
# Copyright (c) 2007. All rights reserved.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2004 Sam Hocevar
# 22 rue de Plaisance, 75014 Paris, France
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
# A Domain Specific Language is used to introduce a new scope with an embedded set of methods.
#
# The idea is to avoid polluting the global namespace. Instead of adding methods to Kernel, we
# add methods to a new DomainSpecificLanguage, and then we can evaluate code with the new language
# using the #eval class method or using the #with method added to Kernel.
#
# For a similar approach, see http://www.infoq.com/articles/eval-options-in-ruby
class DomainSpecificLanguage
# See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
def metaclass; class << self; self; end; end
def meta_eval &blk; metaclass.instance_eval &blk; end
# Adds methods to a metaclass
def meta_def name, &blk
meta_eval { define_method name, &blk }
end
# See http://onestepback.org/index.cgi/Tech/Ruby/RubyBindings.rdoc
class ReadOnlyReference
def initialize(var_name, vars)
print "in ROR initialize, var_name: #{var_name}\n"
@getter = eval "lambda { #{var_name} }", vars
print " @getter.call: #{@getter.call}\n"
end
def value
@getter.call
end
end
attr_reader :outer_binding, :outer_self
# instances of a DomainSpecificLanguage are created each time
# a block is evaluated with that language. The instance is
# intialized with the block's binding.
def initialize(given_binding)
print "in DSL initialize \n"
print " given_binding: #{given_binding}\n"
@outer_binding = given_binding
print " eval :self given_binding: ", eval("#{:self}",given_binding),"\n"
@outer_self = ReadOnlyReference.new(:self, given_binding)
end
# some jiggery-pokery to access methods defined in the block's
# scope, because when the block is evaluated with the DomainSpecificLanguage,
# we use #instance_eval to set self to the DomainSpecificLanguage
# instance.
def method_missing(symbol, *args, &block)
p "missing: ",symbol, "args: ", args
if args.empty?
print " args empty\n"
r = ReadOnlyReference.new(symbol, outer_binding)
meta_def(symbol) { r.value }
r.value
else
print "method_missing else: ", outer_self.value, "\n"
print outer_self.value.class, "\n"
p xx=outer_self.value.send( symbol, *args, &block)
xx
end
end
class << self
# Evaluates a block in the context of a new DomainSpecificlanguage
# instance.
def eval &block
print "in eval -- self: ",self, "\n"
print "block.binding: #{block.binding}\n"
print " eval self block.binding: #{Kernel.eval("self",block.binding)}\n"
# print " eval x test_uses_method:
# #{Kernel.eval("x",Kernel.eval("self",block.binding))}\n"
print "finish: ",zz = new(block.binding).instance_eval(&block),"\n"
zz
end
end
end
# We open Kernel and add just one method for introducing DomainSpecificLanguages
module Kernel
# Evaluate a block with a DomainSpecificLanguage
def with dsl_class, &block
p "in with"
dsl_class.eval(&block)
end
end
# Let is a DomainSpecificLanguage that actually creates DomainSpecificLanguages.
#
# Let works a lot like let in Scheme. Your provide a hash of names and value
# expressions. The value expressions are evaluated in the outer scope, and then we have
# a little domain specific language wher ethe inner scope has the names all bound to the
# values. E.g.
#
# with Let do
# let :x => 100, :y => 50 do
# print "#{x + y} should equal fifty"
# end
# end
#
#
# Within the Let DomainSpecificLanguage, you can declare multiple let statements and nest
# them as you please.
#
# One important limitation: you cannot bind a value to a name that is already a local variable.
class Let < DomainSpecificLanguage
class Scope < DomainSpecificLanguage
# initializes a Scope. In addition to the outer binding, we also pass in the
# hash of names and values. Note the check to ensure we are not trying to
# override a lcoal variable.
def initialize given_binding, let_clauses = {}
print "in Scope.initialize\n"
let_clauses.each do |symbol, value|
var_name = symbol.to_s
raise ArgumentError.new("Cannot override local #{var_name}") if eval("local_variables", given_binding).detect { |local| local == var_name }
meta_eval { attr_accessor(var_name) }
send "#{var_name}=", value
end
p eval("#{:self}",given_binding)
super(given_binding)
end
end
# Define a new Scope: you're really defining a new DomainSpecificLanguage
def let let_clauses = {}, &block
p "in let: ", let_clauses
print " block.binding: #{block.binding}\n"
Scope.new(block.binding, let_clauses).instance_eval(&block)
end
class << self
# If you just want a one-off
# def eval let_clauses = {}, &block
# Scope.new(block.binding, let_clauses).instance_eval(&block)
# end
end
end
# A DomainSpecificDelegator is a DSL that delegates methods to a class or object.
# The main use is to separate the mechanics of scoping from the methods of a utility
# class.
class DomainSpecificDelegator < DomainSpecificLanguage
class << self
# insert one or more #delegate_to calls in the class definition, giving a receiver
# and a hash. Each hash pair is of the form verb => method where verb is the
# name you will use in the DSL and method is the method in the receiver that will handle
# it.
def delegate_to receiver, method_hash
@@delegations ||= {}
method_hash.each { |verb, method_name| @@delegations[verb.to_s] = [receiver, method_name.to_s] }
end
end
def method_missing symbol, *args, &block
receiver, method_name = *@@delegations[symbol.to_s]
if receiver
receiver.send method_name, *args, &block
else
super(symbol, *args, &block)
end
end
end
#!/usr/bin/env ruby
#
# Created by Reginald Braithwaite on 2007-03-11.
# Copyright (c) 2007. All rights reserved.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2004 Sam Hocevar
# 22 rue de Plaisance, 75014 Paris, France
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
require "test/unit"
=begin
class DefinesVariable < DomainSpecificLanguage
def y
5
end
end
class DefinesMethodWithBlock < DomainSpecificLanguage
def say something
something
end
end
class TestDomainSpecificLanguage < Test::Unit::TestCase
def test_simple_read
x = 100
assert_equal(500, DomainSpecificLanguage.eval { x * 5 })
end
def test_define_reader
x = 100
assert_equal(500, DefinesVariable.eval { x * y })
end
def test_define_method
assert_nothing_raised(Exception) { DefinesMethodWithBlock.eval { say 'hello' } }
end
def test_uses_lambda
double = lambda { |x| x * 2 }
assert_equal(8, DomainSpecificLanguage.eval { double.call(4) })
end
def square x
x * x
end
def test_uses_method
p eval("square(4)", binding)
p self.square(4)
x = 333
p DomainSpecificLanguage.instance_eval{x}
# p DomainSpecificLanguage.instance_eval{square(4)}
assert_equal(16, DomainSpecificLanguage.eval { square(4) })
end
end
class DomainSpecificDelegatorTestHelper < DomainSpecificDelegator
delegate_to "hello", :say => :to_s
end
class TestDomainSpecificDelegator < Test::Unit::TestCase
def test_say
with DomainSpecificDelegatorTestHelper do
assert_equal('hello', say)
end
end
def world
'world'
end
def test_outer_scope_method
with DomainSpecificDelegatorTestHelper do
assert_equal('hello world', "#{say} #{world}")
end
end
end
=end
class TestLet < Test::Unit::TestCase
=begin
def test_cannot_try_to_override_locals
with Let do
local_var = :snafu
assert_raise(ArgumentError) do
let :local_var => :fubar do
# fail();
end
end
end
end
def test_scoping
print "in test scoping --self: #{self}\n"
with Let do
print "in test_scoping::Let -- self: #{self}\n"
let :x => 1, :y => 2 do
print "in test_scoping::let -- self: #{self}\n"
assert_equal(1, x)
assert_equal(2, y)
end
end
end
def test_access_to_outer_locals
with Let do
x = 1
let :y => 2 do
assert_equal(3, x + y)
end
end
end
def z
-1
end
def test_access_to_outer_methods
with Let do
let :x => 1 do
assert_equal(0, x + z)
end
end
end
=end
def test_nesting
with Let do
let :x => 0, :y => 1 do
assert_equal(1, x + y)
let :x => 2 do
assert_equal(3, x + y)
end
assert_equal(0, x)
end
end
end
=begin
def test_assignment
with Let do
let :x => 0, :y => 1 do
assert_equal(1, x + y)
let :x => 2 do
assert_equal(3, x + y)
x = 5
assert_equal(5, x)
assert_equal(2, x()) # assignment creates a local that shadows the method.
self.x = 7
assert_equal(5, x)
assert_equal(7, x()) # you can hack around the read only nature, but it's not pretty
end
assert_equal(0, x)
end
end
end
def test_let_and_block_parameters
with Let do
let :x => nil do
(1..10).each { |x| assert !x.nil? }
assert_nil(x)
end
end
end
attr_accessor :w
def test_outer_assigment
w = 5
with Let do
let :x => 5 do
assert_equal(25, w * x)
w = 7
assert_equal(35, w * x)
end
end
assert_equal(7, w)
end
=end
end