75 votes

Bon exemple de paramètre implicite en Scala ?

Jusqu'à présent, les paramètres implicites en Scala ne me semblent pas bons - ils sont trop proches des variables globales, mais comme Scala semble être un langage plutôt strict, je commence à douter de mon propre avis :-).

Question : Pourriez-vous nous montrer un exemple réel (ou proche) où les paramètres implicites fonctionnent vraiment. C'est-à-dire quelque chose de plus sérieux que showPrompt qui justifierait la conception d'un tel langage.

Ou au contraire, pourriez-vous montrer une conception fiable du langage (qui peut être imaginaire) qui rendrait l'implicite inutile. Je pense que même l'absence de mécanisme est préférable aux implicites, car le code est plus clair et il n'y a pas de devinette.

Veuillez noter que je demande des paramètres, et non des fonctions implicites (conversions) !

Mises à jour

Variables globales

Merci pour toutes les bonnes réponses. Je vais peut-être clarifier mon objection concernant les "variables globales". Considérons une telle fonction :

max(x : Int,y : Int) : Int

vous l'appelez

max(5,6);

vous pourriez ( !) le faire comme ceci :

max(x:5,y:6);

mais dans mes yeux implicits fonctionne comme ça :

x = 5;
y = 6;
max()

il n'est pas très différent d'une telle construction (de type PHP)

max() : Int
{
  global x : Int;
  global y : Int;
  ...
}

Réponse de Derek

C'est un excellent exemple, mais si vous pensez à une utilisation flexible de l'envoi de message sans utiliser implicit veuillez poster un contre-exemple. Je suis vraiment curieux de la pureté dans la conception des langues ;-).

0 votes

Si vous faites un implicite global (et vous ne pouvez pas - le mieux que vous puissiez faire est un implicite à l'échelle du paquet), alors votre déclaration pourrait être vraie, mais seulement si vous choisissez de faire une telle chose... ne le faites pas. Et, en fin de compte, la flexibilité de cette API provient de l'utilisation des implicites. Si vous ne les utilisez pas, vous ne pouvez pas obtenir la même flexibilité. Donc, vous demandez de supprimer la fonctionnalité qui rend cette API géniale, tout en la rendant géniale. C'est une demande très étrange.

0 votes

Derek Wyatt, le dernier commentaire est quelque peu étrange - ne cherchez-vous pas l'optimisation dans la vie ? Je le fais. Maintenant, à propos des variables globales -- je ne dis pas que vous devez avoir des variables globales pour utiliser des implicites, je dis qu'ils sont similaires dans leur utilisation. Parce qu'ils sont liés par le nom de l'appelé, implicitement, et ils sont pris hors de la portée de l'appelant, pas de l'appel réel.

99voto

Daniel C. Sobral Points 159554

Dans un sens, oui, les implicites représentent l'état global. Cependant, ils ne sont pas mutables, ce qui est le vrai problème avec les variables globales -- vous ne voyez pas les gens se plaindre des constantes globales, n'est-ce pas ? En fait, les normes de codage imposent généralement de transformer toutes les constantes de votre code en constantes ou enums, qui sont généralement globales.

Notez également que les implicites sont no dans un espace de nom plat, ce qui est également un problème courant avec les globaux. Ils sont explicitement liés aux types et, par conséquent, à la hiérarchie des paquets de ces types.

Prenez donc vos globaux, rendez-les immuables et initialisés à l'endroit de la déclaration, et placez-les dans des espaces de noms. Est-ce qu'ils ressemblent toujours à des globaux ? Sont-ils toujours problématiques ?

Mais ne nous arrêtons pas là. Implicites sont liés aux types, et ils sont tout aussi "globaux" que les types. Le fait que les types soient globaux vous dérange-t-il ?

Quant aux cas d'utilisation, ils sont nombreux, mais nous pouvons faire un bref rappel basé sur leur historique. À l'origine, afaik, Scala n'avait pas d'implicites. Ce que Scala avait, c'était des types de vue, une fonctionnalité que beaucoup d'autres langages avaient. Nous pouvons encore le constater aujourd'hui lorsque vous écrivez quelque chose comme T <% Ordered[T] ce qui signifie que le type T peut être considéré comme un type Ordered[T] . Les types de vue sont un moyen de rendre les casts automatiques disponibles sur les paramètres de type (génériques).

Scala alors généralisé cette caractéristique avec les implicites. Les casts automatiques n'existent plus et, à la place, vous avez conversions implicites -- qui sont juste Function1 et, par conséquent, peuvent être transmises comme paramètres. Dès lors, T <% Ordered[T] signifiait qu'une valeur pour une conversion implicite serait passée comme paramètre. Puisque la conversion est automatique, l'appelant de la fonction n'est pas obligé de passer explicitement le paramètre -- ainsi ces paramètres sont devenus paramètres implicites .

Notez qu'il y a deux concepts -- conversions implicites et paramètres implicites -- qui sont très proches, mais qui ne se recouvrent pas complètement.

Quoi qu'il en soit, les types de vues sont devenus un sucre syntaxique pour les conversions implicites passées implicitement. Ils seraient réécrits comme ceci :

def max[T <% Ordered[T]](a: T, b: T): T = if (a < b) b else a
def max[T](a: T, b: T)(implicit $ev1: Function1[T, Ordered[T]]): T = if ($ev1(a) < b) b else a

Les paramètres implicites sont simplement une généralisation de ce modèle, ce qui rend possible le passage de tout des paramètres implicites, au lieu de simplement Function1 . Leur utilisation effective a ensuite suivi, et le sucre syntaxique pour les ceux Les utilisations sont venues plus tard.

L'un d'eux est Limites du contexte utilisé pour mettre en œuvre le modèle de classe de type (pattern car il ne s'agit pas d'une fonctionnalité intégrée, mais simplement d'une façon d'utiliser le langage qui fournit une fonctionnalité similaire à la classe de type de Haskell). Un context bound est utilisé pour fournir un adaptateur qui implémente une fonctionnalité inhérente à une classe, mais non déclarée par celle-ci. Il offre les avantages de l'héritage et des interfaces sans leurs inconvénients. Par exemple :

def max[T](a: T, b: T)(implicit $ev1: Ordering[T]): T = if ($ev1.lt(a, b)) b else a
// latter followed by the syntactic sugar
def max[T: Ordering](a: T, b: T): T = if (implicitly[Ordering[T]].lt(a, b)) b else a

Vous l'avez probablement déjà utilisé. Il y a un cas d'utilisation courant que les gens ne remarquent généralement pas. Il s'agit de ceci :

new Array[Int](size)

Cela utilise un contexte lié à une classe manifeste, pour permettre une telle initialisation de tableau. Nous pouvons le voir avec cet exemple :

def f[T](size: Int) = new Array[T](size) // won't compile!

Vous pouvez l'écrire comme ceci :

def f[T: ClassManifest](size: Int) = new Array[T](size)

Sur la bibliothèque standard, les limites de contexte les plus utilisées sont :

Manifest      // Provides reflection on a type
ClassManifest // Provides reflection on a type after erasure
Ordering      // Total ordering of elements
Numeric       // Basic arithmetic of elements
CanBuildFrom  // Collection creation

Les trois dernières sont principalement utilisées avec les collections, avec des méthodes telles que max , sum y map . Scalaz est une bibliothèque qui fait un usage intensif des limites de contexte.

Une autre utilisation courante consiste à réduire le nombre d'opérations qui doivent partager un paramètre commun. Par exemple, les opérations :

def withTransaction(f: Transaction => Unit) = {
  val txn = new Transaction

  try { f(txn); txn.commit() }
  catch { case ex => txn.rollback(); throw ex }
}

withTransaction { txn =>
  op1(data)(txn)
  op2(data)(txn)
  op3(data)(txn)
}

Ce qui est ensuite simplifié comme suit :

withTransaction { implicit txn =>
  op1(data)
  op2(data)
  op3(data)
}

Ce modèle est utilisé avec la mémoire transactionnelle, et je pense (mais je n'en suis pas sûr) que la bibliothèque Scala I/O l'utilise également.

La troisième utilisation courante à laquelle je peux penser est de faire des preuves sur les types qui sont passés, ce qui permet de détecter au moment de la compilation des choses qui, autrement, entraîneraient des exceptions au moment de l'exécution. Par exemple, voir cette définition sur Option :

def flatten[B](implicit ev: A <:< Option[B]): Option[B]

C'est ce qui rend cela possible :

scala> Option(Option(2)).flatten // compiles
res0: Option[Int] = Some(2)

scala> Option(2).flatten // does not compile!
<console>:8: error: Cannot prove that Int <:< Option[B].
              Option(2).flatten // does not compile!
                        ^

Une bibliothèque qui fait un usage intensif de cette fonctionnalité est Shapeless.

Je ne pense pas que l'exemple de la bibliothèque Akka corresponde à l'une de ces quatre catégories, mais c'est là tout l'intérêt des fonctionnalités génériques : les gens peuvent les utiliser de toutes sortes de manières, au lieu des manières prescrites par le concepteur du langage.

Si vous aimez qu'on vous prescrive quelque chose (comme, par exemple, Python), alors Scala n'est pas fait pour vous.

7 votes

Le livre que vous écrivez devrait être définitivement en anglais ! :-) Merci pour cet article.

2 votes

Pourquoi SO ne donne-t-il pas une option d'étoile pour une réponse comme celle-ci ? Très bon article !

23voto

Derek Wyatt Points 2070

Bien sûr. Akka en a un excellent exemple avec ses Acteurs. Quand vous êtes à l'intérieur d'un Acteur. receive vous pourriez vouloir envoyer un message à un autre Acteur. Lorsque vous faites cela, Akka regroupera (par défaut) l'Acteur courant en tant que sender du message, comme ceci :

trait ScalaActorRef { this: ActorRef =>
  ...

  def !(message: Any)(implicit sender: ActorRef = null): Unit

  ...
}

Le site sender est implicite. Dans l'Acteur il y a une définition qui ressemble à :

trait Actor {
  ...

  implicit val self = context.self

  ...
}

Cela crée la valeur implicite dans le cadre de votre propre code, et vous permet de faire des choses faciles comme celles-ci :

someOtherActor ! SomeMessage

Maintenant, vous pouvez faire ça aussi, si vous voulez :

someOtherActor.!(SomeMessage)(self)

ou

someOtherActor.!(SomeMessage)(null)

ou

someOtherActor.!(SomeMessage)(anotherActorAltogether)

Mais normalement, vous ne le faites pas. Vous gardez simplement l'usage naturel qui est rendu possible par la définition implicite de la valeur dans le trait Acteur. Il y a environ un million d'autres exemples. Les classes de collection en sont un exemple énorme. Essayez de vous promener dans n'importe quelle bibliothèque Scala non triviale et vous en trouverez une multitude.

0 votes

Je pense que c'est un meilleur exemple que Traversable.max les classes de type et autres.

0 votes

Voici un bon exemple. Dans une certaine mesure, je pense que les variables implicites sont un moyen d'ÉVITER les variables globales et les "singletons de Dieu" (en l'absence d'un meilleur mot) tout en gardant votre code plus lisible car vous n'avez pas à passer explicitement certains éléments de base (les singletons mentionnés ci-dessus). Et encore une fois, vous pouvez toujours les passer explicitement, par exemple lors des tests. Je pense donc que dans de nombreux cas, ils permettent un couplage plus lâche et un code plus propre.

0 votes

@vertti, pas exactement. Je pense que la façon dont le C++ fonctionne est meilleure ici -- c'est-à-dire des paramètres par classe entière et/ou des arguments par défaut. Pour moi, l'idée que la fonction aspire un argument pris quelque part par elle-même est très étrange.

9voto

Debilski Points 28586

Un exemple serait les opérations de comparaison sur Traversable[A] . Par exemple max o sort :

def max[B >: A](implicit cmp: Ordering[B]) : A

Ceux-ci ne peuvent être définis de manière sensée que lorsqu'il y a une opération < sur A . Donc, sans les implicites, nous devrions fournir le contexte Ordering[B] à chaque fois qu'on voudra utiliser cette fonction. (Ou abandonner la vérification statique du type dans max et risquer une erreur de cast d'exécution).

Si toutefois, une comparaison implicite classe de type est dans le champ d'application, par exemple certaines Ordering[Int] nous pouvons l'utiliser immédiatement ou simplement changer la méthode de comparaison en fournissant une autre valeur pour le paramètre implicite.

Bien sûr, les implicites peuvent être masqués et il peut donc y avoir des situations dans lesquelles l'implicite réel qui est dans le champ d'application n'est pas suffisamment clair. Pour les utilisations simples de max o sort il pourrait en effet être suffisant d'avoir un ordre fixe trait sur Int et utiliser une syntaxe pour vérifier si ce trait est disponible. Mais cela signifierait qu'il n'y aurait pas de traits supplémentaires et que chaque morceau de code devrait utiliser les traits définis à l'origine.

Ajout :
Réponse à la variable globale comparaison.

Je pense que vous avez raison de dire que dans un extrait de code comme

implicit val num = 2
implicit val item = "Orange"
def shopping(implicit num: Int, item: String) = {
  "I’m buying "+num+" "+item+(if(num==1) "." else "s.")
}

scala> shopping
res: java.lang.String = I’m buying 2 Oranges.

ça peut sentir les variables globales pourries et maléfiques. Le point crucial, cependant, est qu'il ne peut y avoir qu'une seule variable implicite par type dans le champ d'application. Votre exemple avec deux Int ne va pas fonctionner.

Cela signifie également que, dans la pratique, les variables implicites ne sont utilisées que lorsqu'il existe une instance primaire pas nécessairement unique mais distincte pour un type. Le site self La référence d'un acteur est un bon exemple pour une telle chose. L'exemple de la classe de type en est un autre. Il peut y avoir des dizaines de comparaisons algébriques pour n'importe quel type, mais il y en a une qui est spéciale. (A un autre niveau, la comparaison réelle numéro de ligne dans le code lui-même pourrait également faire une bonne variable implicite, tant qu'elle utilise un type très distinctif).

Normalement, vous n'utilisez pas implicit pour les types courants. Et avec les types spécialisés (comme Ordering[Int] ), il n'y a pas trop de risque à les suivre.

0 votes

Merci, mais il s'agit en fait d'un contre-exemple -- cela devrait être un "trait" de l'instance de la collection. Et ensuite, vous pourriez utiliser max() qui utiliserait l'ordre de la collection ou max(comparer) qui utiliserait un ordre personnalisé.

2 votes

Bien sûr, ce serait possible. Mais cela signifierait également que l'on ne pourrait pas ajouter un autre trait à, par exemple. Int ou tout autre type prédéfini, selon les besoins. (Un exemple souvent cité est celui d'un semi groupe qui pourrait ne pas être un trait original sur Int, ni sur String - et il ne serait pas non plus possible d'ajouter ce trait sous une forme fixe). Le problème est le suivant : Il n'y a aucun moyen de généraliser un type sur tous les traits possibles. Il s'agit toujours de code (annotations de type) qui doit être donné ad-hoc ou vous perdez la sécurité de type. Les variables implicites réduisent simplement le code de base pour cela.

0 votes

Non Int's une collection donnée de Int's comme List ou Array. Si vous supposez que les éléments sont comparables et que vous écrivez un tel implicit comme ci-dessus, vous pourriez aussi définir l'ordre en haut de la classe (comme en C++). En C++, l'espace de noms n'est pas pollué par des noms arbitraires comme "cmp" ici, parce que vous passez la valeur.

4voto

Jean-Philippe Pellet Points 25240

Une autre bonne utilisation générale des paramètres implicites consiste à faire dépendre le type de retour d'une méthode du type de certains des paramètres qui lui sont passés. Un bon exemple, mentionné par Jens, est le framework collections, et des méthodes comme map dont la signature complète est habituellement :

def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[GenSeq[A], B, That]): That

Notez que le type de retour That est déterminé par le meilleur ajustement CanBuildFrom que le compilateur peut trouver.

Pour un autre exemple, voir cette réponse . Là, le type de retour de la méthode Arithmetic.apply est déterminé en fonction d'un certain type de paramètre implicite ( BiConverter ).

0 votes

J'ai peut-être raté quelque chose. Vous ne pouvez pas deviner ici le type Ça, vous devez donc le spécifier, non ? Ne serait-ce pas la même chose, si vous omettez le type That, et convertissez simplement le résultat à la main : map(it => it.foo).toBar() au lieu de map[B,List[Bars]](it => it.foo) ?

0 votes

@macias : Ce dernier ne crée pas de collection intermédiaire. Lorsque vous appelez toBar explicitement, il faut d'abord créer un Foo qui est ensuite converti en Bar. Lorsqu'il y a un paramètre de type, une barre peut être créée directement.

3 votes

@macias : Si tu le convertis à la main, tu le fais dans un deuxième temps, après. Vous pouvez obtenir un List en retour et qu'il faut ensuite le traverser à nouveau pour obtenir une Set . Grâce à l'utilisation de l'annotation implicite, il est possible pour l'utilisateur d'avoir accès à l'information. map afin d'éviter d'initialiser et de remplir la mauvaise collection en premier lieu.

3voto

Jens Schauder Points 23468

Les paramètres implicites sont très utilisés dans l'API de collecte. De nombreuses fonctions obtiennent un CanBuildFrom implicite, qui garantit que vous obtenez la "meilleure" implémentation de la collection de résultats.

Sans les implicites, vous devriez soit passer une telle chose tout le temps, ce qui rendrait l'utilisation normale encombrante. Soit vous utiliseriez des collections moins spécialisées, ce qui serait ennuyeux car vous perdriez en performances et en puissance.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X