Reputation: 524
//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
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:
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
.
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.
find
is called with Node
as the type parameter. Again, either method is applicable, but default
wins due to its higher priority.
Upvotes: 15
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
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
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