2

I am trying to make a sequence (for example, other collection types are also conceivable) comparable to other sequences.

class RichSeq[A](val seq: Seq[A]) extends Ordered[RichSeq[A]]

Of course there is a implicit conversion in the refered package object:

implicit def seq2RichSeq[A](s: Seq[A]) = new RichSeq(s)

Comparing means, first size matters than each element. Code makes it clear:

class RichSeq[A](val seq: Seq[A]) extends Ordered[RichSeq[A]] { 
  def compare(s: RichSeq[A]) = {
    seq.size compare s.seq.size match {
      case 0 => seq.view.zip(s.seq).map { case (x,y) => ord.compare(x,y) }.dropWhile(_ == 0).headOption.getOrElse(0)
      case x => x
    }
  }
}

But that doesn`t compile (of course) because one needs an ordering to compare the elements, so I tried that:

class RichSeq[A](val seq: Seq[A]) extends Ordered[RichSeq[A]] { 
  def compare(s: RichSeq[A])(implicit ord: Ordering[A]) = {
    // ...
  }
}

Now the signature of the compare method is not suitable, so I moved the implicit ord to the class signature (and adapted the implicit conversion):

implicit def seq2RichSeq[A](s: Seq[A])(implicit ord: Ordering[A]) = new RichSeq(s)
class RichSeq[A](val seq: Seq[A])(implicit ord: Ordering[A]) extends Ordered[RichSeq[A]] { 
  def compare(s: RichSeq[A]) = {
      // ...
    }
  }

But now I have a the problem, that all other methods in RichSeq that I want to use via implicit at a Seq[A] also require an implicit Ordering[A] and I can´t always deliver one. Sometimes I use my RichSeq by methods without Ordering and sometimes the compare method.

For example, sometimes I call

def distinctBy[B](f: A => B): Seq[A] = {
  seq.foldLeft { (Buffer[A](),MutMap[B,A]()) } {
    case ((b,m),x) if m contains f(x) => (b,m)
    case ((b,m),x) => 
      m += f(x) -> x
      b += x
      (b,m)
  }._1
}

meanwhile I am not able to define an Ordering[A].

I see one solution in having two different classes (with two implicit conversions):

class RichSeqOrderable[A](val seq: Seq[A])(implicit ord: Ordering[A]) extends Ordered[RichSeqOrderable[A]]

class RichSeq[A](val seq: Seq[A])

But I think that breaks the thought of having all stuff together?!?

Peter Schmitz
  • 5,824
  • 4
  • 26
  • 48

3 Answers3

3

My usual preface that I wouldn't necessarily do things this way, but to use the question as an excuse to illuminate some lesser known features: here if any implicit ordering is available it will use that, but otherwise it will order them by hashcode.

package object foo {
  implicit def seq2RichSeq[A](s: Seq[A])(implicit ord: Ordering[A] = Ordering[Int].on((_: A).##)) = new RichSeq(s)
}
package foo {
  class RichSeq[A](val seq: Seq[A])(implicit ord: Ordering[A]) extends Ordered[RichSeq[A]] { 
    def compare(s: RichSeq[A]) = {
      seq.size compare s.seq.size match {
        case 0 => seq.view.zip(s.seq).map { case (x,y) => ord.compare(x,y) }.dropWhile(_ == 0).headOption.getOrElse(0)
        case x => x
      }
    }
  }
}
psp
  • 12,138
  • 1
  • 41
  • 51
  • 1
    Nice. Another idea would be to compare string representations by default, in the hope that they somehow closely represent the objects in question. Hash codes are a cleaner solution in general, though, but with less *meaning*. In particular, it might put two objects with the same content far apart in the result, since hash code need not be equal for different objects with same content. – Raphael Mar 13 '11 at 10:22
  • Nice! A default ordering by hashcode! I really like to accept your answer, but Daniels answer very slightly appeals more to me. – Peter Schmitz Apr 27 '11 at 17:07
1

I went for something similar to paulp's suggestion:

class RichSeq[A](val seq: Seq[A])(implicit optionalOrd: Option[Ordering[A]] = None) extends Ordered[RichSeq[A]] {
  def compare(s: RichSeq[A]) = {
    seq.size compare s.seq.size match {
      case 0 => seq.view.zip(s.seq).map { case (x,y) => optionalOrd.map(_.compare(x,y)).getOrElse(0) }.dropWhile(_ == 0).headOption.getOrElse(0)
      case x => x
    }
  }
}

object RichSeq {
  implicit def orderingToSome[A](implicit ord: Ordering[A] = null) = Option(ord)
  implicit def seq2RichSeq[A](s: Seq[A])(implicit ord: Option[Ordering[A]]) = new RichSeq(s)
}

It's not a good thing to have too many implicits, particularly of types in the standard library. However, I think Ordering[A] => Option[Ordering[A]] is about as safe as it could be.

On chaining of implicits

Scala has a restriction on automatic conversions to add a method, which is that it won't apply more than one conversion in trying to find methods. For example:

class A(val n: Int)
class B(val m: Int, val n: Int)
class C(val m: Int, val n: Int, val o: Int) {
  def total = m + n + o
}

// This demonstrates implicit conversion chaining restrictions
object T1 { // to make it easy to test on REPL
  implicit def toA(n: Int) = new A(n)
  implicit def aToB(a: A) = new B(a.n, a.n)
  implicit def bToC(b: B) = new C(b.m, b.n, b.m + b.n)

  // won't work
  println(5.total)
  println(new A(5).total)

  // works
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

However, if an implicit definition requires an implicit parameter itself, Scala will look for additional implicit values for as long as needed. Continue from the last example:

// def m[A <% B](m: A) is the same thing as
// def m[A](m: A)(implicit ev: A => B)

object T2 {
  implicit def toA(n: Int) = new A(n)
  implicit def aToB[A1 <% A](a: A1) = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1) = new C(b.m, b.n, b.m + b.n)

  // works
  println(5.total)
  println(new A(5).total)
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

"Magic!", you might say. Not so. Here is how the compiler would translate each one:

object T1Translated {
  implicit def toA(n: Int) = new A(n)
  implicit def aToB(a: A) = new B(a.n, a.n)
  implicit def bToC(b: B) = new C(b.m, b.n, b.m + b.n)

  // Scala won't do this
  println(bToC(aToB(toA(5))).total)
  println(bToC(aToB(new A(5))).total)

  // Just this
  println(bToC(new B(5, 5)).total)

  // No implicits required
  println(new C(5, 5, 10).total)
}

object T2Translated {
  implicit def toA(n: Int) = new A(n)
  implicit def aToB[A1 <% A](a: A1) = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1) = new C(b.m, b.n, b.m + b.n)

  // Scala does this
  println(bToC(5)(x => aToB(x)(y => toA(y))).total)
  println(bToC(new A(5))(x => aTo(B(x)(identity _)).total)
  println(bToC(new B(5, 5))(identity _).total)

  // no implicits required
  println(new C(5, 5, 10).total)
}

So, while bToC is being used as an implicit conversion, aToB and toA are being passed as implicit parameters, instead of being chained as implicit conversions.

Community
  • 1
  • 1
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • Since implicits are not chained, you would have to pass the `Ordering[.]` explicitly to `seq2RichSeq` for it to be implicitly converted to `Option[Ordering[.]]`, wouldn't you? But how to pass something explicitly to an implicit conversion? – Raphael Mar 16 '11 at 11:42
  • What cannot be chained is implicit conversions to allow for a method. For example, if I have a conversion from `A => B` and `B => C`, I cannot, however, call a `C` method on `A`. Here, however, only one implicit conversion is taking place: `seq2RichSeq`. The chaining of implicit parameters, on the other hand, is allowed. So it takes an implicit `Option[Ordering[A]]`, which is provided by `orderingToSome` which takes an implicit `Ordering[A]`, which, itself, is provided implicitly, and maybe even by taking an implicit `A => Ordered[A]`. – Daniel C. Sobral Mar 16 '11 at 12:51
  • Huh. Does that imply that I cannot chain implicit methods, but *can* chain implicit functions? *sratch his head* Ah, no, wait: "chaining" is the wrong word to use here. You are nesting, not chaining, right? I wonder wether one could circumvent the chaining restriction by using nesting, that is build one implicit conversion by nesting several others, in a sense. – Raphael Mar 16 '11 at 13:15
  • @Raphael This was getting too confusing for comments, so I expanded my answer to discuss it. – Daniel C. Sobral Mar 16 '11 at 17:26
  • Nice one. Maybe you should put it as an answer to an new question (Kevin style) to make it more accessible. – Raphael Mar 16 '11 at 20:55
  • 1
    @Raphael Indeed. I think I will do so. – Daniel C. Sobral Mar 16 '11 at 22:49
  • Awesome. Would make me happy if I was the reason that brought you to evolve that sophisticated implicit chaining stuff. The `Option` pattern on implicit parameters should be used more often! I will keep that in mind. Thanks! – Peter Schmitz Apr 27 '11 at 17:10
0

I have not coded it up completely, but why don't you do:

class RichSeq[A <: Ordered[A]](val seq: Seq[A]) extends Ordered[RichSeq[A]] {
  import Ordering.ordered 

  ...
}

The imported implicit conversion will give you Ordering[A] when necessary.

Raphael
  • 9,779
  • 5
  • 63
  • 94
  • Then, I have the same problem: `A` has to be a `Ordered[A]` and that is not always possible. Sometimes I have neither `Ordering[A]` nor `Ordered[A]`. – Peter Schmitz Mar 11 '11 at 19:26
  • Then there is no use in creating a `RichSeq` of that type, at least as long as `RichSeq <: Ordered[RichSeq]`. – Raphael Mar 11 '11 at 22:09