Clases de tipo en Scala3: una guía para principiantes | Libro mayor

Clases de tipo en Scala3: una guía para principiantes | Libro mayor

Clases de tipo en Scala3: una guía para principiantes | Ledger PlatoBlockchain Inteligencia de datos. Búsqueda vertical. Ai.

Este documento está destinado al desarrollador principiante de Scala3 que ya conoce la prosa de Scala, pero que está desconcertado por todos los `implicits` y rasgos parametrizados en el código.

Este documento explica el por qué, cómo, dónde y cuándo de Clases de tipo (TC).

Después de leer este documento, el desarrollador principiante de Scala3 obtendrá conocimientos sólidos para usar y profundizar en el código fuente de bastante de bibliotecas Scala y comenzar a escribir código Scala idiomático.

Comencemos con el por qué...

El problema de la expresión.

En 1998, Philip Wadler declaró que “la expresión problema es un nuevo nombre para un viejo problema”. Es el problema de la extensibilidad del software. Según lo escrito por el señor Wadler, la solución al problema de expresión debe cumplir con las siguientes reglas:

  • Regla 1: Permitir la implementación de comportamientos existentes (piense en el rasgo de Scala) que se aplicará a nuevas representaciones (piense en una clase de caso)
  • Regla 2:  Permitir la implementación de nuevos comportamientos para ser aplicado a representaciones existentes
  • Regla 3: No debe poner en peligro la tipo de seguridad
  • Regla 4: No debe ser necesario volver a compilar código existente

Resolver este problema será el hilo conductor de este artículo.

Regla 1: implementación del comportamiento existente en una nueva representación

Cualquier lenguaje orientado a objetos tiene una solución integrada para la regla 1 con subtipo de polimorfismo. Puede implementar de forma segura cualquier `trait` definido en una dependencia de un `class` en su propio código, sin recompilar la dependencia. Veámoslo en acción:

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo


object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

En este ejemplo ficticio, biblioteca `Lib1` (línea 5) define un rasgo `Blockchain` (línea 6) con 2 implementaciones (líneas 9 y 12). `Lib1`seguirá siendo el mismo en TODO este documento (aplicación de la regla 4).

`Lib2` (línea 15) implementa el comportamiento existente `Blockchain`en una nueva clase`Polkadot` (regla 1) de forma segura (regla 3), sin volver a compilar `Lib1` (regla 4). 

Regla 2: implementación de nuevos comportamientos que se aplicarán a las representaciones existentes

Imaginemos en `Lib2`queremos un nuevo comportamiento `lastBlock` que se implementará específicamente para cada `Blockchain`.

Lo primero que me viene a la mente es crear un gran cambio basado en el tipo de parámetro.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(blockchain: Blockchain): Block = blockchain match
 case _:Ethereum => todo
 case _:Bitcoin => todo
 case _:Polkadot => todo
 

object Lib3:
 import Lib1.*

 case class Polygon() extends Blockchain:
 override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

¡Esta solución es una reimplementación débil del polimorfismo basado en tipos que ya está integrado en el lenguaje!

`Lib1` se deja intacto (recuerde, se aplicó la regla 4 en todo este documento). 

La solución implementada en `Lib2`es bien hasta que se introduzca otra cadena de bloques en `Lib3`. Infringe la regla de seguridad de tipos (regla 3) porque este código falla en tiempo de ejecución en la línea 37. Y modificando `Lib2` infringiría la regla 4.

Otra solución es usar un `extension`.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(): Block = todo

 extension (eth: Ethereum) def lastBlock(): Block = todo

 extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
 // blockchain.lastBlock()
 ???

`Lib1` se deja intacto (aplicación de la regla 4 en todo el documento). 

`Lib2` define el comportamiento para su tipo (línea 21) y `extensiones para los tipos existentes (líneas 23 y 25).

Líneas 28-30, el nuevo comportamiento se puede utilizar en cada clase. 

Pero no hay manera de llamar polimórfico a este nuevo comportamiento (línea 32). Cualquier intento de hacerlo conduce a errores de compilación (línea 33) o a cambios basados ​​en tipos. 

Esta regla n°2 es complicada. Intentamos implementarlo con nuestra propia definición de polimorfismo y truco de "extensión". Y eso fue extraño.

Falta una pieza llamada polimorfismo ad-hoc: la capacidad de enviar de forma segura una implementación de comportamiento de acuerdo con un tipo, dondequiera que se definan el comportamiento y el tipo. Introducir el Clase de tipo patrón.

El patrón de clase de tipo

La receta del patrón Type Class (TC para abreviar) tiene 3 pasos. 

  1. Definir un nuevo comportamiento
  2. implementar el comportamiento
  3. Usa el comportamiento

En la siguiente sección, implemento el patrón TC de la manera más sencilla. Es detallado, torpe y poco práctico. Pero espere, esas advertencias se solucionarán paso a paso en el documento.

1. Definir un nuevo comportamiento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

`Lib1` queda, una vez más, intacta.

El nuevo comportamiento is el CT materializado por el rasgo. Las funciones definidas en el rasgo son una forma de aplicar algunos aspectos de ese comportamiento.

El parámetro `A` representa el tipo al que queremos aplicar el comportamiento, que son subtipos de `Blockchain` en nuestro caso.

Algunas observaciones:

  • Si es necesario, el tipo parametrizado `A` puede verse restringido aún más por el sistema de tipos Scala. Por ejemplo, podríamos hacer cumplir `A`ser un`Blockchain`. 
  • Además, el TC podría tener muchas más funciones declaradas en él.
  • Finalmente, cada función puede tener muchos más parámetros arbitrarios.

Pero mantengamos las cosas simples en aras de la legibilidad.

2. Implementar el comportamiento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Para cada tipo el nuevo `LastBlock`Se espera un comportamiento, hay una instancia específica de ese comportamiento. 

El 'Ethereum` la línea de implementación 22 se calcula a partir de `eth`instancia pasada como parámetro. 

La implementación de `LastBlock`para`Bitcoin`La línea 25 se implementa con una IO no administrada y no utiliza su parámetro.

Entonces, `Lib2`implementa un nuevo comportamiento`LastBlock`para`Lib1`clases.

3. Utilice el comportamiento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

Línea 30`useLastBlock`usa una instancia de`A` y el `LastBlock`comportamiento definido para esa instancia.

Línea 33`useLastBlock`se llama con una instancia de`Ethereum` y una implementación de `LastBlock` definido en `Lib2`. Tenga en cuenta que es posible pasar cualquier implementación alternativa de `LastBlock[A]` (pensar en inyección de dependencia).

`useLastBlock` es el pegamento entre la representación (la A real) y su comportamiento. Los datos y el comportamiento están separados, que es lo que defiende la programación funcional.

Discusión

Recapitulemos las reglas del problema de expresión:

  • Regla 1: Permitir la implementación de comportamientos existentes  para ser aplicado a nuevas clases
  • Regla 2:  Permitir la implementación de nuevos comportamientos para ser aplicado a clases existentes
  • Regla 3: No debe poner en peligro la tipo de seguridad
  • Regla 4: No debe ser necesario volver a compilar código existente

La regla 1 se puede resolver de forma inmediata con polimorfismo de subtipo.

El patrón TC que acabamos de presentar (ver captura de pantalla anterior) resuelve la regla 2. Es seguro para escribir (regla 3) y nunca tocamos `Lib1` (regla 4). 

Sin embargo, su uso no es práctico por varias razones:

  • Líneas 33-34 tenemos que pasar explícitamente el comportamiento a lo largo de su instancia. Esto supone un gasto extra. Simplemente deberíamos escribir `useLastBlock(Bitcoin())`.
  • Línea 31 la sintaxis es poco común. Preferiríamos escribir un texto conciso y más orientado a objetos  `instance.lastBlock()`declaración.

Resaltemos algunas características de Scala para el uso práctico de TC. 

Experiencia de desarrollador mejorada

Scala tiene un conjunto único de características y azúcares sintácticos que hacen de TC una experiencia verdaderamente agradable para los desarrolladores.

Implicitos

El alcance implícito es un alcance especial resuelto en tiempo de compilación donde solo puede existir una instancia de un tipo determinado. 

Un programa pone una instancia en el alcance implícito con el `given`palabra clave. Alternativamente, un programa puede recuperar una instancia del alcance implícito con la palabra clave `using`.

El alcance implícito se resuelve en tiempo de compilación, existe una forma conocida de cambiarlo dinámicamente en tiempo de ejecución. Si el programa se compila, se resuelve el alcance implícito. En tiempo de ejecución, no es posible que falten instancias implícitas en las que se utilizan. La única confusión posible puede provenir del uso de una instancia implícita incorrecta, pero este problema queda en manos de la criatura entre la silla y el teclado.

Es diferente de un alcance global porque: 

  1. Se resuelve contextualmente. Dos ubicaciones de un programa pueden usar una instancia del mismo tipo determinado en un alcance implícito, pero esas dos instancias pueden ser diferentes.
  2. Detrás de escena, el código pasa la función de argumentos implícitos para funcionar hasta que se alcanza el uso implícito. No utiliza un espacio de memoria global.

¡Volviendo a la clase tipo! Tomemos exactamente el mismo ejemplo.

Scala

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

`Lib1` es el mismo código sin modificar que definimos anteriormente. 

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

Línea 19 un nuevo comportamiento `LastBlock`está definido, exactamente como lo hicimos anteriormente.

Línea 22 y línea 25, `val`se reemplaza por `given`. Ambas implementaciones de `LastBlock`se colocan en el alcance implícito.

Línea 31`useLastBlock`declara el comportamiento `LastBlock` como parámetro implícito. El compilador resuelve la instancia apropiada de `LastBlock`desde un alcance implícito contextualizado desde las ubicaciones de las personas que llaman (líneas 33 y 34). La línea 28 importa todo desde `Lib2`, incluido el alcance implícito. Entonces, el compilador pasa las instancias definidas en las líneas 22 y 25 como último parámetro de `useLastBlock`. 

Como usuario de una biblioteca, utilizar una clase de tipos es más fácil que antes. En las líneas 34 y 35, un desarrollador sólo tiene que asegurarse de que se inyecte una instancia del comportamiento en el alcance implícito (y esto puede ser un simple `import`). Si un implícito no es `given`donde está el código`using` eso, le dice el compilador.

Las implícitas de Scala facilitan la tarea de pasar instancias de clases junto con instancias de sus comportamientos.

Azúcares implícitos

¡Las líneas 22 y 25 del código anterior se pueden mejorar aún más! Repitamos las implementaciones de TC.

Scala

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Líneas 22 y 25, si el nombre de la instancia no se utiliza, se puede omitir.

Scala


 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Líneas 22 y 25, la repetición del tipo se puede reemplazar con `with`palabra clave.

Scala

given LastBlock[Ethereum] = _.lastBlock

 given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

Debido a que utilizamos un rasgo degenerado con una sola función, el IDE puede sugerir simplificar el código con una expresión SAM. Aunque es correcto, no creo que sea un uso adecuado de SAM, a menos que estés codificando golf casualmente.

Scala ofrece azúcares sintácticos para optimizar la sintaxis, eliminando nombres, declaraciones y redundancia de tipos innecesarios.

Extensión

Usado sabiamente, el `extension`El mecanismo puede simplificar la sintaxis para usar una clase de tipo.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)
 def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

Líneas 28-29 un método de extensión genérico `lastBlock` está definido para cualquier `A` con un `LastBlock`Parámetro TC en alcance implícito.

Líneas 33-34, la extensión aprovecha una sintaxis orientada a objetos para usar TC.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)(using tc: LastBlock[A])
 def lastBlock = tc.lastBlock(instance)
 def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

Línea 28, el parámetro TC también se puede definir para toda la extensión para evitar repeticiones. Línea 30 reutilizamos el TC en la extensión para definir `penultimateBlock` (aunque podría implementarse en `LastBlock` rasgo directamente)

La magia ocurre cuando se utiliza el TC. La expresión se siente mucho más natural, dando la ilusión de que el comportamiento "lastBlock`se combina con la instancia.

Tipo genérico con TC
Scala

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

Línea 34, la función utiliza un TC implícito. Tenga en cuenta que no es necesario nombrar el TC si ese nombre no es necesario.

El patrón TC se usa tan ampliamente que existe una sintaxis de tipo genérica para expresar "un tipo con un comportamiento implícito". Línea 36 la sintaxis es una alternativa más concisa a la anterior (línea 34). Evita declarar específicamente el parámetro TC implícito sin nombre.

Con esto concluye la sección de experiencia del desarrollador. Hemos visto cómo las extensiones, los implícitos y algo de azúcar sintáctico pueden proporcionar una sintaxis menos desordenada cuando se usa y define el TC.

Derivación automática

Muchas bibliotecas de Scala utilizan TC, lo que deja que el programador las implemente en su código base.

Por ejemplo, Circe (una biblioteca de deserialización json) usa TC `Encoder[T]`y`Decoder[T]` para que los programadores lo implementen en su código base. Una vez implementado, se puede utilizar todo el alcance de la biblioteca. 

Esas implementaciones de CT son más que a menudo mapeadores orientados a datos. No necesitan ninguna lógica de negocios, son aburridos de escribir y es una carga mantenerlos sincronizados con las clases de casos.

En tal situación, esas bibliotecas ofrecen lo que se llama y automática derivación o semiautomático derivación. Véase, por ejemplo, Circe. y automática y semiautomático derivación. Con la derivación semiautomática, el programador puede declarar una instancia de una clase de tipo con alguna sintaxis menor, mientras que la derivación automática no requiere ninguna modificación del código excepto una importación.

Debajo del capó, en tiempo de compilación, las macros genéricas hacen introspección tipos como estructura de datos pura y generar un TC[T] para los usuarios de la biblioteca. 

Derivar genéricamente un TC es muy común, por lo que Scala introdujo una caja de herramientas completa para ese propósito. Este método no siempre se anuncia en la documentación de la biblioteca, aunque es la forma en que Scala 3 utiliza la derivación.

Scala

object GenericLib:

 trait Named[A]:
 def blockchainName(instance: A): String

 object Named:
 import scala.deriving.*

 inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
 val nameOfType: String = inline m match
 case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
 case _ => compiletime.error("Not a product")
 new Named[A]:
 override def blockchainName(instance: A):String = nameOfType.toLowerCase

 extension[A] (instance: A)(using tc: Named[A])
 def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

Línea 18 un nuevo TC `Named`se introduce. Esta CT no está relacionada con el negocio blockchain estrictamente hablando. Su propósito es nombrar la cadena de bloques según el nombre de la clase de caso.

Primero concéntrese en las definiciones de las líneas 36-38. Hay 2 sintaxis para derivar un TC:

  1. Línea 36, ​​la instancia de TC se puede definir directamente en la clase de caso con el `derives`palabra clave. Debajo del capó, el compilador genera un ` determinadoNamed` instancia en `Polkadot` objeto complementario.
  2. Línea 37 y 38, las instancias de clases de tipo se dan en clases preexistentes con `TC.derived

Línea 31 se define una extensión genérica (ver apartados anteriores) y `blockchainName`se usa naturalmente.  

El 'derives`la palabra clave espera un método con la forma`inline def derived[T](using Mirror.Of[T]): TC[T] = ???` que se define en la línea 24. No explicaré en profundidad qué hace el código. A grandes rasgos:

  • `inline def` define una macro
  • `Mirror`es parte de la caja de herramientas para realizar una introspección de tipos. Hay diferentes tipos de espejos y en la línea 26 el código se centra en `Product` espejos (una clase de caso es un producto). Línea 27, si los programadores intentan derivar algo que no sea un `Product`, el código no se compilará.
  • el 'Mirror` contiene otros tipos. Uno de ellos, `MirrorLabel`, es una cadena que contiene el nombre del tipo. Este valor se utiliza en la implementación, línea 29, de `Named`TC.

Los autores de TC pueden utilizar metaprogramación para proporcionar funciones que generen genéricamente instancias de TC dado un tipo. Los programadores pueden utilizar la API de biblioteca dedicada o las herramientas derivadas de Scala para crear instancias para su código.

Ya sea que necesite código genérico o específico para implementar un TC, existe una solución para cada situación. 

Resumen de todos los beneficios

  • Resuelve el problema de expresión.
    • Los nuevos tipos pueden implementar comportamientos existentes a través de la herencia de rasgos tradicionales.
    • Se pueden implementar nuevos comportamientos en tipos existentes.
  • Separación de preocupaciones
    • El código no está alterado y se puede eliminar fácilmente. Un TC separa datos y comportamiento, que es un lema de programación funcional.
  • Es seguro
    • Es seguro porque no depende de la introspección. Evita la coincidencia de grandes patrones que involucran tipos. Si se encuentra escribiendo dicho código, puede detectar un caso en el que el patrón TC se adapte perfectamente.
    • ¡El mecanismo implícito es seguro para la compilación! Si falta una instancia en el momento de la compilación, el código no se compilará. No hay sorpresa en tiempo de ejecución.
  • Aporta polimorfismo ad-hoc.
    • El polimorfismo ad hoc suele faltar en la programación tradicional orientada a objetos.
    • Con el polimorfismo ad-hoc, los desarrolladores pueden implementar el mismo comportamiento para varios tipos no relacionados sin utilizar el subtipo tradicional (que acopla el código).
  • Inyección de dependencia simplificada
    • Una instancia de TC se puede cambiar con respecto al principio de sustitución de Liskov. 
    • Cuando un componente depende de un TC, se puede inyectar fácilmente un TC simulado con fines de prueba. 

Indicaciones de contador

Cada martillo está diseñado para una variedad de problemas.

Las clases de tipo son para problemas de comportamiento y no deben usarse para herencia de datos. Utilice la composición para ese propósito.

La subtipificación habitual es más sencilla. Si posee el código base y no busca la extensibilidad, las clases de tipos pueden ser excesivas.

Por ejemplo, en el núcleo de Scala, hay un `Numeric`clase de tipo:

Scala

trait Numeric[T] extends Ordering[T] {
 def plus(x: T, y: T): T
 def minus(x: T, y: T): T
 def times(x: T, y: T): T

Realmente tiene sentido usar una clase de tipo de este tipo porque no solo permite la reutilización de algoritmos algebraicos en tipos que están integrados en Scala (Int, BigInt,…), sino también en tipos definidos por el usuario (un `ComplexNumber`por ejemplo).

Por otro lado, la implementación de las colecciones de Scala utiliza principalmente subtipos en lugar de clases de tipos. Este diseño tiene sentido por varias razones:

  • Se supone que la API de colección es completa y estable. Expone el comportamiento común a través de rasgos heredados por las implementaciones. Ser altamente extensible no es un objetivo particular aquí.
  • Debe ser sencillo de utilizar. TC agrega una sobrecarga mental al programador del usuario final.
  • La CT también podría incurrir en pequeños gastos generales de rendimiento. Esto puede ser crítico para una API de colección.
  • Sin embargo, la API de colección aún es extensible a través de nuevas TC definidas por bibliotecas de terceros.

Conclusión

Hemos visto que TC es un patrón simple que resuelve un gran problema. Gracias a la rica sintaxis de Scala, el patrón TC se puede implementar y utilizar de muchas maneras. El patrón TC está en línea con el paradigma de programación funcional y es una herramienta fabulosa para una arquitectura limpia. No existe una fórmula milagrosa y se debe aplicar el patrón TC cuando encaje.

Espero que hayas adquirido conocimientos leyendo este documento. 

El código está disponible en https://github.com/jprudent/type-class-article. Comuníquese conmigo si tiene algún tipo de pregunta o comentario. Puede utilizar problemas o comentarios de código en el repositorio si lo desea.


Jerónimo PRUDENTE

Ingeniero de Software

Sello de tiempo:

Mas de Libro mayor