Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

metaprograming String#scan and globals?

My goal is to replace methods in the String class with other methods that do additional work (this is for a research project). This works for many methods by writing code in the String class similar to

alias_method :center_OLD, :center
def center(args*)
  r = self.send(*([:center_OLD] + args))
  #do some work here 
  #return something
end

For some methods, I need to handle a Proc as well, which is no problem. However, for the scan method, invoking it has the side effect of setting special global variables from the regular expression match. As documented, these variables are local to the thread and the method.

Unfortunately, some Rails code makes calls to scan which makes use of the $& variable. That variable gets set inside my version of the scan method, but because it's local, it doesn't make it back to the original caller which uses the variable.

Does anyone know a way to work around this? Please let me know if the problem needs clarification.

If it helps at all, all the uses I've seen so far of the $& variable are inside a Proc passed to the scan function, so I can get the binding for that Proc. However, the user doesn't seem to be able to change $& at all, so I don't know how that will help much.

Current Code

class String
  alias_method :scan_OLD, :scan
  def scan(*args, &b)
    begin

      sargs = [:scan_OLD] + args

      if b.class == Proc
        r = self.send(*sargs, &b)
      else
        r = self.send(*sargs)
      end
      r

    rescue => error
      puts error.backtrace.join("\n")
    end
  end
end

Of course I'll do more things before returning r, but this even is problematic -- so for simplicity we'll stick with this. As a test case, consider:

"hello world".scan(/l./) { |x| puts x }

This works fine both with and without my version of scan. With the "vanilla" String class this produces the same thing as

"hello world".scan(/l./) { puts $&; }

Namely, it prints "ll" and "ld" and returns "hello world". With the modified string class it prints two blank lines (since $& was nil) and then returns "hello world". I'll be happy if we can get that working!

like image 427
bchurchill Avatar asked Oct 26 '13 21:10

bchurchill


People also ask

What is metaprogramming used for?

Metaprogramming can be used to move computations from run-time to compile-time, to generate code using compile time computations, and to enable self-modifying code. The ability of a programming language to be its own metalanguage is called reflection.

What is a metaprogramming language?

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. This means that a program can be designed to read, generate, analyze, or transform other programs, and even modify itself while running.

What is the point of template metaprogramming?

Template metaprogramming is a programming technique that uses templates as blueprints for the compiler to generate code and help developers avoid writing repetitive code.


1 Answers

You cannot set $&, because it is derived from $~, the last MatchData. However, $~ can be set and that actually does what you want. The trick is to set it in the block binding.

The code is inspired by the old Ruby implementation of Pathname.
(The new code is in C and does not need to care about Ruby frame-local variables)

class String
  alias_method :scan_OLD, :scan
  def scan(*args, &block)
    sargs = [:scan_OLD] + args

    if block
      self.send(*sargs) do |*bargs|
        Thread.current[:string_scan_matchdata] = $~
        eval("$~ = Thread.current[:string_scan_matchdata]", block.binding)
        yield(*bargs)
      end
    else
      self.send(*sargs)
    end
  end
end

The saving of the thread-local (well, actually fiber-local) variable seems unnecessary since it is only used to pass the value and the thread never reads any other value than the last one set. It probably is there to restore the original value (most likely nil, because the variable did not exist).

One way to avoid thread-locals at all is to create a setter of $~ as a lambda (but it does create a lambda for each call):

self.send(*sargs) do |*bargs|
  eval("lambda { |m| $~ = m }", block.binding).call($~)
  yield(*bargs)
end

With any of these, your example works!

like image 51
eregon Avatar answered Oct 04 '22 23:10

eregon