Anaphoric Procs
Published .
Tags: computer-science, ruby
If you write a fair amount of Ruby, I’ll bet you use Symbol#to_proc all the time. If you want to reverse every string in an array, for example, you probably do something like:
["foo", "bar", "baz"].map(&:reverse) # => ["oof", "rab", "zab"]
This works because there’s a unary function & in Ruby which is syntactic sugar for #to_proc, so &:reverse is equivalent to :reverse.to_proc.
Now, suppose you want to reverse and upcase every string in an array. You could chain two maps, which in practice is what I usually do:
["foo", "bar", "baz"].map(&:reverse).map(&:upcase) # => ["OOF", "RAB", "ZAB"]
You could also fall back on block syntax:
["foo", "bar", "baz"].map { |x| x.reverse.upcase } # => ["OOF", "RAB", "ZAB"]
I don’t really like either one of these. The first requires looping twice over the same structure, and block syntax means introducing a dummy variable x. It would be nice if we had the advantages of both.
Luckily, we can build a solution. Ruby’s no Lisp, but metaprogramming still gives us quite a lot of leverage to introduce new constructs. First, though, we should talk about anaphora.
Anaphora are a concept that’s been borrowed from linguistics. It literally means “carrying back,” like when a phrase is repeated over and over for emphasis. In programming languages, though, anaphora are expressions that refer back to an object that was previously mentioned; they’re like using a pronoun like “it,” as in, “He hit the ball and knocked it out of the park.”
Let’s introduce that concept to Ruby!
class Anaphora def initialize(proc = -> (x) { x }) @proc = proc end def to_proc @proc end def method_missing(*args) Anaphora.new(-> (x) { to_proc.call(x).send(*args) }) end end class Object def _ Anaphora.new end end
And trying it out:
["foo", "bar"].map(&_.upcase.reverse) # => ["OOF", "RAB"]
OK, wait, what did we do there? Let’s skip the Anaphora class for the moment and examine the effects first.
First, notice that Object#_ has been monkey-patched to return a new instance of Anaphora. Since Object is nigh-universally available, we can now call _ just about anywhere in our code to get a new Anaphora object.
In the argument to map in the last example, we called _ to create a new Anaphora instance, called upcase on it, called reverse on the result of upcase, and finally applied & to the whole thing, which is just the same as calling to_proc.
I’d never introduce this to any codebase that someone other than me had to maintain (and probably not even that!) but it’s a fun example of what a flexible little language Ruby is!