design scala parameters implicit

design - Buen ejemplo de parámetro implícito en Scala?



parameters implicit (8)

En cierto sentido, sí, las implícitas representan el estado global. Sin embargo, no son mutables, que es el verdadero problema con las variables globales: no ves gente quejándose de las constantes globales, ¿verdad? De hecho, los estándares de codificación suelen dictar que se transformen las constantes de su código en constantes o enumeraciones, que generalmente son globales.

Tenga en cuenta también que los implicits no están en un espacio de nombres plano, que también es un problema común con los globales. Están vinculados explícitamente a tipos y, por lo tanto, a la jerarquía de paquetes de esos tipos.

Por lo tanto, tome sus globales, hágalos inmutables e inicializados en el sitio de declaración y colóquelos en espacios de nombres. ¿Todavía se ven como globos globales? ¿Todavía se ven problemáticos?

Pero no nos detengamos allí. Los Implicits están vinculados a los tipos, y son tan "globales" como los tipos. ¿El hecho de que los tipos sean globales te molesta?

En cuanto a los casos de uso, son muchos, pero podemos hacer una breve revisión en función de su historial. Originalmente, afaik, Scala no tenía implicaciones. Lo que Scala tenía eran tipos de vista, una característica que tenían muchos otros idiomas. Todavía podemos ver eso hoy cada vez que escriba algo como T <% Ordered[T] , lo que significa que el tipo T se puede ver como un tipo Ordered[T] . Los tipos de vista son una forma de hacer moldes automáticos disponibles en parámetros de tipo (genéricos).

Scala luego generalizó esa característica con implicaciones. Los moldes automáticos ya no existen y, en cambio, tiene conversiones implícitas , que son solo valores de Function1 y, por lo tanto, se pueden pasar como parámetros. A partir de entonces, T <% Ordered[T] significaba que un valor para una conversión implícita pasaría como parámetro. Como el reparto es automático, el llamador de la función no es necesario para pasar explícitamente el parámetro, por lo que esos parámetros se convirtieron en parámetros implícitos .

Tenga en cuenta que hay dos conceptos, conversiones implícitas y parámetros implícitos, que son muy cercanos, pero no se superponen por completo.

De todos modos, los tipos de vista se convirtieron en azúcar sintáctico para conversiones implícitas que se pasan implícitamente. Ellos serían reescritos así:

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

Los parámetros implícitos son simplemente una generalización de ese patrón, que permite pasar cualquier tipo de parámetros implícitos, en lugar de simplemente Function1 . El uso real para ellos siguió, y el azúcar sintáctico para esos usos vino más tarde.

Uno de ellos es Context Bounds , utilizado para implementar el patrón de clase de tipo (patrón porque no es una función incorporada, solo una forma de utilizar el lenguaje que proporciona una funcionalidad similar a la clase de tipo de Haskell). Un límite de contexto se usa para proporcionar un adaptador que implementa la funcionalidad que es inherente a una clase, pero no declarada por ella. Ofrece los beneficios de la herencia y las interfaces sin sus inconvenientes. Por ejemplo:

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

Probablemente ya lo haya usado; hay un caso de uso común que las personas generalmente no notan. Es esto:

new Array[Int](size)

Utiliza un límite de contexto de una clase manifiesta para habilitar dicha inicialización de matriz. Podemos ver eso con este ejemplo:

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

Puedes escribirlo así:

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

En la biblioteca estándar, los límites de contexto más utilizados son:

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

Los últimos tres se utilizan principalmente con colecciones, con métodos como max , sum y map . Una biblioteca que hace un uso extenso de los límites de contexto es Scalaz.

Otro uso común es disminuir la placa de la caldera en operaciones que deben compartir un parámetro común. Por ejemplo, transacciones:

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

Que luego se simplifica así:

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

Este patrón se usa con la memoria transaccional, y creo (pero no estoy seguro) que la biblioteca de E / S Scala también lo usa.

El tercer uso común que se me ocurre es hacer pruebas sobre los tipos que se pasan, lo que hace posible detectar en tiempo de compilación cosas que, de lo contrario, darían como resultado excepciones de tiempo de ejecución. Por ejemplo, vea esta definición en Option :

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

Eso hace esto posible:

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! ^

Una biblioteca que hace un uso extensivo de esa característica es Shapeless.

No creo que el ejemplo de la biblioteca Akka se ajuste a ninguna de estas cuatro categorías, pero ese es el objetivo de las características genéricas: las personas pueden usarlo de muchas maneras, en lugar de las formas prescritas por el diseñador del lenguaje.

Si le gusta que le receten (como, digamos, Python), entonces Scala no es para usted.

Hasta ahora, los parámetros implícitos en Scala no se ven bien para mí, está demasiado cerca de las variables globales, sin embargo, dado que Scala parece un lenguaje bastante estricto, empiezo a dudar en mi propia opinión :-).

Pregunta: ¿ podría mostrar un buen ejemplo de la vida real (o cercano) cuando los parámetros implícitos realmente funcionan? IOW: algo más serio que showPrompt , que justificaría el diseño del lenguaje.

O al contrario: ¿podría mostrar un diseño de lenguaje confiable (puede ser imaginario) que haría implícito innecesario? Creo que incluso ningún mecanismo es mejor que las implícitas porque el código es más claro y no hay forma de adivinar.

Tenga en cuenta que estoy preguntando sobre los parámetros, no sobre las funciones implícitas (conversiones).

Actualizaciones

Variables globales

Gracias por todas las excelentes respuestas. Quizás clarifique mi objeción a las "variables globales". Considera tal función:

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

tu lo llamas

max(5,6);

podrías (!) hacerlo así:

max(x:5,y:6);

pero en mi opinión, lo implicits funciona así:

x = 5; y = 6; max()

no es muy diferente de tal construcción (similar a PHP)

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

La respuesta de Derek

Este es un gran ejemplo; sin embargo, si puede pensar en un uso flexible del envío de mensajes que no sean implicit , publique un contraejemplo. Tengo mucha curiosidad acerca de la pureza en el diseño del lenguaje ;-).


Es fácil, solo recuerda:

  • para declarar que la variable se pasa como implícita también
  • declarar todos los params implícitos después de los params no implícitos en un ()

p.ej

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


Estoy comentando sobre esta publicación un poco tarde, pero he empezado a aprender scala últimamente. Daniel y otros han dado buenos antecedentes sobre la palabra clave implícita. Me daría dos centavos sobre la variable implícita desde la perspectiva del uso práctico.

Scala es el más adecuado si se usa para escribir códigos Apache Spark. En Spark, tenemos el contexto de chispa y muy probablemente la clase de configuración que puede obtener las claves / valores de configuración de un archivo de configuración.

Ahora, si tengo una clase abstracta y si declaro un objeto de configuración y contexto de chispa como sigue:

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

Como podemos ver el código anterior, tengo dos objetos implícitos en mi clase abstracta, y he pasado esas dos variables implícitas como parámetros implícitos de función / método / definición. Creo que este es el mejor caso de uso que podemos representar en términos de uso de variables implícitas.


Los parámetros implícitos son muy utilizados en la API de recopilación. Muchas funciones obtienen un CanBuildFrom implícito, lo que garantiza que obtenga la ''mejor'' implementación de la colección de resultados.

Sin implicitos, cualquiera pasaría tal cosa todo el tiempo, lo que haría el uso normal engorroso. O usa colecciones menos especializadas que serían molestas porque significaría que pierdes rendimiento / potencia.


Otro buen uso general de los parámetros implícitos es hacer que el tipo de devolución de un método dependa del tipo de algunos de los parámetros que se le pasan. Un buen ejemplo, mencionado por Jens, es el marco de colecciones, y métodos como el map , cuya firma completa suele ser:

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

Tenga en cuenta que el tipo de retorno está determinado por el mejor ajuste de CanBuildFrom que el compilador puede encontrar.

Para otro ejemplo de esto, mira esa respuesta . Allí, el tipo de retorno del método Arithmetic.apply se determina según un determinado tipo de parámetro implícito ( BiConverter ).


Por supuesto. Akka tiene un gran ejemplo de esto con respecto a sus Actores. Cuando estás dentro del método de receive un actor, es posible que desees enviar un mensaje a otro actor. Cuando hagas esto, Akka empaquetará (de forma predeterminada) el actor actual como el sender del mensaje, así:

trait ScalaActorRef { this: ActorRef => ... def !(message: Any)(implicit sender: ActorRef = null): Unit ... }

El sender está implícito. En Actor hay una definición que se ve así:

trait Actor { ... implicit val self = context.self ... }

Esto crea el valor implícito dentro del alcance de su propio código, y le permite hacer cosas sencillas como esta:

someOtherActor ! SomeMessage

Ahora, puedes hacer esto también, si quieres:

someOtherActor.!(SomeMessage)(self)

o

someOtherActor.!(SomeMessage)(null)

o

someOtherActor.!(SomeMessage)(anotherActorAltogether)

Pero normalmente no lo haces. Simplemente mantienes el uso natural que es posible gracias a la definición de valor implícito en el rasgo Actor. Hay alrededor de un millón de otros ejemplos. Las clases de colección son enormes. Intenta deambularte por una biblioteca Scala no trivial y encontrarás una camioneta.


Según mi experiencia, no hay un buen ejemplo para el uso de parámetros implícitos o la conversión de implícitos.

El pequeño beneficio de utilizar implícitos (que no necesitan escribir explícitamente un parámetro o un tipo) es redundante en comparación con los problemas que crean.

Soy desarrollador durante 15 años y he estado trabajando con scala durante los últimos 1.5 años.

He visto muchas veces que los errores causados ​​por el desarrollador no son conscientes del hecho de que se usan implicits, y que una función específica realmente devuelve un tipo diferente al especificado. Debido a la conversión implícita.

También escuché declaraciones que dicen que si no te gustan las implicidades, no las uses. Esto no es práctico en el mundo real ya que muchas veces se usan bibliotecas externas, y muchas de ellas usan implícitos, por lo que su código usa implícitos, y es posible que no lo sepa. Puede escribir un código que tenga:

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

o:

import org.some.common.library._

Ambos códigos se compilarán y ejecutarán. Pero no siempre producirán los mismos resultados ya que la segunda versión de las importaciones implica una conversión que hará que el código se comporte de manera diferente.

El ''error'' que es causado por esto puede ocurrir mucho tiempo después de que se escribió el código, en caso de que algunos valores que se ven afectados por esta conversión no se hayan utilizado originalmente.

Una vez que encuentras el error, no es una tarea fácil encontrar la causa. Tienes que hacer una investigación profunda.

A pesar de que te sientes como un experto en scala una vez que has encontrado el error, y lo solucionaste al cambiar una declaración de importación, en realidad desperdiciaste mucho tiempo valioso.

Razones adicionales por las que generalmente estoy en contra de las implicaciones son:

  • Hacen que el código sea difícil de entender (hay menos código, pero no sabes lo que está haciendo)
  • Tiempo de compilación El código scala compila mucho más lento cuando se usan implicits.
  • En la práctica, cambia el lenguaje de tipeado estáticamente a tipeado dinámicamente. Es cierto que una vez que sigues las pautas de codificación muy estrictas puedes evitar tales situaciones, pero en el mundo real, no siempre es así. Incluso si utiliza el IDE ''eliminar importaciones no utilizadas'', puede hacer que su código se compile y se ejecute aún, pero no es lo mismo que antes de eliminar las importaciones ''no utilizadas''.

No hay opción para compilar scala sin implicits (si es correcto, corrígeme), y si hubiera una opción, ninguna de las bibliotecas comunes de Scala de comunidad habría compilado.

Por todas las razones anteriores, creo que las implícitas son una de las peores prácticas que utiliza el lenguaje scala.

Scala tiene muchas características excelentes, y muchas no tan buenas.

Al elegir un idioma para un nuevo proyecto, las implícitas son una de las razones contra scala, no a favor de ella. En mi opinión.


Un ejemplo serían las operaciones de comparación en Traversable[A] . Ej. max u sort :

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

Estos solo se pueden definir con sensatez cuando hay una operación < en A Entonces, sin implicitos, tendríamos que proporcionar el contexto Ordering[B] cada vez que nos gustaría usar esta función. (O abandone la verificación estática de tipo dentro de max y arriesgue un error de lanzamiento en tiempo de ejecución).

Sin embargo, si una clase de tipo de comparación implícita está dentro del alcance, por ejemplo, un Ordering[Int] , podemos simplemente usarlo de inmediato o simplemente cambiar el método de comparación suministrando algún otro valor para el parámetro implícito.

Por supuesto, las implícitas se pueden sombrear y, por lo tanto, puede haber situaciones en las que lo implícito real que está en el alcance no sea lo suficientemente claro. Para usos simples de max u sort , podría ser suficiente tener un trait ordenamiento fijo en Int y usar alguna sintaxis para verificar si este rasgo está disponible. Pero esto significaría que no podría haber rasgos complementarios y que cada fragmento de código tendría que usar los rasgos que se definieron originalmente.

Adición:
Respuesta a la comparación de variables globales .

Creo que tienes razón en un código cortado como

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.

puede oler a variables globales podridas y malvadas. El punto crucial, sin embargo, es que puede haber solo una variable implícita por tipo en el alcance. Su ejemplo con dos Int s no va a funcionar.

Además, esto significa que las variables prácticamente implícitas se emplean solo cuando existe una instancia primaria no necesariamente única pero distinta para un tipo. La self de un actor es un buen ejemplo para tal cosa. El ejemplo de clase de tipo es otro ejemplo. Puede haber docenas de comparaciones algebraicas para cualquier tipo, pero hay una que es especial. (En otro nivel, el número de línea real en el código mismo también podría constituir una buena variable implícita, siempre que use un tipo muy distintivo).

Normalmente no usas s implicit para tipos de uso diario. Y con tipos especializados (como Ordering[Int] ) no hay demasiado riesgo para sombrearlos.