Conversion implicite contre classe de type

93

Dans Scala, nous pouvons utiliser au moins deux méthodes pour moderniser des types existants ou nouveaux. Supposons que nous voulions exprimer que quelque chose peut être quantifié en utilisant un Int. Nous pouvons définir le trait suivant.

Conversion implicite

trait Quantifiable{ def quantify: Int }

Et puis nous pouvons utiliser des conversions implicites pour quantifier par exemple les chaînes et les listes.

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

Après les avoir importés, nous pouvons appeler la méthode quantifysur des chaînes et des listes. Notez que la liste quantifiable stocke sa longueur, ce qui évite la traversée coûteuse de la liste lors des appels ultérieurs à quantify.

Classes de type

Une alternative est de définir un «témoin» Quantified[A]qui déclare qu'un certain type Apeut être quantifié.

trait Quantified[A] { def quantify(a: A): Int }

Nous fournissons ensuite des instances de cette classe de type pour Stringet Listquelque part.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

Et si on écrit ensuite une méthode qui a besoin de quantifier ses arguments, on écrit:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

Ou en utilisant la syntaxe liée au contexte:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

Mais quand utiliser quelle méthode?

Vient maintenant la question. Comment puis-je choisir entre ces deux concepts?

Ce que j'ai remarqué jusqu'à présent.

classes de type

  • les classes de type permettent la belle syntaxe liée au contexte
  • avec les classes de type je ne crée pas de nouvel objet wrapper à chaque utilisation
  • la syntaxe liée au contexte ne fonctionne plus si la classe de type a plusieurs paramètres de type; imaginez que je veux quantifier les choses non seulement avec des entiers mais avec des valeurs d'un type général T. Je voudrais créer une classe de typeQuantified[A,T]

conversion implicite

  • puisque je crée un nouvel objet, je peux y mettre en cache des valeurs ou calculer une meilleure représentation; mais dois-je éviter cela, car cela peut se produire plusieurs fois et une conversion explicite ne serait probablement invoquée qu'une seule fois?

Ce que j'attends d'une réponse

Présentez un (ou plusieurs) cas d'utilisation dans lesquels la différence entre les deux concepts est importante et expliquez pourquoi je préférerais l'un à l'autre. Expliquer également l'essence des deux concepts et leur relation l'un avec l'autre serait bien, même sans exemple.

ziggystar
la source
Il y a une certaine confusion dans les points de classe de type où vous mentionnez "lié à la vue", bien que les classes de type utilisent des limites de contexte.
Daniel C.Sobral
1
+1 excellente question; Je suis très intéressé par une réponse approfondie à cela.
Dan Burton
@Daniel Merci. Je me trompe toujours.
ziggystar
2
Vous vous trompez à un endroit: dans votre deuxième exemple de conversion implicite, vous stockez le sized'une liste dans une valeur et dites que cela évite le parcours coûteux de la liste lors des appels ultérieurs à quantifier, mais à chaque appel à la, quantifyle list2quantifiableest déclenché une fois de plus réinstaurant ainsi Quantifiableet recalculant la quantifypropriété. Ce que je dis, c'est qu'il n'y a en fait aucun moyen de mettre en cache les résultats avec des conversions implicites.
Nikita Volkov
@NikitaVolkov Votre observation est juste. Et j'aborde cela dans ma question dans l'avant-dernier paragraphe. La mise en cache fonctionne lorsque l'objet converti est utilisé plus longtemps après un appel de méthode de conversion (et peut-être est-il transmis sous sa forme convertie). Alors que les classes de types seraient probablement enchaînées le long de l'objet non converti en allant plus loin.
ziggystar

Réponses:

42

Bien que je ne veuille pas dupliquer mon matériel de Scala In Depth , je pense qu'il vaut la peine de noter que les classes de types / traits de type sont infiniment plus flexibles.

def foo[T: TypeClass](t: T) = ...

a la capacité de rechercher dans son environnement local une classe de type par défaut. Cependant, je peux remplacer le comportement par défaut à tout moment de l'une des deux manières suivantes:

  1. Création / importation d'une instance de classe de type implicite dans Scope pour court-circuiter la recherche implicite
  2. Passer directement une classe de type

Voici un exemple:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

Cela rend les classes de types infiniment plus flexibles. Une autre chose est que les classes / traits de types supportent mieux la recherche implicite .

Dans votre premier exemple, si vous utilisez une vue implicite, le compilateur effectuera une recherche implicite pour:

Function1[Int, ?]

Qui regardera Function1l 'objet compagnon de et l' Intobjet compagnon.

Notez que ce Quantifiablen'est nulle part dans la recherche implicite. Cela signifie que vous devez placer la vue implicite dans un objet de package ou l' importer dans la portée. Il est plus difficile de se souvenir de ce qui se passe.

En revanche, une classe de type est explicite . Vous voyez ce qu'il recherche dans la signature de la méthode. Vous avez également une recherche implicite de

Quantifiable[Int]

qui cherchera dans Quantifiablel'objet compagnon de et dans Int l'objet compagnon de. Cela signifie que vous pouvez fournir des valeurs par défaut et de nouveaux types (comme une MyStringclasse) peuvent fournir une valeur par défaut dans leur objet compagnon et il sera recherché implicitement.

En général, j'utilise des classes de types. Ils sont infiniment plus flexibles pour l'exemple initial. Le seul endroit où j'utilise des conversions implicites est lors de l'utilisation d'une couche API entre un wrapper Scala et une bibliothèque Java, et même cela peut être `` dangereux '' si vous ne faites pas attention.

jsuereth
la source
20

Un critère qui peut entrer en jeu est la façon dont vous voulez que la nouvelle fonctionnalité "se sente"; en utilisant des conversions implicites, vous pouvez donner l'impression que ce n'est qu'une autre méthode:

"my string".newFeature

... tout en utilisant des classes de types, il semblera toujours que vous appelez une fonction externe:

newFeature("my string")

Une chose que vous pouvez réaliser avec des classes de type et non avec des conversions implicites consiste à ajouter des propriétés à un type plutôt qu'à une instance d'un type. Vous pouvez ensuite accéder à ces propriétés même si vous ne disposez pas d'une instance du type disponible. Un exemple canonique serait:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

Cet exemple montre également comment les concepts sont étroitement liés: les classes de type ne seraient pas aussi utiles s'il n'y avait pas de mécanisme pour produire une infinité de leurs instances; sans la implicitméthode (pas une conversion, certes), je ne pourrais avoir qu'une infinité de types possédant la Defaultpropriété.

Philippe
la source
@Phillippe - Je suis très intéressé par la technique que vous avez écrite ... mais elle semble ne pas fonctionner sur Scala 2.11.6. J'ai posté une question demandant une mise à jour sur votre réponse. merci d'avance si vous pouvez aider: S'il vous plaît voir: stackoverflow.com/questions/31910923/…
Chris Bedford
@ChrisBedford J'ai ajouté la définition de defaultpour les futurs lecteurs.
Philippe le
13

Vous pouvez penser à la différence entre les deux techniques par analogie à l'application de fonction, juste avec un wrapper nommé. Par exemple:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

Une instance de la première encapsule une fonction de type A => Int, alors qu'une instance de la seconde a déjà été appliquée à un A. Vous pouvez continuer le modèle ...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

ainsi vous pourriez penser à une Foo1[B]sorte de comme l'application partielle de Foo2[A, B]à une Ainstance. Un bon exemple de ceci a été écrit par Miles Sabin sous le nom de «Dépendances fonctionnelles dans Scala» .

Donc vraiment mon point est que, en principe:

  • "pimping" une classe (par conversion implicite) est le cas "d'ordre zéro" ...
  • déclarer une classe de types est le cas du "premier ordre" ...
  • les classes de types multi-paramètres avec fundeps (ou quelque chose comme fundeps) est le cas général.
fusion de conflit
la source