Monkey-Patching, Single Responsibility Principle, and Scala Implicits

Feb 09, 2010

When it's impossible to extend core classes, there's no choice but to write a whole bunch of classes with names like StringUtil to house your utility methods. Every namespace winds up having at least one StringUtil class, and it gets really ugly.

In ruby, it's possible to add methods to absolutely any class including String, Integer and other core classes. Rather than calling StringUtil.pluralize("monkey"), you call "monkey".pluralize. The technique is known as monkey-patching. Compared to utility classes, it's a hell of a lot more convenient, and it reads better.

But, monkey-patching isn't without flaw. When you add a method to String, you add it to everybody's String. You're polluting a global namespace.

A lot of rubyists will tell you that in practice, monkey-patching related namespace pollution isn't a problem, and they're mostly right. But, sometimes, they're wrong. The json gem doesn't work properly with activesupport, for example. That's a real problem that gives me real headaches on a daily basis.

In his CUSEC talk, and follow-up blog post, Reg Braithwaite raises the issue that monkey-patching Integer to add duration methods (like 1.hour or is a violation of single responsibility principle. He goes on to justify monkey-patching somewhat:

A Ruby program with extensive metaprogramming is a meta-program that writes a target program. The target program may have classes that groan under the weight of monkey patches and cross-cutting concerns. But the meta-program might be divided up into small, clean entities that each have a single responsibility...In effect, the target program's classes and methods have many responsibilities, but they are assembled by the meta-program from smaller modules that each have a single responsibility.

I think this is a mostly adequate justification for violating SRP. But, needing such an in depth justification at all feels wrong. There must be a better way.

It turns out there is, in scala at least, in the form of something called implicits. Implicits are a way to tell the scala compiler how to convert from one type to another in the event that a method is needed from the second type. Implicits can be definted at (or imported in to) arbitrary scope, and their effects are entirely localized. In fact, they don't do anything at all unless they're needed.

So, let's say we wanted to add a k-combinator type method (like ruby's Object#tap) to all scala objects. First, we need a class that implements the tap method in a way that can be applied to any type.

class Tapper[A](tapMe: A) {
  def tap(f: (A) => Unit): A = {

This class has one method that taps the object supplied to the class's constructor. The details of its implementation are interesting, but unimportant here. This REPL session should explain everything:

scala> val tapper = new Tapper("hello!")
tapper: Tapper[java.lang.String] = Tapper@5e53bbfa

scala> tapper.tap { s => println(s) }
res0: java.lang.String = hello!

Now we need to get objects responding to #tap. We do that by defining an implicit.

object Tap {
  implicit def any2Tapper[A](toTap: A): Tapper[A] = new Tapper(toTap)

We wrap the implicit's definition in an object, which is scala for singleton. Those methods can then be imported in to arbitrary scope using the import statement. This REPL session should make everything clear:

scala> "hello!".tap { s => println(s) }
<console>:5: error: value tap is not a member of java.lang.String
       "hello!".tap { s => println(s) }

scala> import Tap._
import Tap._

scala> "hello!".tap { s => println(s) }
res2: java.lang.String = hello!

This Tapper certainly doesn't violate SRP. The addition of the #tap method to all Objects is localized and doesn't affect other running code. Libraries your code depends on can implement their own tap method without collision.

In the end, Reg's argument about meta-programming might still apply here. But, if it does, the kind of meta-programming introduced by implicits is limited in scope and prevents the kind of global namespace pollution that can bite you in the ass in ruby. And the resulting code doesn't have to violate SRP.

In practice, I've found scala's implicits a lot more pleasant to work with than ruby's monkey patching. They're more explicit which provides additional clarity without sacrificing much in the way of terseness. Like anything else, scala isn't a perfect language, but implicit type conversions are a really elegant solution to an ugly problem.