Conversión implícita frente a clase de tipo

93

En Scala, podemos usar al menos dos métodos para actualizar tipos nuevos o existentes. Supongamos que queremos expresar que algo se puede cuantificar usando 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 importarlos, podemos llamar al método quantifyen 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 a quantify.

Clases de tipo

Una alternativa es definir un "testigo" Quantified[A]que declare que algún tipo Apuede cuantificarse.

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

Luego proporcionamos instancias de esta clase de tipo para Stringy en Listalgú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 usando la sintaxis ligada al 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 tipos permiten la agradable sintaxis vinculada al contexto
  • con clases de tipo, no creo un nuevo objeto contenedor en cada uso
  • la sintaxis ligada al contexto ya no funciona si la clase de tipo tiene varios parámetros de tipo; imagina que quiero cuantificar cosas no solo con números enteros sino con valores de algún tipo general T. Me gustaría crear una clase de tipoQuantified[A,T]

conversión implícita

  • como creo un nuevo objeto, puedo almacenar valores en caché allí o calcular una mejor representación; pero ¿debo 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) casos de uso en los que la diferencia entre ambos conceptos sea importante y explique por qué preferiría uno sobre el otro. También sería bueno explicar la esencia de los dos conceptos y su relación entre sí, incluso sin ejemplo.

ziggystar
fuente
Hay cierta confusión en los puntos de la clase de tipo donde mencionas "vista limitada", aunque las clases de tipo usan límites de contexto.
Daniel C. Sobral
1
+1 excelente pregunta; Estoy muy interesado en una respuesta completa a esto.
Dan Burton
@Daniel Gracias. Siempre me equivoco.
ziggystar
2
Se equivoca en un solo lugar: en su segundo ejemplo conversión implícita se almacena el sizede una lista en un valor y decir que evita el recorrido caro de la lista de llamadas posteriores a cuantificar, sino de toda su llamada a la quantifydel list2quantifiablese desencadena de nuevo restableciendo Quantifiabley recalculando la quantifypropiedad. Lo que estoy diciendo es que en realidad no hay forma de almacenar en caché los resultados con conversiones implícitas.
Nikita Volkov
@NikitaVolkov Tu observación es correcta. Y abordo esto en mi pregunta en el penúltimo párrafo. El almacenamiento en caché funciona cuando el objeto convertido se usa durante más tiempo después de una llamada al método de conversión (y tal vez se transmite en su forma convertida). Mientras que las clases de tipos probablemente se encadenarían a lo largo del objeto no convertido al profundizar.
ziggystar

Respuestas:

42

Si bien no quiero duplicar mi material de Scala In Depth , creo que vale la pena señalar que las clases de tipo / rasgos 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 formas:

  1. Creación / importación de una instancia de clase de tipo implícita en Scope para cortocircuitar la búsqueda implícita
  2. Pasar directamente una clase de tipo

He aquí 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 tipos sean infinitamente más flexibles. Otra cosa es que las clases / rasgos de tipo soportan mejor la búsqueda implícita .

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

Function1[Int, ?]

Que mirará Function1el objeto compañero y el Intobjeto compañero.

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

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

Quantifiable[Int]

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

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

jsuereth
fuente
20

Un criterio que puede entrar en juego es cómo quiere que se "sienta" la nueva función; usando conversiones implícitas, puede hacer que parezca que es solo otro método:

"my string".newFeature

... mientras usa clases de tipos, siempre parecerá que está llamando a una función externa:

newFeature("my string")

Una cosa que puede lograr con clases de tipos y no con 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 tiene 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 un número infinito de sus instancias; sin el implicitmétodo (no una conversión, es cierto), solo podría tener un número finito de tipos con la Defaultpropiedad.

Philippe
fuente
@Phillippe - Estoy muy interesado en la técnica que escribiste ... pero parece que no funciona en Scala 2.11.6. Publiqué una pregunta solicitando una actualización de su respuesta. gracias de antemano si puede ayudar: consulte: stackoverflow.com/questions/31910923/…
Chris Bedford
@ChrisBedford Agregué la definición de defaultpara futuros lectores.
Philippe
13

Puede pensar en la diferencia entre las dos técnicas por analogía con la aplicación de la 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 del primero encapsula una función de tipo A => Int, mientras que una instancia del segundo ya se ha aplicado a un A. Podrías continuar con el patrón ...

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

por lo tanto, podría pensar en una Foo1[B]especie de aplicación parcial de Foo2[A, B]a alguna Ainstancia. Miles Sabin escribió un gran ejemplo de esto como "Dependencias funcionales en Scala" .

Así que realmente mi punto es que, en principio:

  • "proxenetismo" una clase (mediante conversión implícita) es el caso de "orden cero" ...
  • declarar una clase de tipos es el caso de "primer orden" ...
  • clases de tipos de parámetros múltiples con fundeps (o algo como fundeps) es el caso general.
Conflicto
fuente