Let's say I'm designing a domain-specific language. For this simplified example, we have variables, numbers, and the ability to add things together (either variables or numbers)
class Variable
attr_reader :name
def initialize(name)
@name = name
end
end
class Addition
attr_reader :lhs, :rhs
def initialize(lhs, rhs)
@lhs = lhs
@rhs = rhs
end
end
Now, if I want to represent the expression x + 1, I write Addition.new(Variable.new('x'), 1).
We can make this more convenient by providing a + method on Variable.
class Variable
def +(other)
Addition.new(self, other)
end
end
Then we can write Variable.new('x') + 1.
But now, suppose I want the opposite: 1 + x. Obviously, I don't want to monkey-patch Integer#+, as that disables ordinary Ruby integer addition permanently. I thought this would be a good use case for refinements.
Specifically, I want to define a method expr which takes a block and evaluates that block in a context where + is redefined to construct instances of my DSL. That is, I want something like
module Context
refine Integer do
def +(other)
Addition.new(self, other)
end
end
end
def expr(&block)
Context.module_eval(&block)
end
So that, ideally, expr { 1 + Variable.new('x') } would result in the DSL expression Addition.new(1, Variable.new('x')).
However, it seems that Ruby refinements are quite fickle, and module_eval'ing into a scope with an active refinements does not activate that refinement inside the block, as I was hoping it would. Is there a way to use module_eval, instance_eval, etc. to activate a refinement inside a particular Ruby block?
I realize that I could wrap integers in a IntegerExpr class and provide + on that. However, this is Ruby, and the sky is the limit with metaprogramming, so I'm curious if it can be done with ordinary Ruby Integer instances. I want to define a method expr such that, in
expr { 1 + Variable.new('x') }
the + inside the block is a refinement-defined Integer#+, even if that refinement is not active at the call site of expr. Is this possible?