Las entradas que he publicado hasta la fecha, en su su mayoría, son descripciones y ejemplos de componentes de librerías como Scalaz o Circe. Todas las librerías aplican, en función del problema a resolver, un patrón común el cual es el Patrón Type Class. De la misma manera que en programación orientada a objetos está la clase y la herencia, en la programación funcional, se presenta el patrón Type Class que nos permite el polimorfismo en función del tipo de elementos a tratar.
El patrón Type Class apareció por primera vez con el lenguaje Haskell, lenguaje puramente funcional, para implementar operadores sobrecargados de aritmética e igualdad. En nuestro caso, el patrón type class lo utilizaremos para definir API’s.
La estructura del patrón type class está formado por cuatro elementos básicos los cuales son los siguientes:
- Definición del trait con la definición del API.
- Definición del trait con las instancias de los elementos que implementa el API en función del tipo.
- Definición del trait con la sintaxis.
- Definición del objeto que hereda de las instancias y se comportan como el resto de elementos trait.
Este patrón es utilizado en las librerías genéricas de Scala como Scalaz y Cats; librerías que complementan al propio lenguaje y solucionan determinados problemas de la programación funcional. Cada librería, organiza el patrón y estructura sus componentes de forma diferente; pero, en líneas generales, la estructura es la del patrón.
Para realizar la demostración, realizaré la implementación del patrón type class para diferentes funcionalidades.
API Impresión (Printable)
El API de impresión definirá la funcionalidad para realizar la conversión de tipos enteros, string y una entidad a tipo String para poder mostrar por consola. Evidentemente, el tipo String no tiene mucho sentido convertirlo porque ya es tipo String pero, realizaré la funcionalidad necesaria para que sea ilustrativa al lector.
El código del type class de impresión se define en el siguiente snippet del API Printable2 de la siguiente forma:
package es.ams.cap1introduccion case class Cat(name:String, age:Int, color:String) trait Printable2[A] { def format(a: => A):String } object Printable2 extends PrintableInstances2 with PrintableSyntax2 trait PrintableInstances2{ def apply[A](implicit P:Printable[A]) = P implicit val printable2String = new Printable2[String]{ def format(a: => String): String = a } implicit val printable2Int = new Printable2[Int]{ def format(a: => Int): String = a.toString } implicit val printable2Cat = new Printable2[Cat]{ def format(a: => Cat): String = a.name + " tiene " + a.age + " y es de color " + a.color } } trait PrintableSyntax2{ object syntax{ def format[A](elem: => A)(implicit P:Printable2[A]): String = P.format(elem) def printer[A](elem: => A)(implicit P:Printable2[A]): Unit = println(s"=>${P.format(elem)}") implicit class PrintableSyntax2Ops[A](elem: => A)(implicit P:Printable2[A]){ def formatOps():String = P.format( elem ) def printOps(): Unit = println( s" ===>${P.format(elem)}" ) } } }
El primer elemento del type class es el trait Printable2 para un tipo genérico A. El API define la función format el cual recibe un elemento de tipo A que lo transforma en un String.
El segundo elemento del type class es el object Printable2 que hereda de las instancias definidas en el trait Printable2Instances y se comporta como las funciones definidas en el trait Printable2Syntax.
El tecer elemento del type class es el trait Printable2Instances2 el cual define todos los elementos que implementan el API Printable2 para los tipos especificados. En nuestro caso, se implementan las instancias del API Printable2 para los siguientes tipos: String, con el objeto printable2String; Int, con el objeto printable2Int; y, Cat, con el elemento printable2Cat. Para los tres casos, la funcionalidad es sencilla, simplemente, los parámetros de la función format se pasan a String. Además, las tres implementaciones están definidas de forma implicita con la palabra implicit.
Por otro lado, para este tercer caso, es importante la definición de la función apply la cual realiza la construcción de aquella instancia que se requiere en función del tipo, representado por la letra A.
El cuarto y último elemento en este type class es el trait PrintableSyntax2 el cual define las aquellas funciones genéricas para los tipos definidos en el trait con las instancias. En nuestro caso, defino dos funciones y una clase. Las funciones definen las funciones helper y, la clase, define aquellas funciones para elementos de tipo A. Como puede analizar el lector, los elementos operativos son los objetos implícitos que se definen con los parámetros implicit.
A continuación, muestro unos ejemplos de uso de la utilización del API Printable2:
import Printable2.syntax._ println( "->" + format(69) ) println printer( 89 ) println val gato: Cat = Cat( name = "John", age=18, color="Blanco") println( "-->" + format(gato) ) println printer( Cat( name = "John", age=28, color="Rojo") ) println val gato = Cat( name = "John", age=38, color="Verde") println(s"Gato:${gato formatOps()}" ) println val gato2 = Cat( name = "John", age=48, color="Rosa") gato2.printOps() println
La salida por consola es la siguiente:
->69 =>89 -->John tiene 18 y es de color Blanco =>John tiene 28 y es de color Rojo Gato:John tiene 38 y es de color Verde ===>John tiene 48 y es de color Rosa
Como observamos en los ejemplos, es necesaria la importación de la sintaxis y el compilador, tras la inferencia de tipos, infiere qué instancia implícita es la que tiene que utilizar.
API Visualización (Show)
En muchas ocasiones no es necesaria la creación de cualquier API porque las librerías genéricas Scalaz o Cats nos propocionan esas API. En el presente apartado, realizaré la encapsulación del API Show existente en la librería Cats. Este ejemplo sigue la misma estructura y es meramente ilustrativo.
Para realizar dicho ejemplo es necesario definir en el fichero build.sbt la dependencia con la librería Cats. La dependencia se define de la siguiente forma:
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.0-MF"
Para importar los elementos necesarios en la aplicación, se realiza de la siguiente forma:
import cats._ import cats.implicits._
La definición del API MyShow que encapsula el API Show de Cats es el siguiente:
trait MyShow[A] { def show(elem:A):String } object MyShow extends MyShowInstances with MyShowSyntax trait MyShowInstances{ def apply[A](implicit S:MyShow[A]) = S implicit val myShowInt = new MyShow[Int] { def show(elem:Int): String = { Show.apply[Int].show(elem) } } implicit val myShowString = new MyShow[String] { def show(elem:String): String = { elem.show } } implicit val myShowCat = new MyShow[Cat] { def show(elem:Cat): String = { elem.name.capitalize.show + " tiene " + elem.age.show + " y es de color " + elem.color.show } } } trait MyShowSyntax{ object syntax{ def show[A](elem:A)(implicit S: MyShow[A]) = S.show(elem) implicit class MyShowOps[A](elem:A)(implicit S: MyShow[A]){ def show():String = S.show(elem) def =*=>():String = S.show(elem) } } }
La estructura y elementos del APi son las mismas que en el caso del API Printable2. La diferencia reside en las instancias implícitas del trait MyShowInstances las cuáles utilizan el API Show de Cats.
Los ejemeplos de utilización del API MyShow son los siguientes:
import MyShow.syntax._ println( "[Syntax] show(69) = " + show(69) ) println val gato: Cat = Cat(name="gato", age=18, color="Rosa") println( "[Syntax] show(69) = " + 69 ) println println( "[Syntax] =*=>()= " + gato.=*=>() ) println println( "[Syntax] show()= " + gato.show() ) println
La salida por consola es la siguiente:
[Syntax] show(69) = 69 [Syntax] show(69) = 69 [Syntax] =*=>()= Gato tiene 18 y es de color Rosa [Syntax] show()= Gato tiene 18 y es de color Rosa
Visión funcional
Una función es pura cuando en un programa se puede sustituir una función por el resultado de dicha función y, el funcionamiento del programa, sigue siendo el mismo. Una función no es pura cuando presenta efectos de lado los cuáles son todas aquellas operaciones que suponen a la función que tenga resultados distintos en cada ejecución; como por ejemplo: una operación de entrada-salida, una operación a una base de datos, o bien, una excepción.
En el patrón type class, se diferencian las funciones puras y las funciones que pueden presentar efectos de lado. Las funciones no puras son aquellas que se definen en las instancias del API y, las funciones puras, son las definidas en el trait de la sintaxis y en el API. Así, podemos definir un API entendible por negocio y, la parte de infraestructura, en las instancias; consiguiendo separar los dos ámbitos: el ámbito del mundo de negocio y el ámbito de la infraestructura.
Conclusión
La estructura del patrón Type Class es siempre la misma. Para su correcta entendimiento, es necesario tener claro cómo funcionan los elementos implícitos y, sobre todo, saber diferenciar los elementos funcionales puros y los elementos con efectos de lados; efectos, que suponen que las funciones no sean puras. Este patrón es utilizado por ejemplo para la implementación de Monoides, Funtores o Mónadas.