Alex Kravets
Alex Kravets

Reputation: 524

Is it possible in Scala to force the caller to specify a type parameter for a polymorphic method?

//API
class Node
class Person extends Node

object Finder
{
  def find[T <: Node](name: String): T = doFind(name).asInstanceOf[T]
}

//Call site (correct)
val person = find[Person]("joe")

//Call site (dies with a ClassCast inside b/c inferred type is Nothing)
val person = find("joe")

In the code above the client site "forgot" to specify the type parameter, as the API writer I want that to mean "just return Node". Is there any way to define a generic method (not a class) to achieve this (or equivalent). Note: using a manifest inside the implementation to do the cast if (manifest != scala.reflect.Manifest.Nothing) won't compile ... I have a nagging feeling that some Scala Wizard knows how to use Predef.<:< for this :-)

Ideas ?

Upvotes: 14

Views: 3436

Answers (4)

Aaron Novstrup
Aaron Novstrup

Reputation: 21017

Yet another solution is to specify a default type for the parameter as follows:

object Finder {
   def find[T <: Node](name: String)(implicit e: T DefaultsTo Node): T = 
      doFind(name).asInstanceOf[T]
}

The key is to define the following phantom type to act as a witness for the default:

sealed class DefaultsTo[A, B]
trait LowPriorityDefaultsTo {
   implicit def overrideDefault[A,B] = new DefaultsTo[A,B]
}
object DefaultsTo extends LowPriorityDefaultsTo {
   implicit def default[B] = new DefaultsTo[B, B]
}

The advantage of this approach is that it avoids the error altogether (at both run-time and compile-time). If the caller does not specify the type parameter, it defaults to Node.

Explanation:

The signature of the find method ensures that it can only be called if the caller can supply an object of type DefaultsTo[T, Node]. Of course, the default and overrideDefault methods make it easy to create such an object for any type T. Since these methods are implicit, the compiler automatically handles the business of calling one of them and passing the result into find.

But how does the compiler know which method to call? It uses its type inference and implicit resolution rules to determine the appropriate method. There are three cases to consider:

  1. find is called with no type parameter. In this case, type T must be inferred. Searching for an implicit method that can provide an object of type DefaultsTo[T, Node], the compiler finds default and overrideDefault. default is chosen since it has priority (because it's defined in a proper subclass of the trait that defines overrideDefault). As a result, T must be bound to Node.

  2. find is called with a non-Node type parameter (e.g., find[MyObj]("name")). In this case, an object of type DefaultsTo[MyObj, Node] must be supplied. Only the overrideDefault method can supply it, so the compiler inserts the appropriate call.

  3. find is called with Node as the type parameter. Again, either method is applicable, but default wins due to its higher priority.

Upvotes: 15

Aaron Novstrup
Aaron Novstrup

Reputation: 21017

It's possible to get what you're after, but it's not simple. The problem is that without an explicit type parameter, the compiler can only infer that T is Nothing. In that case, you want find to return something of type Node, not of type T (i.e. Nothing), but in every other case you want find to return something of type T.

When you want your return type to vary based on a type parameter, you can use a technique similar to the one I used in my method lifting API.

object Finder {
   def find[T <: Node] = new Find[T]

   class Find[T <: Node] {
       def apply[R](name: String)(implicit e: T ReturnAs R): R = 
          doFind(name).asInstanceOf[R]
   }

   sealed class ReturnAs[T, R]
   trait DefaultReturns {
      implicit def returnAs[T] = new ReturnAs[T, T]
   }
   object ReturnAs extends DefaultReturns {
      implicit object returnNothingAsNode extends ReturnAs[Nothing, Node]
   }
}

Here, the find method returns a polymorphic functor that, when applied to a name, will return an object of either type T or of type Node depending on the value of the ReturnAs argument supplied by the compiler. If T is Nothing, the compiler will supply the returnNothingAsNode object and the apply method will return a Node. Otherwise, the compiler will supply a ReturnAs[T, T], and the apply method will return a T.


Riffing off of Paul's solution on the mailing list, another possibility is to provide an implicit for each type that "works". Instead of returning a Node when the type parameter is omitted, a compile error will be issued:

object Finder {
   def find[T : IsOk](name: String): T = 
      doFind(name).asInstanceOf[T]

   class IsOk[T]
   object IsOk {
      implicit object personIsOk extends IsOk[Person]
      implicit object nodeIsOk extends IsOk[Node]
   }
}

Of course, this solution does not scale well.

Upvotes: 5

Aaron Novstrup
Aaron Novstrup

Reputation: 21017

Miles Sabin posted a really nice solution for this problem on the scala-user mailing list. Define a NotNothing type class as follows:

sealed trait NotNothing[T] { type U }                                          
object NotNothing {
   implicit val nothingIsNothing = new NotNothing[Nothing] { type U = Any }
   implicit def notNothing[T] = new NotNothing[T] { type U = T }           
}

Now you can define your Finder as

object Finder {
   def find[T <: Node : NotNothing](name: String): T = 
      doFind(name).asInstanceOf[T]
}

If you try to invoke Finder.find without a type parameter, you'll get a compile-time error:

error: ambiguous implicit values: both method notNothing in object $iw of type [T]java.lang.Object with NotNothing[T]{type U = T} and value nothingIsNothing in object $iw of type => java.lang.Object with NotNothing[Nothing]{type U = Any} match expected type NotNothing[T] Finder.find("joe")

This solution is far more general than the ones proposed in my other answers. The only drawback I can see is that the compile-time error is pretty opaque, and the @implicitNotFound annotation doesn't help.

Upvotes: 8

Jim Balter
Jim Balter

Reputation: 16406

Paul's solution provides a lower bound on T, so val person = find("joe") is a compile-time error, forcing you to explicitly state the type (e.g., Node). But it's a rather awful solution (Paul explicitly said he wasn't recommending it) since it requires you to enumerate all your subtypes.

Upvotes: 1

Related Questions