2

I was reading the (great) post A Generic Quicksort in Scala from Josh Suereth. Especially interesting was the part about the deferring of type inference for collections.

Now I wonder if this also applies for non-collections. The following 2 methods create a Foo[T,O]

def sort1[T, O](from: T, to: T)(implicit ev: O <:< Ordering[T], ord: O):Foo[T,O] = {
   ...
}

def sort2[T, O <: Ordering[Int]](from: T, to: T)(implicit ord: O):Foo[T,O] = {
   ...
}

Which of the two methods is preferred and why?

sort2(2,5) does work, with sort1(2,5) the compiler seems find more implicits since there is an ambiguous implicit resolution error.

Manuel Schmidt
  • 2,429
  • 1
  • 19
  • 32
  • Other link http://jsuereth.com/scala/2011/06/16/generic-quicksort.html – som-snytt Aug 12 '13 at 10:53
  • I think the answer is "neither": use `def sort[T](from: T, to: T)(implicit ord: Ordering[T]): Foo[T, Ordering[T]] = ...` (see also *context bounds* if you don't need to use `ord` directly) – Luigi Plinge Aug 13 '13 at 02:58
  • @LuigiPlinge I thought about it. But for me it is important which Ordering was used to create the Foo. For example a function `compose(foo1, foo2)` should only accept `Foo` with sorted with the same `Ordering` – Manuel Schmidt Aug 13 '13 at 11:08

2 Answers2

1

Indeed, this can seems unintuitive.

The reason is that sort1 defines no constraints on O, so 'ord: O' is ambiguous. (the constraint on the first implicit parameter only defines a constraint on 'ev' type).

Hope it helps :)

user2674268
  • 231
  • 2
  • 3
1

Deferring type inference using generalized type constraints is all about going around limitations with type inference. These limitations are not necessarily bugs, they can be by design.

I can think of two common situations where they are useful:

  • You want to get the type inside another higher-kinded type, but you have no constraint to get it.

    Example:

    def sort[T, Coll <: SeqLike[T, Coll](a: Coll): Coll
    

    The compiler has no way to get the type parameter T because it is not constraint in any way: it does not appear on the left-hand side of a <: or >: in the type parameter list, and does not appear in the argument's types.

    Generalized type constraints allow here to constraint (through an argument) the type T:

    def sort[T, Coll](a: Coll)(implicit ev: Coll <:< SeqLike[T, Coll]): Coll
    

    Note: this is only one way to do this. There is usually a way to get the same thing something very close without the implicit evidence. Here it would be:

    def sort[T, Coll <: SeqLike[T, Coll]](a: Coll with SeqLike[T, Coll]): Coll
    
  • You have no control over the type parameter, because it comes from the enclosing class.

    For example, adding a flatten method on List[A] that only works if A is a collection itself. You cannot change the type parameter A just for that one method, but you can locally add a constraint with an implicit ev: A <:< Traversable[B] or something similar.

    Note 2: this isn't what is done in the collection library, which uses implicit ev: (A) => Traversable[B], so that anything that can be converted to a collection works (like String or Array), but sometimes you do not want that.

Edit to address the sort1 vs sort2 question: adding a generalized type constraint when none is needed can create this kind of error because types become under-constrained. Since there is no constraint on O in sort1, ord: O can be anything. The implicit evidence can only be used to view an O as an Ordering[T] within the body of the method.

If you really wanted to keep the implicit evidence, you would have to re-introduce some constraint somewhere. On the type parameter like in sort2, or on ord itself:

def sort3[T, O](from: T, to: T)
  (implicit ev: O <:< Ordering[T], ord: O with Ordering[T]):Foo[T,O]

in this case, sort2 seems to be the best way to do this.

Community
  • 1
  • 1
gourlaysama
  • 11,240
  • 3
  • 44
  • 51
  • The blog mentions to avoid specifying the number of type params. I just noticed this with TraversableLike.to. You can to[List] but never to[Map]. Use case was Map.mapValues.toMap returns itself; I was going for a different idiom from .view.force or .iterator.toMap. – som-snytt Aug 14 '13 at 22:34
  • @som-snytt sadly one can avoid specifying the number of type params when asking for a type, but not for a type constructor. I would kill for a `def to[Coll[_*]] = macro to_Impl` that pattern matches on the number of type params (including 0) and looks for the proper implicits... – gourlaysama Aug 15 '13 at 22:48