D'après ce que j'ai compris de cet article de blog "classes de type" dans Scala est simplement un "modèle" implémenté avec des traits et des adaptateurs implicites.
Comme le blog l'indique si j'ai trait A
et un adaptateur B -> A
, je peux appeler une fonction qui requiert un argument de type A
, avec un argument de type B
sans appeler explicitement cet adaptateur.
Je l'ai trouvé sympa mais pas particulièrement utile. Pourriez-vous donner un exemple d'utilisation/cas, qui montre à quoi cette fonctionnalité est utile?
Un cas d'utilisation, à la demande ...
Imaginez que vous ayez une liste d'éléments, par exemple des nombres entiers, des nombres à virgule flottante, des matrices, des chaînes de caractères, des formes d'onde, etc. Compte tenu de cette liste, vous souhaitez ajouter le contenu.
Une façon de le faire serait d'avoir un trait Addable
qui doit être hérité par chaque type pouvant être additionné, ou une conversion implicite en un Addable
s'il s'agit d'objets provenant d'une bibliothèque tierce. que vous ne pouvez pas adapter à des interfaces.
Cette approche devient rapidement décourageante lorsque vous souhaitez également commencer à ajouter d'autres opérations de ce type pouvant être effectuées à une liste d'objets. De plus, cela ne fonctionne pas bien si vous avez besoin d’alternatives (par exemple; l’ajout de deux formes d’ondes les concatène ou les superpose-t-il?) La solution est un polymorphisme ad hoc, où vous pouvez choisissez et choisissez le comportement à adapter aux types existants.
Pour le problème initial, vous pouvez alors implémenter une classe de type Addable
:
trait Addable[T] {
def zero: T
def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!
Vous pouvez ensuite en créer des instances implicites sous-classées, correspondant à chaque type que vous souhaitez rendre modifiable:
implicit object IntIsAddable extends Addable[Int] {
def zero = 0
def append(a: Int, b: Int) = a + b
}
implicit object StringIsAddable extends Addable[String] {
def zero = ""
def append(a: String, b: String) = a + b
}
//etc...
La méthode pour résumer une liste devient alors triviale pour écrire ...
def sum[T](xs: List[T])(implicit addable: Addable[T]) =
xs.FoldLeft(addable.zero)(addable.append)
//or the same thing, using context bounds:
def sum[T : Addable](xs: List[T]) = {
val addable = implicitly[Addable[T]]
xs.FoldLeft(addable.zero)(addable.append)
}
L'intérêt de cette approche réside dans le fait que vous pouvez fournir une définition alternative d'une classe de types, en contrôlant l'implicite que vous voulez dans la portée via les importations ou en fournissant explicitement l'argument autrement implicite. Il devient donc possible de fournir différentes manières d’ajouter des formes d’ondes, ou de spécifier une arithmétique modulo pour l’addition d’entiers. Il est également relativement simple d'ajouter un type d'une bibliothèque tierce à votre classe de types.
Incidemment, c'est exactement l'approche adoptée par l'API 2.8 Collections. Bien que la méthode sum
soit définie sur TraversableLike
au lieu de List
, et que la classe de type soit Numeric
(elle contient également quelques opérations de plus que la simple zero
et append
)
Relisez le premier commentaire ici:
Une distinction cruciale entre les classes de types et les interfaces est que pour que la classe A soit un "membre" d’une interface, elle doit le déclarer sur le site de sa propre définition. En revanche, tout type peut être ajouté à tout moment à une classe de types, à condition que vous puissiez fournir les définitions requises. Ainsi, les membres d'une classe de types dépendent à tout moment de l'étendue actuelle. Par conséquent, nous ne nous soucions pas de savoir si le créateur de A a anticipé la classe de types à laquelle nous souhaitons appartenir; sinon, nous pouvons simplement créer notre propre définition montrant qu'elle appartient effectivement, puis l'utiliser en conséquence. Cela ne constitue donc pas seulement une meilleure solution que les adaptateurs, il élimine en quelque sorte tout le problème que les adaptateurs étaient censés résoudre.
Je pense que c'est l'avantage le plus important des classes de types.
En outre, ils gèrent correctement les cas où les opérations n'ont pas l'argument du type que nous envoyons, ou en ont plusieurs. Par exemple. Considérons cette classe de type:
case class Default[T](val default: T)
object Default {
implicit def IntDefault: Default[Int] = Default(0)
implicit def OptionDefault[T]: Default[Option[T]] = Default(None)
...
}
Je considère les classes de types comme la possibilité d’ajouter des métadonnées de type sécurisées à une classe.
Donc, vous définissez d’abord une classe pour modéliser le domaine problématique, puis vous réfléchissez aux métadonnées à ajouter. Des choses comme Equals, Hashable, Viewable, etc. Cela crée une séparation du domaine du problème et des mécanismes pour utiliser la classe et ouvre les sous-classes parce que la classe est plus légère.
À part cela, vous pouvez ajouter des classes de type n'importe où dans l'étendue, pas seulement là où la classe est définie et vous pouvez modifier les implémentations. Par exemple, si je calcule un code de hachage pour une classe Point à l'aide de Point # hashCode, je suis limité à cette implémentation spécifique, qui risque de ne pas créer une bonne distribution de valeurs pour l'ensemble de points que j'ai. Mais si j'utilise Hashable [Point], je peux alors fournir ma propre implémentation.
[Mis à jour avec l'exemple] À titre d'exemple, voici un cas d'utilisation que j'ai eu la semaine dernière. Dans notre produit, il existe plusieurs cas de cartes contenant des conteneurs en tant que valeurs. Exemple: Map[Int, List[String]]
ou Map[String, Set[Int]]
. L'ajout à ces collections peut être prolixe:
map += key -> (value :: map.getOrElse(key, List()))
Je voulais donc avoir une fonction qui englobe tout ça pour pouvoir écrire
map +++= key -> value
Le problème principal est que les collections n'ont pas toutes les mêmes méthodes pour ajouter des éléments. Certains ont '+' alors que d'autres ': +'. Je souhaitais également conserver l'efficacité de l'ajout d'éléments à une liste. Je ne voulais donc pas utiliser fold/map pour créer de nouvelles collections.
La solution consiste à utiliser des classes de types:
trait Addable[C, CC] {
def add(c: C, cc: CC) : CC
def empty: CC
}
object Addable {
implicit def listAddable[A] = new Addable[A, List[A]] {
def empty = Nil
def add(c: A, cc: List[A]) = c :: cc
}
implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] {
def empty = cbf().result
def add(c: A, cc: Add) = (cbf(cc) += c).result
}
}
Ici, j'ai défini une classe de types Addable
pouvant ajouter un élément C à une collection CC. J'ai 2 implémentations par défaut: Pour les listes utilisant ::
et pour les autres collections, en utilisant le framework de générateur.
Ensuite, utiliser cette classe type est:
class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) {
def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = {
val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) ))
(map + pair).asInstanceOf[That]
}
def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf)
}
implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col
Le bit spécial utilise adder.add
pour ajouter les éléments et adder.empty
pour créer de nouvelles collections pour de nouvelles clés.
Pour comparer, sans classes de types, j'aurais eu 3 options: 1. écrire une méthode par type de collection. Ex., addElementToSubList
et addElementToSet
etc. Cela crée beaucoup de passe-partout dans l'implémentation et pollue l'espace de noms 2. utiliser la réflexion pour déterminer si la sous-collection est une liste/un ensemble. Ceci est délicat car la carte est vide pour commencer (bien sûr, scala aide ici aussi avec Manifests) 3. avoir la classe de type de l'homme pauvre en obligeant l'utilisateur à fournir l'additionneur. Donc, quelque chose comme addToMap(map, key, value, adder)
, qui est tout simplement laid
Une autre façon de trouver ce billet de blog utile est de décrire les classes de types: Les monades ne sont pas des métaphores
Rechercher dans l'article pour classeclass. Ce devrait être le premier match. Dans cet article, l'auteur fournit un exemple d'une classe de types Monad.
Une façon de voir les classes de types est qu'elles activent retroactive extension ou retroactive polymorphism. Casual Miracles et Daniel Westheide présentent des exemples d'utilisation de classes de types dans Scala pour y parvenir.
Voici un post sur mon blog qui explore diverses méthodes dans la scala de retroactive supertyping, une sorte d’extension rétroactive, incluant un exemple de type.
Le fil de discussion " Qu'est-ce qui rend les classes de type meilleures que les traits? " fait ressortir quelques points intéressants:
- Les classes de types peuvent très facilement représenter des notions assez difficiles à représenter en présence de sous-typage, telles que l'égalité et ordering.
Exercice: créez une petite hiérarchie de classes/traits et essayez d'implémenter.equals
sur chaque classe/traits de telle sorte que l'opération sur les instances arbitraires de la hiérarchie soit correctement réflexive, symétrique et transitive.- Les classes de types vous permettent de prouver qu'un type situé en dehors de votre "contrôle" est conforme à un comportement donné.
Le type de quelqu'un d'autre peut être un membre de votre classe.- Vous ne pouvez pas exprimer "cette méthode prend/retourne une valeur du même type que la méthode receveur" en termes de sous-typage, mais cette contrainte (très utile) est simple à l'aide de classes de types. Il s'agit du problème des types bornés f (où un type lié F est paramétré sur ses propres sous-types).
- Toutes les opérations définies sur un trait nécessitent une instance; il y a toujours un argument
this
. Vous ne pouvez donc pas définir par exemple une méthodefromString(s:String): Foo
surtrait Foo
de manière à pouvoir l'appeler sans une instance deFoo
.
En Scala, cela se manifeste par des personnes qui essaient désespérément d’abstraire des objets compagnons.
Mais il est simple avec une classe, comme l’illustre l’élément zéro dans cet exemple monoïde .- Les classes de types peuvent être définies de manière inductive; Par exemple, si vous avez un
JsonCodec[Woozle]
, vous pouvez obtenir unJsonCodec[List[Woozle]]
gratuitement.
L'exemple ci-dessus illustre ceci pour "les choses que vous pouvez ajouter ensemble".
Dans les cours de type scala
Le comportement peut être étendu - au moment de la compilation - après coup - sans changer/recompiler le code existant
Scala Implique
La dernière liste de paramètres d'une méthode peut être marquée implicite
Les paramètres implicites sont renseignés par le compilateur
En effet, vous avez besoin d'une preuve du compilateur
… Comme l'existence d'une classe de type dans la portée
Vous pouvez également spécifier explicitement les paramètres, si nécessaire
Ci-dessous, Exemple d'extension sur la classe String avec l'implémentation de la classe de type étend la classe avec une nouvelle méthode même si la chaîne est finale :)
/**
* Created by nihat.hosgur on 2/19/17.
*/
case class PrintTwiceString(val original: String) {
def printTwice = original + original
}
object TypeClassString extends App {
implicit def stringToString(s: String) = PrintTwiceString(s)
val name: String = "Nihat"
name.printTwice
}
Je ne connais pas d'autre cas d'utilisation que polymorhisme ad hoc qui est expliqué ici de la meilleure façon possible.
Implicite _ et classes-types sont utilisés pour Type-conversion. Le principal cas d'utilisation pour les deux est de fournir le polymorphisme ad-hoc (i.e) sur des classes que vous ne pouvez pas modifier mais attendez-vous à un polymorphisme de type héritage. En cas d'implication, vous pouvez utiliser à la fois une définition implicite ou une classe implicite (qui est votre classe wrapper mais masquée du client). Les classes de types sont plus puissantes car elles peuvent ajouter des fonctionnalités à une chaîne d'héritage existante (par exemple: Ordering [T] dans la fonction de tri de scala). Pour plus de détails, vous pouvez voir https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/
Ceci est une différence importante (nécessaire pour la programmation fonctionnelle):
considérer inc:Num a=> a -> a
:
a
reçu est le même que celui retourné, cela ne peut pas être fait avec le sous-typage
J'aime utiliser les classes de types en tant que forme idiomatique Scala légère de l'injection de dépendance qui fonctionne toujours avec des dépendances circulaires sans pour autant ajouter beaucoup de complexité au code. J'ai récemment réécrit un projet Scala consistant à utiliser le modèle de gâteau pour taper des classes pour DI et à réduire de 59% la taille du code.