简体   繁体   中英

Is Comparator a type class?

I've been reading up on type classes in Scala and thought I had a good grasp on it, until I remembered Java's java.util.Comparator .

If I understand properly, Ordering is the prototypical example of a type class. The only difference I can think of between a Comparator and an instance of Ordering is that comparators are necessarily explicit, while orderings can be, and often are, implicit.

Is Comparator a type class? I get the (mistaken?) impression that Java does not actually have type classes. Does this mean that a type class needs to be able to be implicit? I considered implicit conversions of type classes to be mostly syntactic sugar - awesome as it is, it's "simply" giving the compiler enough hint - was I missing something?


The following code example shows how Comparator adds an ordering operation to a type that didn't have it, without having to modify said type.

// Comparator used to retroactively fit the MyExample class with an ordering operation.
public static class MyExampleComparator implements Comparator<MyExample> {
    public static final Comparator<MyExample> SINGLETON = new MyExampleComparator();

    private MyExampleComparator() {}

    public int compare(MyExample a, MyExample b) {
        return a.value - b.value;
    }
}

// Custom type, its only purpose is to show that Comparator can add an ordering operation to it when it doesn't
// have one to begin with.
public static class MyExample {
    private final int value;

    public MyExample(int v) {
        value = v;
    }

    public String toString() {
        return Integer.toString(value);
    }
}

public static void main(String... args) {
    List<MyExample> list = new ArrayList<MyExample>();

    for(int i = 0; i < 10; i++)
        list.add(new MyExample(-i));

    // Sorts the list without having had to modify MyExample to implement an interface.
    Collections.sort(list, MyExampleComparator.SINGLETON);

    // Prints the expected [-9, -8, -7, -6, -5, -4, -3, -2, -1, 0]
    System.out.println(list);
}

I prefer not to talk specifically about type classes but about the type class pattern in Scala; the reason is that when you start asking "what is the type class", you end up concluding that it is just an interface used in a particular way.

(In Haskell it makes more sense to call a specific construct a type class .)

The type class pattern consists of three essential parts (but there are usually a couple more for convenience). The first is an interface parameterized by a single type that abstracts some sort of capability on the parameterized type. java.util.Comparator is a perfect example: it provides an interface for comparison. Let's just use that.

The second thing you need is a method that makes use of that parameterization, which you can specify with short-hand notation in Scala:

// Short signature
//             v------------------- "We must be able to find a Comparator for A"
def ordered[A: java.util.Comparator](a0: A, a1: A, a2: A) = {
  val cmp = implicitly[java.util.Comparator[A]]   // This is the Comparator
  cmp.compare(a0, a1) <= 0 && cmp.compare(a1, a2) <= 0
}

// Long signature version
def ordered[A](a0: A, a1: A, a2: A)(implicit cmp: java.util.Comparator[A]) = {
  cmp.compare(a0, a1) <= 0 && cmp.compare(a1, a2) <= 0
}

Okay, but where do you get that comparator from? That's the third necessary piece. By default, Scala doesn't give you Comparator s for the classes you might like, but you can define your own:

implicit object IntComp extends java.util.Comparator[Int] {
  def compare(a: Int, b: Int) = a.compareTo(b)
}

scala> ordered(1,2,3)
res5: Boolean = true

scala> ordered(1,3,2)
res6: Boolean = false

Now that you've provided the functionality for Int (implicitly), the compiler will fill in the implicit parameter to ordered to make it work. If you haven't yet provided the functionality, it gives an error:

scala> ordered("fish","wish","dish")
<console>:12: error: could not find implicit value
for parameter cmp: java.util.Comparator[String]
          ordered("fish","wish","dish")

until you supply that functionality:

implicit object StringComp extends java.util.Comparator[String] {
  def compare(a: String, b: String) = a.compareTo(b)
}

scala> ordered("fish","wish","dish")
res11: Boolean = false

So, do we call java.util.Comparator a type class? It certainly functions just as well as a Scala trait that handles the equivalent part of the type class pattern. So even though the type class pattern doesn't work as well in Java (since you have to explicitly specify the instance to use instead of having it implicitly looked up), from a Scala perspective java.util.Comparator is as much a type class as anything.

The term type class comes from Haskell were they are part of the language. In scala, it is not, it is more of a pattern, which happens to have a lot of language support in scala (implicits, mostly). The pattern makes sense even without this syntactic support, for instance in java, and I would say that Comparator is a typical example of that pattern there (although the term type class is not used in java).

From an object oriented perspective, the pattern consist in having Comparator rather than Comparable . The most basic object thinking would have the comparison service in the object, say class String implements Comparable<String> . However, extracting it has numerous advantages:

  • You can provide the service for a class whose code you cannot change (for instance, arrays)
  • You can provide different implementation of the service (there are zillion ways to compare strings (case insensitive and a lot of language dependent one). People may be sorted by their name, their age, whatever. And also, you may simply want an ordering reversed.

These two reasons are enough to have Comparable in java, and to use them in in sorted collections (eg TreeSet ) Comparable is kept, as it gives a convenient default (no need to pass a Comparator when you want the "default" comparison, and it is easier to call (x.compareTo(y) rather than comparator.compare(x,y)). In scala, with implicits, none of this reason is compelling (interoperability with java would still be a reason to implement Ordered/Comparable in scala).

There are other, less obvious advantages to type classes. Among them :

  • A type class implementation is available and may be useful even when you have no instance of the type it operates on. Consider the operation sum(list) . It requires that there is some sort of addition available on the elements of the list. This might be available in the element themselves. Say they could be some Addable[T] with def add(other: T): T . But if you pass the empty list to sum, it should return the "zero" of the type of the type of the list (0 for ints, the empty string for strings...). Having a def zero: T in Addable[T] would be useless, as at that moment, you have no Addable around. But this works fine with a type class such as Numeric or Monoid .
  • As the type class are reified (they are objects rather than methods) they are first class, you can combine them, transform them. A very simple example is reversing an Ordering (you could implement that on Comparable too, in java probably in a static method). You can combine the ordering of Int and String to have an ordering defined on the pair (Int, String) , or given an Ordering on T , build an ordering on List[T] . Scala does that with implicits, but it still makes sense in java, explicitly.

A more sophisticated example:

// Comparison  by the first comparator which finds the two elements different. 
public static Comparator<T> lexicographic<T>(final Comparator<T>... comparators) {
   return new Comparator<T>() {
      public int compare(T t1, T t2) {
         for(comparator : comparators) {
            int result = comparator.compare(t1, t2);
            if (result != 0) return result;
         }
         return 0;
      }
   }
}

(might be simpler in scala, but again, this is of interest in java)

There are some small disadvantages too (much more so in java than in scala, but still)

  • You must pass around the type class instance from method to method. Much easier in scala with implicit parameter, or the [T : Comparable] constraint, but still, something has to be written in the methods definitions if not at call site, and at run time, the parameter has to be passed around.
  • Everything must be set at compile time (even in scala where it is set implicitly So while you can try if(x is Comparable<?>) {do some sorting} , this would not be possible with a Comparator.

No java.util.Comparator is an interface

public interface Comparator<T>

A comparison function, which imposes a total ordering on some collection of objects. Comparators can be passed to a sort method (such as Collections.sort or Arrays.sort) to allow precise control over the sort order. Comparators can also be used to control the order of certain data structures (such as sorted sets or sorted maps), or to provide an ordering for collections of objects that don't have a natural ordering.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM