In Scala, how to use Ordering[T] with List.min or List.max and keep code readable

Viewed 25982

In Scala 2.8, I had a need to call List.min and provide my own compare function to get the value based on the second element of a Tuple2. I had to write this kind of code:

val list = ("a", 5) :: ("b", 3) :: ("c", 2) :: Nil

list.min( new Ordering[Tuple2[String,Int]] { 
  def compare(x:Tuple2[String,Int],y:Tuple2[String,Int]): Int = x._2 compare y._2 
} )

Is there a way to make this more readable or to create an Ordering out of an anonymous function like you can do with list.sortBy(_._2)?

6 Answers

C'mon guys, you made the poor questioner find "on" himself. Pretty shabby performance. You could shave a little further writing it like this:

list min Ordering[Int].on[(_,Int)](_._2)

Which is still far too noisy but that's where we are at the moment.

One thing you can do is use the more concise standard tuple type syntax instead of using Tuple2:

val min = list.min(new Ordering[(String, Int)] { 
  def compare(x: (String, Int), y: (String, Int)): Int = x._2 compare y._2 
})

Or use reduceLeft to have a more concise solution altogether:

val min = list.reduceLeft((a, b) => (if (a._2 < b._2) a else b))

Or you could sort the list by your criterion and get the first element (or last for the max):

val min = list.sort( (a, b) => a._2 < b._2 ).first

Which can be further shortened using the placeholder syntax:

val min = list.sort( _._2 < _._2 ).first

Which, as you wrote yourself, can be shortened to:

val min = list.sortBy( _._2 ).first

But as you suggested sortBy yourself, I'm not sure if you are looking for something different here.

The function Ordering#on witnesses the fact that Ordering is a contra-variant functor. Others include Comparator, Function1, Comparable and scalaz.Equal.

Scalaz provides a unified view on these types, so for any of them you can adapt the input with value contramap f, or with symbolic denotation, value ∙ f

scala> import scalaz._
import scalaz._

scala> import Scalaz._
import Scalaz._

scala> val ordering = implicitly[scala.Ordering[Int]] ∙ {x: (_, Int) => x._2}
ordering: scala.math.Ordering[Tuple2[_, Int]] = scala.math.Ordering$$anon$2@34df289d

scala> List(("1", 1), ("2", 2)) min ordering  
res2: (java.lang.String, Int) = (1,1)

Here's the conversion from the Ordering[Int] to Ordering[(_, Int)] in more detail:

scala> scalaz.Scalaz.maContravariantImplicit[Ordering, Int](Ordering.Int).contramap { x: (_, Int) => x._2 }
res8: scala.math.Ordering[Tuple2[_, Int]] = scala.math.Ordering$$anon$2@4fa666bf
list.min(Ordering.fromLessThan[(String, Int)](_._2 < _._2))

Which is still too verbose, of course. I'd probably declare it as a val or object.

You could always define your own implicit conversion:

implicit def funToOrdering[T,R <% Ordered[R]](f: T => R) = new Ordering[T] {
  def compare(x: T, y: T) = f(x) compare f(y)
}

val list = ("a", 5) :: ("b", 3) :: ("c", 2) :: Nil

list.min { t: (String,Int) => t._2 }  // (c, 2)

EDIT: Per @Dario's comments.

Might be more readable if the conversion wasn't implicit, but using an "on" function:

def on[T,R <% Ordered[R]](f: T => R) = new Ordering[T] {
  def compare(x: T, y: T) = f(x) compare f(y)
}

val list = ("a", 5) :: ("b", 3) :: ("c", 2) :: Nil

list.min( on { t: (String,Int) => t._2 } ) // (c, 2)
Related