Thursday, April 17, 2014

Scrap your interfaces

This post is inspired by Scrap your type classes by Gabriel Gonzales. Basically I want to translate his idea into an object-oriented programming language and see if it works.

1. Comparable and Comparator

We begin by noticing a problem with OOP languages. Let's try to define a Comparable interface, for objects that we want to put in sorted collections etc.:

interface Comparable {
  int compareTo(Comparable); // Hmm, is this right?
}

class Foo implements Comparable {
  int compareTo(Comparable); // Definitely wrong
}

The problem is that we want to compare Foo to another Foo, not to any Comparable. And we want the compiler to enforce that guarantee, because that's what type safety is all about. Some might say we should add a type parameter to the Comparable interface:

interface Comparable<Foo> {
  int compareTo(Foo);
}

And since we don't want users to write something like "Bar implements Comparable<Foo>", we can use F-bounded polymorphism to prevent that:

interface Comparable<Foo extends Comparable<Foo>> {
  int compareTo(Foo);
}

That solution would work, but for purposes of this post I'd like to explore something simpler and more general. Instead of putting the comparison operation inside the class, we can use an external object that implements the Comparator interface:

interface Comparator<Foo> {
  int compare(Foo,Foo);
}

This way, the compiler makes sure that the "compare" method takes two arguments of type Foo. Another benefit of that solution is that you can have different comparators defined on the same class. That would not be possible with Comparable, because a class cannot implement the same interface in two different ways. The drawback is that now you have to pass the Comparator object around manually.

2. Serializable and Serializer

The above solution with Comparator is an instance of a general pattern that I want to explore in this post. Let's take another example. Let's try to define an interface for objects that can be serialized and deserialized:

interface Serializable {
  ByteArray serialize();
  void deserialize(ByteArray);
}

The problem is that having "deserialize" as an instance method is not ideal, because that forces the user to construct a new Foo first before deserializing. Ideally we'd want the deserialization code to be responsible for constructing Foo, so it can choose which constructor of Foo to call. To solve that problem, let's define an external Serializer interface, like in the previous section:

interface Serializer<Foo> {
  ByteArray serialize(Foo);
  Foo deserialize(ByteArray);
}

Note that since the class Foo doesn't need to implement Serializer<Foo> or reference it in any way, we can write different serializers for the same class, just like with Comparator.

3. Numeric types

For a third example, let's take arithmetic. Can we define an interface Numeric that includes all arithmetic operations, like addition and multiplication, so that users can implement it in their own classes that support arithmetic? That's difficult, because we want to stop users from adding together numbers of different types (the same problem we had with Comparable), and also because we'd like to provide zero and one as static constants. The solution is once again to pull the operations into an external interface:

interface ArithmeticOperations<Foo> {
  Foo add(Foo,Foo);
  Foo subtract(Foo,Foo);
  Foo multiply(Foo,Foo);
  Foo divide(Foo,Foo);
  Foo zero();
  Foo one();
}

That way users can implement their own numeric types and pass them to existing libraries, or even do more fancy things like tracing which arithmetic operations get called by the existing libraries (by writing a custom implementation of ArithmeticOperations<Int>).

4. Conclusions

By now I think the common thread is clear. Using interfaces in the usual way is sometimes limited and unnatural, because when a method of class Foo is defined in some interface, it only accepts one argument of type Foo (the "this" pointer). You'll need unnatural tricks if you want it to accept two Foos, return a Foo, construct a Foo from nothing, etc. Also you can't make Foo implement the same interface in two different ways, and other people can't make Foo implement an interface that you didn't anticipate.

The "external interfaces" shown above are free from all these restrictions. You can view them as a kind of generalized virtual table for Foo, which is not attached to any individual Foo. And other people don't need to depend on Foo implementing the right interfaces, because they can write their own implementations of Comparator<Foo> or Serializer<Foo> without modifying Foo. In this way, the relationship between classes and interfaces becomes truly many-to-many and extensible by third parties.

I don't know if you should go as extreme as never having interfaces on instances at all, and always use these "external interfaces" instead. There are probably drawbacks to that. The main syntactic drawback is having to pass the implementation of the "external interface" everywhere, but maybe it can be addressed with some syntactic sugar.

Trying to come up with such syntactic sugar for an OOP language could be an interesting exercise in language design, I think. Part of the inspiration could come from functional languages like Scala or Agda, which try to solve a similar problem. I intentionally didn't put any complicated FP stuff in this post, but you can find plenty of it in these Reddit discussions: Scrap your type classes, Instances and Dictionaries.

Edit: the Reddit discussion about this post has some very nice comments.

1 comment:

  1. I've noticed the same issue using validator classes, for example you might want a common interface for validators `List validate(Object);` and then write some class `UserValidator` that implements the method, but like your Comparator example, clearly that's not what you want (since you want to validate a User, not an Object). One potential work around using the constructor to accept arguments:

    interface Validator {
    List validate();
    }
    class UserValidator implements Validator {
    private User user;
    UserValidator(User user) {
    this.user = user;
    }
    public List validate() {
    // validate this.user
    }
    }

    ReplyDelete