¿Para qué sirven las clases de tipos en Scala?

Como entiendo de este blog, las “clases de tipo” en Scala son solo un “patrón” implementado con rasgos y adaptadores implícitos.

Como dice el blog, si tengo el rasgo A y un adaptador B -> A entonces puedo invocar una función, que requiere un argumento de tipo A , con un argumento de tipo B sin invocar explícitamente este adaptador.

Lo encontré agradable pero no particularmente útil. ¿Podría dar un caso de uso / ejemplo, que muestra para qué sirve esta característica?

Un caso de uso, según lo solicitado …

Imagine que tiene una lista de cosas, podría ser enteros, números flotantes, matrices, cadenas, formas de onda, etc. Dada esta lista, desea agregar los contenidos.

Una forma de hacerlo sería tener algún rasgo Addable que debe ser heredado por cada tipo individual que se puede agregar, o una conversión implícita a Addable si se trata de objetos de una biblioteca de un tercero al que no se pueden adaptar interfaces para .

Este enfoque se vuelve rápidamente abrumador cuando también desea comenzar a agregar otras operaciones de ese tipo que se pueden hacer a una lista de objetos. Tampoco funciona bien si necesita alternativas (por ejemplo, si la adición de dos formas de onda las concatenan o las superponen). La solución es el polymorphism ad-hoc, donde puede elegir y elegir el comportamiento para adaptarlo a los tipos existentes.

Para el problema original, entonces, podría implementar una clase de tipo Addable :

 trait Addable[T] { def zero: T def append(a: T, b: T): T } //yup, it's our friend the monoid, with a different name! 

A continuación, puede crear instancias de subclases implícitas de este, correspondientes a cada tipo que desea que se pueda agregar:

 implicit object IntIsAddable extends Addable[Int] { def zero = 0 def append(a: Int, b: Int) = a + b } implicit object StringIsAddable extends Addable[String] { def zero = "" def append(a: String, b: String) = a + b } //etc... 

El método para sumr una lista se vuelve trivial para escribir …

 def sum[T](xs: List[T])(implicit addable: Addable[T]) = xs.FoldLeft(addable.zero)(addable.append) //or the same thing, using context bounds: def sum[T : Addable](xs: List[T]) = { val addable = implicitly[Addable[T]] xs.FoldLeft(addable.zero)(addable.append) } 

La belleza de este enfoque es que puede proporcionar una definición alternativa de alguna clase de letra, ya sea controlando lo implícito que desea en el scope a través de importaciones, o proporcionando explícitamente el argumento que de otra manera sería implícito. Por lo tanto, es posible proporcionar diferentes formas de agregar formas de onda, o especificar la aritmética de módulo para la sum entera. También es bastante sencillo agregar un tipo de biblioteca de terceros a su clase de letra.

Por cierto, este es exactamente el enfoque adoptado por la API de 2,8 colecciones. Aunque el método de sum se define en TraversableLike lugar de en List , y la clase de tipo es Numeric (también contiene algunas operaciones más que solo zero y se append )

Vuelve a leer el primer comentario allí:

Una distinción crucial entre clases de tipos e interfaces es que para que la clase A sea un “miembro” de una interfaz debe declararlo en el sitio de su propia definición. Por el contrario, cualquier tipo se puede agregar a una clase de tipo en cualquier momento, siempre que pueda proporcionar las definiciones requeridas, por lo que los miembros de una clase de tipo en un momento dado dependen del scope actual. Por lo tanto, no nos importa si el creador de A anticipó la clase de tipo a la que queremos que pertenezca; si no, podemos simplemente crear nuestra propia definición que demuestre que realmente pertenece, y luego usarla en consecuencia. Por lo tanto, esto no solo proporciona una mejor solución que los adaptadores, en cierto sentido, evita el problema que los adaptadores debían abordar.

Creo que esta es la ventaja más importante de las clases de tipos.

Además, manejan adecuadamente los casos donde las operaciones no tienen el argumento del tipo que estamos enviando, o tienen más de uno. Por ejemplo, considere esta clase de tipo:

 case class Default[T](val default: T) object Default { implicit def IntDefault: Default[Int] = Default(0) implicit def OptionDefault[T]: Default[Option[T]] = Default(None) ... } 

Pienso en las clases de tipos como la capacidad de agregar tipos de metadatos seguros a una clase.

Así que primero defines una clase para modelar el dominio del problema y luego piensas en los metadatos que se agregarán. Cosas como Equals, Hashable, Viewable, etc. Esto crea una separación entre el dominio del problema y las mecánicas para usar la clase y abre las subclases porque la clase es más ágil.

Excepto por eso, puede agregar tipos de clases en cualquier parte del scope, no solo donde se define la clase y puede cambiar las implementaciones. Por ejemplo, si calculo un código hash para una clase Point usando Point # hashCode, entonces estoy limitado a esa implementación específica que puede no crear una buena distribución de valores para el conjunto específico de Points que tengo. Pero si uso Hashable [Point], entonces puedo proporcionar mi propia implementación.

[Actualizado con el ejemplo] Como ejemplo, aquí hay un caso de uso que tuve la semana pasada. En nuestro producto hay varios casos de Mapas que contienen contenedores como valores. Ej., Map[Int, List[String]] o Map[String, Set[Int]] . Agregar a estas colecciones puede ser detallado:

 map += key -> (value :: map.getOrElse(key, List())) 

Así que quería tener una función que envolviera esto para poder escribir

 map +++= key -> value 

El problema principal es que las colecciones no tienen todos los mismos métodos para agregar elementos. Algunos tienen ‘+’ mientras que otros ‘: +’. También quería mantener la eficiencia de agregar elementos a una lista, por lo que no quería usar fold / map, que crea colecciones nuevas.

La solución es usar clases de tipos:

  trait Addable[C, CC] { def add(c: C, cc: CC) : CC def empty: CC } object Addable { implicit def listAddable[A] = new Addable[A, List[A]] { def empty = Nil def add(c: A, cc: List[A]) = c :: cc } implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] { def empty = cbf().result def add(c: A, cc: Add) = (cbf(cc) += c).result } } 

Aquí Addable una clase de tipo Addable que puede agregar un elemento C a una colección CC. Tengo 2 implementaciones predeterminadas: Para listas que usan :: y para otras colecciones, usando el marco de trabajo del constructor.

Luego, usar esta clase de tipo es:

 class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) { def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = { val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) )) (map + pair).asInstanceOf[That] } def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf) } implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col 

El bit especial es usar adder.add para agregar los elementos y adder.empty para crear colecciones nuevas para claves nuevas.

Para comparar, sin clases de tipo habría tenido 3 opciones: 1. escribir un método por tipo de colección. Por ejemplo, addElementToSubList y addElementToSet etc. Esto crea una gran repetición en la implementación y contamina el espacio de nombres 2. para usar la reflexión para determinar si la sub colección es una Lista / Conjunto. Esto es complicado ya que el mapa está vacío para empezar (por supuesto, Scala también ayuda aquí con Manifests) 3. Tener la clase de tipo de hombre pobre requiriendo que el usuario suministre el sumdor. Así que algo como addToMap(map, key, value, adder) , que es bastante feo

Otra forma en la que encuentro útil esta publicación de blog es donde describe las clases de tipos: las mónadas no son metáforas

Busque en el artículo la clase de tipo. Debería ser el primer partido. En este artículo, el autor proporciona un ejemplo de una clase de tipo Monad.

Una forma de ver las clases de tipos es que permiten la extensión retroactiva o el polymorphism retroactivo . Hay un par de excelentes publicaciones de Casual Miracles y Daniel Westheide que muestran ejemplos del uso de Type Classes en Scala para lograr esto.

Aquí hay una publicación en mi blog que explora varios métodos en scala de supertyping retroactivo , una especie de extensión retroactiva, que incluye un ejemplo de clase de tipo.

El hilo del foro ” ¿Qué hace que las clases de tipo sean mejores que los rasgos? ” Hace algunos puntos interesantes:

  • Las clases de tipos pueden representar muy fácilmente nociones que son bastante difíciles de representar en presencia de subtipado, como igualdad y ordenamiento .
    Ejercicio: cree una pequeña jerarquía de clase / rasgo e intente implementar .equals en cada clase / rasgo de tal manera que la operación sobre instancias arbitrarias de la jerarquía sea apropiadamente reflexiva, simétrica y transitiva.
  • Las clases de tipo le permiten proporcionar evidencia de que un tipo fuera de su “control” se ajusta a algún comportamiento.
    El tipo de otra persona puede ser miembro de tu clase de escritura.
  • No se puede express que “este método toma / devuelve un valor del mismo tipo que el receptor del método” en términos de subtipado, pero esta restricción (muy útil) es simple al usar clases de tipos. Este es el problema de los tipos de límites f (donde un tipo de límites F se parametriza sobre sus propios subtipos).
  • Todas las operaciones definidas en un rasgo requieren una instancia ; siempre hay un argumento. Por lo tanto, no puede definir, por ejemplo, un fromString(s:String): Foo en el trait Foo de tal forma que pueda invocarlo sin una instancia de Foo .
    En Scala esto se manifiesta como personas que tratan desesperadamente de abstraerse sobre objetos de compañía.
    Pero es sencillo con una clase de tipo, como se ilustra con el elemento cero en este ejemplo monoide .
  • Las clases de tipos se pueden definir inductivamente ; por ejemplo, si tiene un JsonCodec[Woozle] , puede obtener un JsonCodec[List[Woozle]] gratis.
    El ejemplo anterior ilustra esto para “cosas que puedes agregar juntas”.

No conozco ningún otro caso de uso que no sea el polymorphism Ad-hoc que se explica aquí de la mejor manera posible.

Ambos implicits y typeclasses se usan para la conversión de tipos . El principal caso de uso para ambos es proporcionar polymorphism ad-hoc (es decir) en clases que no se pueden modificar, pero que esperan un tipo de polymorphism de herencia. En el caso de implicits, puede usar tanto una definición implícita como una clase implícita (que es su clase contenedora pero oculta para el cliente). Las clases de tipos son más potentes, ya que pueden agregar funcionalidad a una cadena de herencia ya existente (por ejemplo: ordenar [T] en la función de clasificación de Scala). Para obtener más detalles, puede ver https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

En clases de tipo scala

  • Permite el polymorphism ad-hoc
  • Estáticamente tipeado (es decir, seguro para tipos)
  • Prestado de Haskell
  • Resuelve el problema de expresión

El comportamiento se puede extender, en tiempo de comstackción, después del hecho, sin cambiar / recomstackr el código existente

Scala Implicits

La última lista de parámetros de un método se puede marcar implícita

  • Los parámetros implícitos son completados por el comstackdor

  • En efecto, necesita evidencia del comstackdor

  • … como la existencia de una clase de tipo en el scope

  • También puede especificar parámetros explícitamente, si es necesario

Debajo de la extensión de ejemplo en la clase String con la implementación de la clase de tipo se amplía la clase con un nuevo método aunque la cadena sea final 🙂

 /** * Created by nihat.hosgur on 2/19/17. */ case class PrintTwiceString(val original: String) { def printTwice = original + original } object TypeClassString extends App { implicit def stringToString(s: String) = PrintTwiceString(s) val name: String = "Nihat" name.printTwice } 

Esta es una diferencia importante (necesaria para la progtwigción funcional):

enter image description here

considere inc:Num a=> a -> a :

a recibida es la misma que se devuelve, esto no se puede hacer con la subtipificación

Me gusta utilizar las clases de tipo como una forma idiomática Scala ligera de Dependency Injection que todavía funciona con dependencias circulares pero que no agrega mucha complejidad al código. Recientemente, reescribí un proyecto de Scala para que no usara Cake Pattern y escriba clases para DI y logre una reducción del 59% en el tamaño del código.