Ruby refinements subtleties
Asked Answered
B

1

7

There is a pretty good documentation of the current implementation of refinements in ruby here: http://ruby-doc.org//core-2.2.0/doc/syntax/refinements_rdoc.html, but there are some strange corner cases.

First, include module is orthogonal to using module (one include the instance method of module while the other activates the refinement). But there is a trick to include a refinement module itself, see Better way to turn a ruby class into a module than using refinements?.

def to_module(klass)
  Module.new do
    #note that we return the refinement module itself here
    return refine(klass) {
      yield if block_given?
    }
  end
end

class Base
  def foo
    "foo"
  end
end
class Receiver
  include to_module(Base) {
    def foo
      "refined " + super
    end
  }
end
Receiver.new.foo #=> "refined foo"

Strangely this refinement module can't be used with using!

m=to_module(Base) {}
m.class #=> Module
using m    
#=>TypeError: wrong argument type Class (expected Module)

So using only work on the enclosing module of the refinement modules. Secondly I wanted to use the above yield trick to be able to pass a Proc to refine (even through it only accepts a block), without resorting to converting the Proc back to source as in https://www.new-bamboo.co.uk/blog/2014/02/05/refinements-under-the-knife/. But using yield as in the include example does not work:

def ref_module1(klass)
  Module.new do
    refine(klass) {
      yield
    }
  end
end

class Receiver1
  using ref_module1(Base) {
    def foo
      "refined " + super
    end
  }
  def bar
    Base.new.foo
  end
end
Receiver1.new.bar #=> NoMethodError: super: no superclass method `foo'

We see that Receiver1 still use Bar#foo and not the refined method. Howewer we can use module_eval instead:

def ref_module2(klass,&b)
  Module.new do
    refine(klass) {
      module_eval(&b)
    }
  end
end

class Receiver2
  using ref_module2(Base) {
    def foo
      "refined " + super
    end
  }
  def bar
    Base.new.foo
  end
end
Receiver2.new.bar #=> "refined foo"

I don't quite understand why module_eval works here and not the yield method. Inside the refinement block, the 'default_definee' is the refinement module, so module_eval which puts the 'default_definee' to self='the refinement module' should not affect it. And indeed in the 'include' example at the beginning, I get the same result when I use module_eval or a direct yield.

Can anyone explain this behavior?

Barkley answered 21/2, 2015 at 18:18 Comment(1)
Great question. I hope you get some equally-great answers.Caracul
T
4

Context (or binding) is the reason why module_eval works and yield doesn't in your last set of examples. It actually has nothing to do with refinements, as demonstrated below.

Starting with module_eval:

class Foo
  def run(&block)
    self.class.module_eval(&block)
  end
end

foo = Foo.new
foo.run {
  def hello
    "hello"
  end
}

puts foo.hello # => "hello"
puts hello => # '<main>': undefined method 'hello' for main:Object (NameError)

In Foo#run we call module_eval on Foo. This switches the context (self) to be Foo. The result is much like we had simple defined hello inside of class Foo originally.

Now let's take a look at yield:

class Foo
  def run
    yield
  end
end

foo = Foo.new
foo.run {
  def hello
    "hello"
  end
}

puts hello # => "hello"
puts foo.hello # => '<main>': private method 'hello' called for ...

yield simply invokes the block in its original context, which in this example would be <main>. When the block is invoked, the end result is exactly the same as if the method were defined at the top level normally:

class Foo
  def run
    yield
  end
end

foo = Foo.new

def hello
  "hello"
end

puts hello # => "hello"
puts foo.hello # => '<main>': private method 'hello' called for ...

You might notice that foo seems to have the hello method in the yield examples. This is a side effect of defining hello as a method at the top level. It turns out that <main> is just an instance of Object, and defining top level methods is really just defining private methods on Object which nearly everything else ends up inheriting. You can see this by opening up irb and running the following:

self       # => main
self.class # => Object

def some_method
end

"string".method(:some_method) # => #<Method: String(Object)#some_method>

Now back to your examples.

Here's what happens in the yield example:

def ref_module1(klass)
  Module.new do
    refine(klass) {
      yield
    }
  end
end

class Receiver1
  # like my yield example, this block is going to
  # end up being invoked in its original context
  using ref_module1(Base) {
    def foo
      "I'm defined on Receiver1"
    end
  }

  def bar
    # calling foo here will simply call the original
    # Base#foo method
    Base.new.foo
  end
end

# as expected, if we call Receiver1#bar
# we get the original Base#foo method
Receiver1.new.bar # => "foo"

# since the block is executed in its original context
# the method gets defined in Receiver1 -- its original context
Receiver1.new.foo # => "I'm defined on Receiver1"

As for module_eval, it works in your examples because it causes the block to be run in the context of the new module, rather than on the Receiver1 class.

Twentyone answered 10/7, 2015 at 5:12 Comment(1)
You are right, since the block is a closure it will contains the current values of 'self' and 'klass' so we need to use module_eval to set 'klass' to the anonymous module. I was confused by the fact that the first example was working, but actually 'foo' is defined on Receiver and not on the anonymous module as I thought. Thanks!Barkley

© 2022 - 2024 — McMap. All rights reserved.