4

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?

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • Operating under the assumption that this question is contextually accurate to your situation you can solve this explicit issue in a much cleaner fashion by defining `Variable#coerce`. By doing so both `Variable.new('x') + 1` and `1 + Variable.new('x')` will work accordingly [Example](https://replit.com/@engineersmnky/GreatHappyUsername#main.rb) – engineersmnky Jun 27 '21 at 02:47
  • 1
    Operating under the assumption this is merely a boiled down example for context I am unaware of anyway you could use a refinement in the desired fashion (other than `eval` with a string which is generally not recommended). Refinements are lexically scoped and activated in a very limited scope as well; essentially between the `using` statement and the end of the class or end of the file that contains this statement and nowhere else. The [Docs](https://docs.ruby-lang.org/en/3.0.0/doc/syntax/refinements_rdoc.html) do a fairly good job of explaining and illustrating these "limitations". – engineersmnky Jun 27 '21 at 03:11
  • I wasn't actually aware of `coerce`, so that is helpful. It doesn't fully solve the problem in my current codebase (there are situations where I want to apply `+` to two built-in types, say a number and a hash, perhaps), but if worst comes to worst I can probably make it work pretty well. I'm still interested in knowing if there's a way to accomplish my original question though. Thanks for the tips! – Silvio Mayolo Jun 27 '21 at 03:57
  • 1
    As I mentioned I am not aware of a way to accomplish your original question simply because that is not how refinements are scoped. This part is really the issue "...even if that refinement is not active at the call site of expr." and the answer to is this possible is No, to the best of my knowledge. – engineersmnky Jun 27 '21 at 13:33

0 Answers0