public
Last active

  • Download Gist
Example.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
## Ruby Quiz #666
 
module CompileSite; end
 
def compile(name, source)
CompileSite.class_eval <<-RUBY
def #{name}(locals)
Thread.current[:tilt_vars] = [self, locals]
class << self
this, locals = Thread.current[:tilt_vars]
this.instance_eval do
#{source}
end
end
end
RUBY
end
 
def render(name, scope, locals, &blk)
scope.__send__(name, locals, &blk)
end
 
module Helper
A = "A"
end
 
class Scope
include CompileSite
include Helper
B = "B"
end
 
class ScopeB < Scope
B = 'C'
end
 
compile('hello', <<-RUBY)
if locals.nil?
yield
else
[locals, A, B, yield, render(:hello, self, nil) { 'Y2' }, yield, locals].join(" ")
end
RUBY
 
CompileSite.instance_method(:hello)
 
res = render(:hello, Scope.new, "L") { "Y1" }
res2 = render(:hello, ScopeB.new, "L") { "Y1" }
 
if res == "L A B Y1 Y2 Y1 L" and res2 == "L A C Y1 Y2 Y1 L"
puts "YOU DID IT!"
else
puts "Failed:", res, res2
end

(I've updated the gist so it's closer to the original Tilt code)

raggi: why does the method have to be defined there? this seems like a violation of rubys constant lookup, CompileSite -> Helper

raggi: i vote for the method being defined on Scope so that it has "normal" semantics

You'll have to look at this in the context of template engines:

A fast template engine will compile the template into plain Ruby. For instance: Hello <%= world %> could be compiled into "Hello #{world}". In order to evaluate such a template you could have something like this:

class Template
  def render(scope, locals)
    # You need the Kernel.eval because of a bug in JRuby
    scope.instance_eval { Kernel.eval(@compiled_source, binding) }
  end
end

This passes every test above. BUT: it's quite slow because Ruby has to parse the string every time. This can be fixed by defining it as method instead (and then render will call that method). But we still want it to be able to pass in any scope you want: render("hello"), render(Request.new) and render(Object.new) should still work. In order to do that we had two choices: define the method under Object (which is available everywhere) or define the method under Tilt::CompileSite (which you can include if you want fast rendering). And we don't want to pollute Object with tons of methods.

Currently, render basically looks like this:

class Template
  def render(scope, locals, &blk)
    if scope.respond_to?(:__tilt__) # this method is defined in CompileSite
      compile! unless compiled?
      scope.__send__(method_name, locals, &blk)
    else
      # instance_eval version above
    end
  end
end

This means that if you simply include Tilt::CompileSite in your class, render(YourClass.new) will evaluate several times faster.

So even though we define the method under Tilt::CompileSite, we want it to work like it was defined under whatever scope you pass in to #render. My first patch to fix the constant lookup wasn't quite optimal, because it ran the code under the constant scope of the first render call, so render(Foo.new) + render(Bar.new) would both use Foo's constant scope. It wasn't perfect, but it was certainly better than having constant lookup relative to Tilt::CompileSite.

Your suggestion to define the method under each scope has a few disadvantages:

  • It would need to define a separate method each time you render it under a different scope.
  • If you render it under Object.new it will actually define the method on Object and thus pollute the namespace.

As far as defining a method on each scope class, for the most part, in real world scenarios, that wouldn't bother me, it'd get defined for each 'controller' that renders a given template. To me that's relatively reasonable, although I can see for something that was partial heavy, this could bloat.

I think this latest solution using instance eval in the metaclass is the better one out of the lot. The problem is, it bugs out on JRuby, RBX, and 1.9.1. JRuby has issues with ScopeB, it looks up Scope::B instead of ScopeB::B. RBX and 1.9.1 raise local jump errors because the yield context is dropped in the metaclass. I get the feeling Evans going to hate this... ;-)

Interesting stuff :-)

I should also mention that it currently defines different methods depending on the locals you pass in. It actually "unrolls" the locals so the source actually looks like this:

foo = locals[:foo]
bar = locals[:bar]
#{original_source}

So if we define the method directly on the scope, it means that a total of L * S methods will be defined (where L is the number of variations of lvars and S is the number of variations of scope).

Yeah, I had a feeling you'd simplified that from the original. That's fine... :-)

Good work on the bug reports!

JRuby is weird. This works:

class Base
  class_eval <<-RUBY
    def hello
      class << self
        yield
      end
    end
  RUBY
end

p Base.new.hello { 123 }

But this does not:

class Base
  #class_eval <<-RUBY
    def hello
      class << self
        yield
      end
    end
  #RUBY
end

p Base.new.hello { 123 }

Yay! The bugs in Rubinius are now closed :-)

Merged. Passes all tests on 1.8.7. I'm pulling down the latest 1.9.1 now. Review appreciated.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.