Robin Schwartz


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!