2010-12-09 3 views
13

Dans le code ci-dessus, le site client "a oublié" de spécifier le paramètre de type, car l'éditeur d'API I veut pour signifier "juste retourner un nœud". Existe-t-il un moyen de définir une méthode générique (pas une classe) pour atteindre cet objectif (ou l'équivalent)? Note: en utilisant un manifeste dans l'implémentation pour faire la distribution si (manifeste! = Scala.reflect.Manifest.Nothing) ne compile pas ... J'ai le sentiment lancinant que certains Scala Wizard savent utiliser Predef. <: < pour cela :-)Est-il possible dans Scala de forcer l'appelant à spécifier un paramètre de type pour une méthode polymorphe?

Idées?

+0

@Ben, thx pour votre correction à mon post initial bâclée :-) –

Répondre

11

Une autre solution consiste à spécifier un type par défaut pour le paramètre comme suit:

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

La clé est de définir le type fantôme suivant d'agir comme témoin pour la valeur par défaut:

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

L'avantage de cette approche est qu'elle évite complètement l'erreur (à la fois à l'exécution et à la compilation). Si l'appelant ne spécifie pas le paramètre de type, il est par défaut Node.

Explication:

La signature de la méthode find assure qu'elle ne peut être appelé si l'appelant peut fournir un objet de type DefaultsTo[T, Node]. Bien sûr, les méthodes default et overrideDefault facilitent la création d'un tel objet pour tout type T. Étant donné que ces méthodes sont implicites, le compilateur gère automatiquement l'appel de l'une d'elles et transmet le résultat dans find.

Mais comment le compilateur sait-il quelle méthode appeler? Il utilise ses règles d'inférence de type et de résolution implicite pour déterminer la méthode appropriée. Il y a trois cas à considérer:

  1. find est appelée sans paramètre de type. Dans ce cas, le type T doit être déduit. En recherchant une méthode implicite qui peut fournir un objet de type DefaultsTo[T, Node], le compilateur trouve default et overrideDefault. default est choisi car il est prioritaire (car il est défini dans une sous-classe propre du trait qui définit overrideDefault). Par conséquent, T doit être lié à Node.

  2. find est appelé avec un paramètre de type non Node (par exemple, find[MyObj]("name")). Dans ce cas, un objet de type DefaultsTo[MyObj, Node] doit être fourni. Seule la méthode overrideDefault peut le fournir, de sorte que le compilateur insère l'appel approprié.

  3. find est appelé avec Node comme paramètre de type. Encore une fois, l'une ou l'autre méthode est applicable, mais default gagne en raison de sa priorité plus élevée.

+0

Ceci améliore le réponse choisie précédemment, fait à peu près tout ce que l'on veut. Donner le contrôle. Thanx Aaron. –

+0

Ceci est référencé par Akka Concurrency book. C'est sympa. Mais je ne comprends pas la raison du trait 'LowPriorityDefaultsTo' ... serait bien d'avoir une explication for-dummies (peut-être un article de blog)? – drozzy

+0

@drozzy J'ai ajouté une explication à la réponse - brièvement, le trait 'LowPriorityDefaultsTo' est nécessaire pour éviter toute ambiguïté puisque' default' ou 'overrideDefault' pourrait fournir un objet de type' DefaultsTo [A, A] '. Mettre 'overrideDefault' dans un trait fait que le compilateur préfère' default' dans toute situation où l'une ou l'autre des méthodes le ferait. –

5

Il est possible d'obtenir ce que vous recherchez, mais ce n'est pas simple. Le problème est que sans un paramètre de type explicite, le compilateur peut seulement déduire que T est Nothing. Dans ce cas, vous voulez find pour retourner quelque chose de type Node, pas de type T (à savoir Nothing), mais dans tous les cas, vous voulez trouver quelque chose pour revenir de type T.

Lorsque vous souhaitez que votre type de retour varie en fonction d'un paramètre de type, vous pouvez utiliser une technique similaire à celle que j'ai utilisée dans 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] 
    } 
} 

Ici, le procédé find retourne un foncteur polymorphe qui, lorsqu'elle est appliquée à un nom, retourne un objet de chaque type ou de type TNode en fonction de la valeur de l'argument ReturnAs fourni par le compilateur. Si T est Nothing, le compilateur fournira l'objet returnNothingAsNode et la méthode apply retournera un Node. Sinon, le compilateur fournira un ReturnAs[T, T] et la méthode apply retournera un T.


Riffing au large de la solution de Paul sur la liste de diffusion, une autre possibilité est de fournir un implicite pour chaque type qui « fonctionne ». Au lieu de retourner un Node lorsque le paramètre de type est omis, une erreur de compilation sera émis:

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

Bien sûr, cette solution n'échelle pas bien.

+0

Une solution alternative à l'aide d'un "type Bottom domaine" spécial a été proposé par Paul Philips dans la liste de diffusion Scala, voir ici http://article.gmane.org/gmane.comp.lang.scala/21986 –

+0

Peut-être qu'il me manque quelque chose, mais je ne vois pas comment cela résout le problème que vous avez posé ici. Lancer une 'Person' à' BottomNode' ne fonctionnerait pas mieux que de lancer 'Person' à' Nothing'. –

+0

Coulée, no. Mais en utilisant un trait de marqueur BottomNode auquel tous les types de domaines * * doivent se mélanger, cela permettrait d'établir la limite inférieure, la distribution de courtoisie quand aucun paramètre n'est fourni (serait impossible avec une limite inférieure) nécessite toujours votre solution. À ce stade, votre solution (un peu améliorée) est celle que nous utiliserons le plus probablement :-) –

1

La solution de Paul fournit une borne inférieure sur T, donc val person = find ("joe") est une erreur de compilation, vous obligeant à indiquer explicitement le type (par exemple, Node). Mais c'est une solution plutôt affreuse (Paul a explicitement dit qu'il ne le recommandait pas), car il vous oblige à énumérer tous vos sous-types.

+0

Je suis d'accord, une diff. La mise en œuvre de l'idée de Paul serait d'avoir un seul trait de marqueur que tous les types dans le domaine doivent mélanger dans lequel servira de limite inférieure raisonnable. Je ne l'ai pas encore testé, mais je le ferai dans quelques semaines une fois que le feu actuel dans un système indépendant sera éteint. Si cela ne fonctionne pas, je vais revenir à la suggestion d'Aaron ci-dessus. –

+0

Jim, je vous suggère de l'essayer. Ajouter une borne inférieure, dire 'BottomNode', sur' T' ne fait pas 'val person = find (" joe ")' une erreur de compilation. Il change simplement le type inféré de 'Nothing' en' BottomNode', et entraîne toujours une erreur d'exécution. La raison pour laquelle 'val dog = kennel' ne compile pas dans le code de Paul est parce qu'il n'y a pas d'objet implicite' BottomDog' dans la portée. L'utilisation d'implicits de cette manière ne fonctionne pas si vous avez plusieurs instances de chaque type (comme c'est probablement le cas pour une recherche basée sur une chaîne). –

+0

@Aaron, hmmm ... Je dois avouer que j'ai été téméraire en postant mon commentaire ci-dessus. Vous avez raison ! On dirait qu'il n'y a pas d'alternative à "Levage" comme vous l'appelez la méthode pour le Functor. –

6

Miles Sabin posted a really nice solution pour ce problème dans la liste de diffusion scala-user.Définir une classe de type NotNothing comme suit:

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

Maintenant, vous pouvez définir votre Finder comme

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

Si vous essayez d'appeler Finder.find sans paramètre de type, vous obtiendrez une erreur de compilation:

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

Cette solution est beaucoup plus générale que celles proposées dans mes autres réponses. Le seul inconvénient que je peux voir est que l'erreur de compilation est assez opaque, et l'annotation @implicitNotFound n'aide pas.

+0

J'ai enregistré https://issues.scala-lang.org/browse/SI-6806 pour faire du lobbying pour une annotation @ambiguousImplicits pour ces cas. Votez pour ça! – sourcedelica

+0

Cela ne fonctionne que si vous déclarez un paramètre polymorphe dans votre méthode: 'def find [T: NotNothing] (nom: String, quelquechose: T): T' mais dans ce cas' def find [T: NotNothing] (name: String): T' vous pouvez faire une déclaration 'obj.find (" bla ")' et le compilateur ne lance pas l'erreur ambiguë et il se bloque à l'exécution 'pas une instance de scala.runtime.Nothing' – lisak

Questions connexes