2010-12-09 7 views
13
//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") 

En el código anterior el sitio del cliente "se olvidó" para especificar el tipo de parámetro, como el escritor API que quiere que significa "simplemente volver nodo". ¿Hay alguna manera de definir un método genérico (no una clase) para lograr esto (o su equivalente)? Nota: usar un manifiesto dentro de la implementación para hacer el reparto if (manifest! = Scala.reflect.Manifest.Nothing) no se compilará ... Tengo la sensación de que algún Scala Wizard sabe cómo usar Predef. <: < para esto :-)¿Es posible en Scala obligar al llamante a especificar un parámetro de tipo para un método polimórfico?

Ideas?

+0

@Ben, thx para su corrección a mi mensaje inicial descuidado :-) –

Respuesta

11

Sin embargo, otra solución es especificar un tipo predeterminado para el parámetro de la siguiente manera:

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

La clave es definir la siguiente tipo fantasma para actuar como testigo para el valor por defecto:

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] 
} 

La ventaja de este enfoque es que evita el error por completo (tanto en tiempo de ejecución como en tiempo de compilación). Si la persona que llama no especifica el parámetro de tipo, su valor predeterminado es Node.

Explicación:

La firma del método find asegura que sólo se puede llamar si la persona que llama puede suministrar un objeto de tipo DefaultsTo[T, Node]. Por supuesto, los métodos default y overrideDefault facilitan la creación de dicho objeto para cualquier tipo T. Como estos métodos son implícitos, el compilador maneja automáticamente el negocio de llamar a uno de ellos y pasar el resultado al find.

Pero, ¿cómo sabe el compilador qué método llamar? Utiliza su inferencia de tipo y reglas de resolución implícitas para determinar el método apropiado. Hay tres casos a considerar:

  1. find se llama sin ningún tipo de parámetro. En este caso, se debe inferir el tipo T. Al buscar un método implícito que pueda proporcionar un objeto de tipo DefaultsTo[T, Node], el compilador encuentra default y overrideDefault. Se elige default ya que tiene prioridad (porque está definido en una subclase adecuada del rasgo que define overrideDefault). Como resultado, T debe estar vinculado a Node.

  2. find se llama con un parámetro de tipo Node (por ejemplo, find[MyObj]("name")). En este caso, se debe suministrar un objeto del tipo DefaultsTo[MyObj, Node]. Solo el método overrideDefault puede suministrarlo, por lo que el compilador inserta la llamada correspondiente.

  3. find se llama con Node como el parámetro de tipo. De nuevo, cualquiera de los dos métodos es aplicable, pero default gana debido a su mayor prioridad.

+0

Esto mejora la respuesta elegida anterior, hace todo lo que uno querría. Dándole el control. Gracias, Aaron. –

+0

Esto se refiere al libro Akka Concurrency. Esto es bonito. Pero no entiendo la razón para el rasgo 'LowPriorityDefaultsTo' ... sería bueno obtener una explicación para-tontos de esto (¿tal vez una publicación de blog)? – drozzy

+0

@drozzy Agregué una explicación a la respuesta - brevemente, el rasgo 'LowPriorityDefaultsTo' es necesario para evitar la ambigüedad ya que' default' o 'overrideDefault' podría proporcionar un objeto de tipo' DefaultsTo [A, A] '. Poner 'overrideDefault' en un rasgo hace que el compilador prefiera' default' en cualquier situación en que lo haga cualquiera de los dos métodos. –

5

Es posible obtener lo que buscas, pero no es simple. El problema es que sin un parámetro de tipo explícito, el compilador solo puede inferir que T es Nothing. En ese caso, usted quiere find devolver algo del tipo Node, no de tipo T (es decir Nothing), pero en todos los demás casos que desea encontrar a devolver algo del tipo T.

Cuando desee que su tipo de devolución varíe en función de un parámetro de tipo, puede utilizar una técnica similar a la que utilicé en mi 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] 
    } 
} 

Aquí, el método devuelve un find funtor polimórfico que, cuando se aplica a un nombre, devolverá un objeto de cualquier tipo T o de tipo Node dependiendo del valor del argumento ReturnAs suministrado por el compilador. Si T es Nothing, el compilador suministrará el objeto returnNothingAsNode y el método de aplicación devolverá un Node. De lo contrario, el compilador proporcionará un ReturnAs[T, T], y el método de aplicación devolverá un T.


Riffing fuera de la solución de Pablo en la lista de correo, otra posibilidad es proporcionar un implícito para cada tipo que "funciona". En lugar de devolver un Node cuando se omite el parámetro de tipo, se emitirá un error de compilación:

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] 
    } 
} 

Por supuesto, esta solución no se adapta bien.

+0

Una solución alternativa usando un especial de "dominio del tipo de fondo" fue propuesto por Paul Philips en la lista de correo Scala, ver aquí http://article.gmane.org/gmane.comp.lang.scala/21986 –

+0

Tal vez me esté perdiendo algo, pero no veo cómo eso resuelve el problema que planteaste aquí. Lanzar una 'Persona' a' BottomNode' no funcionaría mejor que lanzar una 'Persona' a' Nada'. –

+0

Fundición, no. Pero utilizando un rasgo del marcador BottomNode cuyos * todos * tipos de dominio * deben * mezclarse, permitiría establecer el límite inferior, el reparto de cortesía cuando no se proporcione ningún parámetro (sería imposible con un límite inferior) aún requiere su solución. En este punto, la solución (un poco complicada) es la que terminaremos usando :-) –

1

La solución de Paul proporciona un límite inferior en T, por lo que val person = find ("joe") es un error en tiempo de compilación, lo que le obliga a indicar explícitamente el tipo (por ejemplo, Nodo). Pero es una solución bastante horrible (Paul dijo explícitamente que no la recomendaba), ya que requiere que enumere todos sus subtipos.

+0

Estoy de acuerdo, un diff. la implementación de la idea de Paul sería tener un solo rasgo de marcador que todos los tipos en la mezcla de dominio deben mezclar, lo que servirá como un límite inferior razonable. No he probado esto todavía, pero lo haré en unas pocas semanas una vez que se apague el fuego actual en un sistema no relacionado. Si eso no funciona, volveré a utilizar la sugerencia de Aaron anterior. –

+0

Jim, te sugiero que lo intentes. Agregar un límite inferior, digamos 'BottomNode', en' T' no hace 'val person = find (" joe ")' un error en tiempo de compilación. Simplemente cambia el tipo inferido de 'Nothing' a' BottomNode', y aún da como resultado un error en tiempo de ejecución. La razón por la cual 'val dog = kennel' no se compila en el código de Paul es porque no hay un objeto' BottomDog' implícito en el alcance. El uso de implícitos de esa manera no funciona si tiene más de una instancia de cada tipo (como presumiblemente es el caso de una búsqueda basada en cadenas). –

+0

@ Aaron, hmmm ... Debo confesar que fui irresponsable al publicar mi comentario arriba. Estás en lo correcto ! Parece que no hay alternativa a "Levantar" como lo llama el método para el Functor. –

6

Miles Sabin posted a really nice solution para este problema en la lista de correo de scala-user.Definir una clase NotNothing tipo de la siguiente manera:

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 }   
} 

Ahora puede definir su Finder como

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

Si intenta invocar Finder.find sin un parámetro de tipo, obtendrá un error de compilación:

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")

Esta solución es mucho más general que las propuestas en mis otras respuestas. El único inconveniente que puedo ver es que el error en tiempo de compilación es bastante opaco, y la anotación @implicitNotFound no ayuda.

+0

He registrado https://issues.scala-lang.org/browse/SI-6806 para cabildear una anotación @ambiguousImplicits para estos casos. ¡Vota por eso! – sourcedelica

+0

Realmente solo funciona si declaras un parámetro polimórfico en tu método: 'def find [T: NotNothing] (name: String, whatever: T): T' pero en este caso' def find [T: NotNothing] (name: String): T' usted puede hacer una declaración 'obj.find (" bla ")' y el compilador no arroja el error ambiguo y se bloquea en el tiempo de ejecución 'no es una instancia de scala.runtime.Nothing' – lisak

Cuestiones relacionadas