Classboxes

I’m not convinced classboxes actually work.

I was reading up on safer approaches to monkeypatching and while looking into refinements in ruby I reread the classboxes article by Bergel, Ducasse, and Wuyts. This time I had some concrete uses in mind so I tried applying their lookup algorithm to those, and the way they behaved I’m not sure would work well in practice. This post assumes you’re already familiar with the classboxes article, but maybe it’s been a while so here’s a quick reminder.

Monkeypatching, as used in practice in languages like ruby, means adding methods to existing classes such that they’re visible across the whole program. This is obviously dangerous. Classboxes is intended as a more well-behaved way of doing the same thing. The extension is locally scoped and only visible to the parts of the program that explicitly import it.

The way classboxes work, basically, is that instead of the method dictionary of a class being a mapping from the name of a method to its implementations, it becomes a mapping from the name and the classbox that defined it to the implementation. So you’re still actually monkeypatching the entire program, but because methods are now tagged with their classbox they can be considered or ignored as appropriate, based on where the method you’re looking for is being called. If you’re making a call in a particular class box, and you’ve imported some other classbox, directly or indirectly, then the lookup will consider methods from that other classbox. Otherwise not. (Well, sort of, more or less). Okay, so here’s a concrete example of how it works and maybe doesn’t work.

Imagine you have a simple core library in one classbox that, among other basic types, defines a string type: (Don’t get hung up on the syntax, it’s just pseudocode).

classbox core {

  class String {
    # Yields the i'th character of this string.
    def this[i] => ...

    # The number of characters in this string.
    def this.length => ...
  }

  ...

}

Let’s say you also have a simple collection library in another classbox that defines a simple array type,

classbox collection {

  import core;

  class Array {
    # Yields the i'th element of this array.
    def this[i] => ...

    # The number of elements in this array.
    def this.length => ...

    # Call the given block for each element.
    def this.for_each(block) => ...

    # Return the concatenation of this and that.
    def this.concat(that) {
      def result := new Array();
      for e in this do
        result.add(e);
      for e in that do
        result.add(e)
      return result;
    }
  }

  ...

}

So far so good, this is the basics. Now, it’s easy to imagine someone thinking: a string sure looks a lot like an array of characters, it just needs for_each and a concat method and then I can use my full set of array functions on strings as well. I know, I’ll make an extension:

classbox strings_are_arrays {

  import core.String;

  refine core.String {
    # Call the given block for each character.
    def this.for_each(block) => ...

    ...
  }

}

Now we can use a string as if it were an array, for instance

classbox main {

  import array;
  import strings_are_arrays;

  def main() {
    def arr := ['f', 'o', 'o'];
    def str := "bar";
    print(arr.concat(str));
    # prints ['f', 'o', 'o', 'b', 'a', 'r']
  }

}

Neat right? This is how I would expect extension methods to work. Except, as far as I can tell, it doesn’t. This won’t print an array, it will error inside Array.concat.

Classbox method lookup uses the classbox that started the lookup to determine which methods are available. A method call in main will be able to see all the classboxes: itself, array and strings_are_arrays because it imports them, and core because strings_are_arrays imports it. So it can call String.for_each. It doesn’t call it directly though but indirectly through Array.concat. The classboxes visible from Array is collection because it’s in it and core because it imports it, not strings_are_arrays. So the attempt to call for_each on the string will fail.

Maybe this is a deliberate design choice. I suspect that design won’t work well in practice though. This way, extensions are second-class citizens because they disappear as soon as your object flows into code in another classbox. It also makes code sharing less effective: there may be a method that does what you want but you have to copy it into your own classbox for the extensions you need to be visible. It may also fail the other way round: someone passes you an object of a type you have extended and suddenly it changes behavior in a way neither you nor the caller could not have expected.

There’s of course also the chance that I’ve misunderstood the mechanism completely. I haven’t been able to find a full formal semantics so some of this is based on sort-of guesswork about how it works. But if this is how it does work I’m not convinced it’s a good idea.

3 Responses to Classboxes

  1. Pingback: A Really Simple Turing Machine simulator in Python | Computers, Science, & Computer Science

  2. Wouldn’t one merely need to do something like this within main?


    refine core.Array {
    import strings_are_arrays;
    }

  3. Something like that would work though I don’t know that the classbox model supports refining imports. It also breaks modularity. I don’t know how you would maintain something like that in a larger system.

    A broader point is that if you do indeed need to do something like that, assuming the model allows you to express it, then it starts to look a lot like just traditional monkey patching where a refinement in one place becomes globally visible, except worse because you have to maintain the transitive closure by hand.

Leave a Reply

Your email address will not be published. Required fields are marked *


*