web-dev-qa-db-fra.com

Un bon exemple de paramètre implicite dans Scala?

Jusqu'à présent, les paramètres implicites dans Scala ne me semblent pas bons - ils sont trop proches des variables globales, cependant puisque Scala semble être un langage plutôt strict que je commence) doutant à mon avis :-).

Question: pourriez-vous montrer un bon exemple réel (ou proche) lorsque les paramètres implicites fonctionnent vraiment. IOW: quelque chose de plus sérieux que showPrompt, qui justifierait une telle conception de langage.

Ou au contraire - pourriez-vous montrer une conception de langage fiable (peut être imaginaire) qui rendrait implicite non nécessaire. Je pense que même aucun mécanisme n'est meilleur que les implicites car le code est plus clair et il n'y a pas de devinettes.

Veuillez noter que je pose des questions sur les paramètres, pas sur les fonctions implicites (conversions)!

Mises à jour

Variables globales

Merci pour toutes les bonnes réponses. Peut-être que je clarifie mon objection aux "variables globales". Considérez une telle fonction:

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

tu l'appelles

max(5,6);

vous pourriez (!) le faire comme ceci:

max(x:5,y:6);

mais à mes yeux implicits fonctionne comme ceci:

x = 5;
y = 6;
max()

ce n'est pas très différent d'une telle construction (comme PHP)

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

La réponse de Derek

C'est un excellent exemple, cependant si vous pouvez considérer comme une utilisation flexible de l'envoi de messages n'utilisant pas implicit veuillez poster un contre-exemple. Je suis vraiment curieux de connaître la pureté dans la conception des langages ;-).

73
greenoldman

Dans un sens, oui, les implicites représentent un état global. Cependant, ils ne sont pas modifiables, 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 exigent généralement que vous transformiez toutes les constantes de votre code en constantes ou enums, qui sont généralement globaux.

Notez également que les implicites sont pas dans un espace de noms 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 de packages de ces types.

Alors, prenez vos globales, rendez-les immuables et initialisées sur le site de déclaration, et mettez-les sur des espaces de noms. Ressemblent-ils toujours à des globaux? Ont-ils toujours l'air problématiques?

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

Quant aux cas d'utilisation, ils sont nombreux, mais nous pouvons faire un bref examen en fonction de leur historique. À l'origine, afaik, Scala n'avait aucune implication. Ce que Scala avait étaient des types de vue, une fonctionnalité que beaucoup d'autres langues avaient. Nous pouvons encore le voir aujourd'hui chaque fois que vous écrire 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 conversions automatiques disponibles sur les paramètres de type (génériques).

Scala alors généralisé cette fonctionnalité avec implicits. Les conversions automatiques n'existent plus, et, à la place, vous avez conversions implicites - qui ne sont que Function1 valeurs et, par conséquent, peuvent être passées en tant que paramètres. À partir de maintenant, T <% Ordered[T] signifiait qu'une valeur pour une conversion implicite serait passée en paramètre. Comme la conversion est automatique, l'appelant de la fonction n'est pas obligé de passer explicitement le paramètre - donc ces paramètres sont devenus paramètres implicites.

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

Quoi qu'il en soit, les types de vues sont devenus du sucre syntaxique pour les conversions implicites transmises 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, permettant de passer tout type de paramètres implicites, au lieu de simplement Function1. Leur utilisation réelle a ensuite suivi, et le sucre syntaxique pour ces utilisations est venu en dernier.

L'un d'eux est Context Bounds, utilisé pour implémenter type class pattern (pattern car ce n'est pas une fonction intégrée, juste une façon d'utiliser le langage qui fournit des fonctionnalité à la classe de type de Haskell). Un contexte lié 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. C'est ça:

new Array[Int](size)

Cela utilise un contexte lié à un manifeste de classe, pour permettre une telle initialisation de tableau. Nous pouvons voir cela 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 derniers sont principalement utilisés avec des collections, avec des méthodes telles que max, sum et map. Scalaz est une bibliothèque qui utilise largement les limites du contexte.

Un autre usage courant consiste à diminuer la plaque de chaudière sur les opérations qui doivent partager un paramètre commun. Par exemple, les transactions:

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 alors simplifié comme ceci:

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

Ce modèle est utilisé avec la mémoire transactionnelle et je pense (mais je ne suis pas sûr) que la bibliothèque d'E/S Scala l'utilise également).

Le troisième usage courant auquel 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, sinon, entraîneraient des exceptions au moment de l'exécution. Par exemple, voyez cette définition sur Option:

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

Cela 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 utilise largement cette fonctionnalité est Shapeless.

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

Si vous aimez être prescrit à (comme, disons, Python le fait), alors Scala n'est pas fait pour vous).

97
Daniel C. Sobral

Sûr. Akka en a un excellent exemple en ce qui concerne ses acteurs. Lorsque vous êtes dans la méthode receive d'un acteur, vous pouvez envoyer un message à un autre acteur. Lorsque vous faites cela, Akka regroupera (par défaut) l'acteur actuel en tant que sender du message, comme ceci:

trait ScalaActorRef { this: ActorRef =>
  ...

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

  ...
}

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 la portée de votre propre code et vous permet de faire des choses faciles comme ceci:

someOtherActor ! SomeMessage

Maintenant, vous pouvez également le faire si vous le souhaitez:

someOtherActor.!(SomeMessage)(self)

ou

someOtherActor.!(SomeMessage)(null)

ou

someOtherActor.!(SomeMessage)(anotherActorAltogether)

Mais normalement non. Vous conservez simplement l'utilisation naturelle rendue possible par la définition de valeur implicite dans le trait Acteur. Il y a environ un million d'autres exemples. Les classes de collecte sont énormes. Essayez de vous promener dans n'importe quelle bibliothèque non triviale Scala et vous trouverez un camion.

23
Derek Wyatt

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

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

Ceux-ci ne peuvent être définis de manière sensible qu'en cas d'opération < sur A. Donc, sans implication, nous devons fournir le contexte Ordering[B] chaque fois que nous aimerions utiliser cette fonction. (Ou abandonnez la vérification statique de type dans max et risquez une erreur de transtypage lors de l'exécution.)

Si toutefois, une classe de type de comparaison implicite est dans la portée, par ex. certains Ordering[Int], nous pouvons simplement 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 implications peuvent être occultées et il peut donc y avoir des situations dans lesquelles l'implicite réel qui est dans la portée n'est pas suffisamment clair. Pour les utilisations simples de max ou sort, il pourrait en effet suffire d'avoir un ordre fixe trait sur Int et d'utiliser une syntaxe pour vérifier si ce trait est disponible. Mais cela signifierait qu'il ne pourrait pas y avoir de traits supplémentaires et que chaque morceau de code devrait utiliser les traits qui ont été définis à l'origine.

Addition:
Réponse à la comparaison de la variable globale .

Je pense que vous avez raison dans un code coupé 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.

il peut sentir des variables globales pourries et mauvaises. Le point crucial, cependant, est qu'il ne peut y avoir qu'une seule variable implicite par type dans la portée. Votre exemple avec deux Int ne fonctionnera pas.

Cela signifie également que, pratiquement, les variables implicites ne sont utilisées que lorsqu'il existe une instance principale pas nécessairement unique mais distincte pour un type. La référence self d'un acteur est un bon exemple d'une telle chose. L'exemple de classe de type est un autre exemple. 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. (À un autre niveau, le numéro de ligne réel dans le code lui-même peut également constituer une bonne variable implicite tant qu'il utilise un type très distinctif.)

Normalement, vous n'utilisez pas implicits pour les types de tous les jours. Et avec des types spécialisés (comme Ordering[Int]) il n'y a pas trop de risques à les observer.

9
Debilski

D'après mon expérience, il n'y a pas vraiment de bon exemple d'utilisation des paramètres implicites ou de la conversion implicite.

Le petit avantage de l'utilisation des implicits (ne pas avoir besoin d'écrire explicitement un paramètre ou un type) est redondant par rapport aux problèmes qu'ils créent.

Je suis développeur depuis 15 ans et je travaille avec scala depuis 1,5 an).

J'ai vu à plusieurs reprises des bogues causés par le développeur qui ne savait pas que des implicits étaient utilisés et qu'une fonction spécifique renvoyait en fait un type différent de celui spécifié. En raison de la conversion implicite.

J'ai également entendu des déclarations disant que si vous n'aimez pas les implicites, ne les utilisez pas. Ce n'est pas pratique dans le monde réel car de nombreuses fois des bibliothèques externes sont utilisées, et beaucoup d'entre elles utilisent des implicits, donc votre code utilise des implicits, et vous ne le savez peut-être pas. Vous pouvez écrire un code qui a soit:

import org.some.common.library.{TypeA, TypeB}

ou:

import org.some.common.library._

Les deux codes seront compilés et exécutés. Mais ils ne produiront pas toujours les mêmes résultats puisque la deuxième version importe des conversions implicites qui feront que le code se comportera différemment.

Le "bogue" qui en résulte peut se produire très longtemps après l'écriture du code, au cas où certaines valeurs affectées par cette conversion n'auraient pas été utilisées à l'origine.

Une fois que vous rencontrez le bug, ce n'est pas une tâche facile de trouver la cause. Vous devez faire une enquête approfondie.

Même si vous vous sentez comme un expert en scala une fois que vous avez trouvé le bogue et que vous l'avez corrigé en changeant une déclaration d'importation, vous avez en fait perdu beaucoup de temps précieux.

Les raisons supplémentaires pour lesquelles je suis généralement contre les implicites sont:

  • Ils rendent le code difficile à comprendre (il y a moins de code, mais vous ne savez pas ce qu'il fait)
  • Temps de compilation. scala se compile beaucoup plus lentement lorsque des implicits sont utilisés.
  • En pratique, il change la langue de typé statiquement en typé dynamiquement. Il est vrai qu'une fois que vous suivez des directives de codage très strictes, vous pouvez éviter de telles situations, mais dans le monde réel, ce n'est pas toujours le cas. Même en utilisant IDE 'supprimer les importations inutilisées', votre code peut toujours être compilé et exécuté, mais pas comme avant la suppression des importations 'inutilisées'.

Il n'y a pas d'option pour compiler scala sans implicites (s'il y en a s'il vous plaît corrigez-moi), et s'il y avait une option, aucune de la communauté commune scala bibliothèques aurait compiler.

Pour toutes les raisons ci-dessus, je pense que les implicites sont l'une des pires pratiques utilisées par la langue scala.

Scala a de nombreuses fonctionnalités intéressantes, et beaucoup moins géniales.

Lors du choix d'une langue pour un nouveau projet, les implications sont l'une des raisons contre scala, pas en sa faveur. À mon avis.

5
noam

C'est facile, rappelez-vous juste:

  • pour déclarer la variable à passer comme implicite aussi
  • pour déclarer tous les paramètres implicites après les paramètres non implicites dans un séparé ()

par exemple.

def myFunction(): Int = {
  implicit val y: Int = 33
  implicit val z: Double = 3.3

  functionWithImplicit("foo") // calls functionWithImplicit("foo")(y, z)
}

def functionWithImplicit(foo: String)(implicit x: Int, d: Double) = // blar blar
4
samthebest

Une autre bonne utilisation générale des paramètres implicites est de 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 cadre des collections et des méthodes comme map, dont la signature complète est généralement:

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 de cela, 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).

4

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

Sans implicites, vous passeriez une telle chose tout le temps, ce qui rendrait l'utilisation normale encombrante. Ou utilisez des collections moins spécialisées, ce qui serait ennuyeux car cela signifierait que vous perdez performances/puissance.

3
Jens Schauder

Je commente ce post un peu en retard, mais j'ai commencé à apprendre scala récemment. Daniel et d'autres ont donné un bon aperçu du mot-clé implicite. Je voudrais me donner deux cents sur la variable implicite à partir de l'utilisation pratique la perspective.

Scala est le mieux adapté s'il est utilisé pour écrire des codes Apache Spark. Dans Spark, nous avons le contexte spark et probablement la classe de configuration qui peut récupérer les clés de configuration)./valeurs d'un fichier de configuration.

Maintenant, si j'ai une classe abstraite et si je déclare un objet de configuration et le contexte spark comme suit: -

abstract class myImplicitClass {

implicit val config = new myConfigClass()

val conf = new SparkConf().setMaster().setAppName()
implicit val sc = new SparkContext(conf)

def overrideThisMethod(implicit sc: SparkContext, config: Config) : Unit
}

class MyClass extends myImplicitClass {

override def overrideThisMethod(implicit sc: SparkContext, config: Config){

/*I can provide here n number of methods where I can pass the sc and config 
objects, what are implicit*/
def firstFn(firstParam: Int) (implicit sc: SparkContext, config: Config){ 
    /*I can use "sc" and "config" as I wish: making rdd or getting data from cassandra, for e.g.*/
    val myRdd = sc.parallelize(List("abc","123"))
}
def secondFn(firstParam: Int) (implicit sc: SparkContext, config: Config){
 /*following are the ways we can use "sc" and "config" */

        val keyspace = config.getString("keyspace")
        val tableName = config.getString("table")
        val hostName = config.getString("Host")
        val userName = config.getString("username")
        val pswd = config.getString("password")

    implicit val cassandraConnectorObj = CassandraConnector(....)
    val cassandraRdd = sc.cassandraTable(keyspace, tableName)
}

}
}

Comme nous pouvons le voir ci-dessus, j'ai deux objets implicites dans ma classe abstraite, et j'ai passé ces deux variables implicites en tant que paramètres implicites fonction/méthode/définition. Je pense que c'est le meilleur cas d'utilisation que nous pouvons représenter en termes d'utilisation de variables implicites.

0
anshuman sharma