Why Scala cannot infer type argument when it's obvious?

763 views Asked by At

In the following example, I was trying to create an implicit conversion between MySource and TypedPipe[T]. I own MySource, in fact I have a lot of such sources, so I wanted to use a Porable[T] trait to mark what type argument T I want for the output TypedPipe[T], so that the implicit conversion can automatically do the .toTypedPipe[T] part (so that I don't have to write .toTypedPipe[T] for every source I have when I use them).

import scala.language.implicitConversions

// The following two are pre-defined and I cannot change them

class TypedPipe[T](val path: String) {
  def mapWithValue = {
    println("values from " + path + " of type " + this.getClass)
  }
}

class Source(val path: String) {
  def toTypedPipe[T] = { new TypedPipe[T](path) }
}

// The following are defined by me, so yes I can change them.    

trait Portable[T]

class MySource(p: String) extends Source(p) with Portable[Tuple2[Int, Long]]

object conversions {
  implicit def portableSourceToTypedPipe[S <: Source with Portable[T], T](source: S): TypedPipe[T] = {
    source
      .toTypedPipe[T]
  }
}

import conversions._

portableSourceToTypedPipe(new MySource("real_path")).mapWithValue

But the problem is, Scala seem to not be able to infer T for the last statement:

scala> import scala.language.implicitConversions
import scala.language.implicitConversions

scala> class TypedPipe[T](val path: String) {
     |   def mapWithValue = {
     |     println("values from " + path + " of type " + this.getClass)
     |   }
     | }
defined class TypedPipe

scala> class Source(val path: String) {
     |   def toTypedPipe[T] = { new TypedPipe[T](path) }
     | }
defined class Source

scala>

scala> trait Portable[T]
defined trait Portable

scala>

scala> class MySource(p: String) extends Source(p) with Portable[Tuple2[Int, Long]]
defined class MySource

scala> object conversions {
     |   implicit def portableSourceToTypedPipe[S <: Source with Portable[T], T](source: S): TypedPipe[T] = {
     |     source
     |       .toTypedPipe[T]
     |   }
     | }
defined module conversions

scala> import conversions._
import conversions._

scala> portableSourceToTypedPipe(new MySource("real_path")).mapWithValue
<console>:17: error: inferred type arguments [MySource,Nothing] do not conform to method portableSourceToTypedPipe's type parameter bounds [S <: Source with Portable[T],T]
              portableSourceToTypedPipe(new MySource("real_path")).mapWithValue
              ^
<console>:17: error: type mismatch;
 found   : MySource
 required: S
              portableSourceToTypedPipe(new MySource("real_path")).mapWithValue
                                        ^

scala>

From the example, it's pretty obvious that MySource implements Portable[Tuple2[Int, Long]], so T should be Tuple2[Int, Long]. Why it's not inferred this way (which would have made the example work)?

EDIT:

Following this question and its answer mentioned by n.m., I revised my code to use an implicit evidence parameter to express the relation between the two type arguments. The explicit conversion call works now, but not the implicit conversion call. So still need help.

scala> object conversions {
     |   implicit def portableSourceToTypedPipe[S, T](source: S)(implicit ev: S <:< Source with Portable[T]): TypedPipe[T] = {
     |     source
     |       .toTypedPipe[T]
     |   }
     | }
defined module conversions

scala> import conversions._
import conversions._

scala> portableSourceToTypedPipe(new MySource("real_path")).mapWithValue
values from real_path of type class $line4.$read$$iw$$iw$TypedPipe

scala> (new MySource("real_path")).mapWithValue
<console>:17: error: Cannot prove that MySource <:< Source with Portable[T].
              (new MySource("real_path")).mapWithValue
               ^
<console>:17: error: value mapWithValue is not a member of MySource
              (new MySource("real_path")).mapWithValue

EDIT2

The reason for me to choose a trait Portable[T] is that it can potentially work with multiple base Source types. One familiar with Scalding might know that we have many types of sources, e.g. DailySuffixSource, HourlySuffixSource, not to mention that one can plug-in other traits like SuccessFileSource and DelimitedScheme. Having to implement something for each base source/traits combination will need quite a bit of work. Thus my trait choice. Of course it's not a must - any answer that can with multiple base source/traits combinations with O(1) amount of implementation will do.

2

There are 2 answers

0
Régis Jean-Gilles On BEST ANSWER

Given that you are not using the type parameter S anywhere in the return type, why not just have portableSourceToTypedPipe take a Source with Portable[T]. In other words:

implicit def portableSourceToTypedPipe[T](source: Source with Portable[T]): TypedPipe[T]

This trivially fixes your compilation problem. In general, the more explicit you are, the higher the chances are that the compiler can resolve the constraints represented by the type parameters. This starts with removing unnecessary type parameters altogether.

2
Alexey Romanov On

Your Source definition says you can call toTypedPipe[T] for any T. If you actually want MySource to convert to Tuple2[Int, Long] only, it should be

class TypedSource[T](path: String) extends Source(path) {
  def toSpecificTypedPipe = toTypedPipe[T]
}

class MySource(p: String) extends TypedSource[(Int, Long)](p)

implicit def portableSourceToTypedPipe[T](source: TypedSource[T]): TypedPipe[T] = 
  source.toSpecificTypedPipe

(you could also use composition instead of inheritance for TypedSource.)

If you do want ability convert to any type with one "preferred", just get rid of S, you don't need it:

implicit def portableSourceToTypedPipe[T](source: Source with Portable[T]): TypedPipe[T]