scala coding-style implicit-conversion

scala - Conversión implícita vs. tipo de clase



coding-style implicit-conversion (3)

En Scala, podemos usar al menos dos métodos para actualizar los tipos existentes o nuevos. Supongamos que queremos expresar que algo se puede cuantificar utilizando un Int . Podemos definir el siguiente rasgo.

Conversión implícita

trait Quantifiable{ def quantify: Int }

Y luego podemos usar conversiones implícitas para cuantificar, por ejemplo, cadenas y listas.

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

Después de importar estos, podemos llamar al método quantify en cadenas y listas. Tenga en cuenta que la lista cuantificable almacena su longitud, por lo que evita el costoso recorrido de la lista en llamadas posteriores para quantify .

Clases de tipo

Una alternativa es definir un "testigo" Quantified[A] que establece que se puede cuantificar algún tipo A

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

A continuación, proporcionamos instancias de esta clase de tipo para String y List algún lugar.

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

Y si luego escribimos un método que necesita cuantificar sus argumentos, escribimos:

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

O utilizando la sintaxis del contexto:

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

¿Pero cuándo usar qué método?

Ahora viene la pregunta. ¿Cómo puedo decidir entre esos dos conceptos?

Lo que he notado hasta ahora.

clases de tipo

  • las clases de tipo permiten la buena sintaxis del contexto
  • con clases de tipo no creo un nuevo objeto contenedor en cada uso
  • la sintaxis de límite de contexto ya no funciona si la clase de tipo tiene múltiples parámetros de tipo; imagino que quiero cuantificar cosas no solo con enteros sino con valores de algún tipo general T Me gustaría crear una clase de tipo Quantified[A,T]

conversión implícita

  • ya que creo un nuevo objeto, puedo almacenar en caché los valores allí o calcular una mejor representación; pero ¿debería evitar esto, ya que podría suceder varias veces y una conversión explícita probablemente se invocaría solo una vez?

Lo que espero de una respuesta

Presente uno (o más) caso (s) de uso donde la diferencia entre ambos conceptos importa y explique por qué preferiría uno sobre el otro. También sería agradable explicar la esencia de los dos conceptos y su relación entre ellos, incluso sin un ejemplo.


Puede pensar en la diferencia entre las dos técnicas por analogía a la aplicación de función, solo con un contenedor con nombre. Por ejemplo:

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

Una instancia de la primera encapsula una función de tipo A => Int , mientras que una instancia de la última ya se ha aplicado a una A Puedes continuar el patrón ...

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

por lo tanto, podrías pensar que Foo1[B] algo así como la aplicación parcial de Foo2[A, B] a alguna instancia A Un gran ejemplo de esto fue escrito por Miles Sabin como "Dependencias funcionales en Scala" .

Entonces, realmente mi punto es que, en principio:

  • "proxenetismo" una clase (a través de la conversión implícita) es el caso "orden cero" ...
  • declarar una clase de tipo es el caso de "primer orden" ...
  • clases de tipos multiparamétricas con fundeps (o algo así como fundeps) es el caso general.

Si bien no quiero duplicar mi material de Scala In Depth , creo que vale la pena señalar que los tipos de clases / características de tipo son infinitamente más flexibles.

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

tiene la capacidad de buscar en su entorno local una clase de tipo predeterminada. Sin embargo, puedo anular el comportamiento predeterminado en cualquier momento de una de estas dos maneras:

  1. Crear / importar una instancia de clase de tipo implícita en Scope para provocar un cortocircuito en la búsqueda implícita
  2. Pasar directamente una clase de tipo

Aquí hay un ejemplo:

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 }

Esto hace que las clases de tipo sean infinitamente más flexibles. Otra cosa es que las clases / características de tipo soportan mejor la búsqueda implícita.

En su primer ejemplo, si usa una vista implícita, el compilador realizará una búsqueda implícita de:

Function1[Int, ?]

Que mirará el objeto complementario de Function1 y el objeto complementario Int .

Observe que Quantifiable no está en ninguna parte en la búsqueda implícita. Esto significa que debe colocar la vista implícita en un objeto de paquete o importarlo al ámbito. Es más trabajo recordar lo que está pasando.

Por otro lado, una clase de tipo es explícita . Usted ve lo que está buscando en la firma del método. También tiene una búsqueda implícita de

Quantifiable[Int]

que se verá en el objeto complementario de Quantifiable y en el objeto complementario de Int . Lo que significa que puede proporcionar valores predeterminados y los nuevos tipos (como una clase MyString ) pueden proporcionar un valor predeterminado en su objeto complementario y se buscará implícitamente.

En general, uso clases de tipo. Son infinitamente más flexibles para el ejemplo inicial. El único lugar donde uso las conversiones implícitas es cuando se utiliza una capa API entre un contenedor Scala y una biblioteca Java, e incluso esto puede ser ''peligroso'' si no tiene cuidado.


Un criterio que puede entrar en juego es cómo quiere que se sienta la nueva característica; Con las conversiones implícitas, puede hacer que parezca que es solo otro método:

"my string".newFeature

... durante el uso de clases de tipo siempre se verá como si estuvieras llamando a una función externa:

newFeature("my string")

Una cosa que puede lograr con las clases de tipo y no con las conversiones implícitas es agregar propiedades a un tipo , en lugar de a una instancia de un tipo. A continuación, puede acceder a estas propiedades incluso cuando no tenga una instancia del tipo disponible. Un ejemplo canónico sería:

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

Este ejemplo también muestra cómo los conceptos están estrechamente relacionados: las clases de tipos no serían tan útiles si no hubiera un mecanismo para producir infinitamente muchas de sus instancias; sin el método implicit (no es una conversión, en verdad), solo podría tener finitamente muchos tipos con la propiedad Default .