Composición de FreeMonad

En la entrada anterior, FreeMonad en Cats, realicé una descripción de cómo definir un DSL utilizando una FreeMonad pero, en ciertas situaciones, necesitamos utilizar un segundo DSL conjuntamente con el primero. En la entrada de hoy, Composición de FreeMonad, realizaré un ejemplo de una aplicación que utiliza conjuntamente dos DSL para la definición de un programa.

Presentación del problema

Continuando con las entidades de dominio de la entrada anterior, supongamos que necesitamos realizar la creación de un esquema de base de datos, realizar unas inserciones, eliminar y consultar; es decir, necesitamos un programa que realice unas operaciones básicas de tipo CRUD; pero además, necesitamos que las operaciones sean trazadas en un log en la salida estándar; pudiendo ser de dos tipos: mensajes informativos, tipo info; y, mensajes de depuración, tipo debug.
Para definir un programa que cumpla con estos requisitos necesitamos lo siguiente: definir un DSL para las operaciones en base de datos, definir un DSL para las operaciones de log y un intérprete que entienda el lenguaje de los DSL`s.

Definición de los ADT`s

Definiré dos ADT’s para los dos tipos de operaciones: DBOperations, para el ADT’s de las operaciones en base de datos; y, LogOperation. Antes de definir los ADT’s, definiremos el alias a utilizar. Definimos un alias con nombre Response con un tipo genérico, define un contenedor binario de tipo Either de un tipo Exception y un tipo genérico. La definición del alias es el siguiente:

type Response[A] = Either[Exception, A]

El ADT que define las operaciones de base de datos es el mismo que se utiliza en la entrada anterior, mediante la siguiente estructura de clases:

sealed trait DBOperation[A]
case class Configure(xa: Aux[IO, Unit]) extends DBOperation[Response[Unit]]
case class CreateSchema() extends DBOperation[Response[Boolean]]
case class Insert(author: Author) extends DBOperation[Response[Int]]
case class Select(key: Int) extends DBOperation[Response[Option[String]]]
case class Delete(key: Int) extends DBOperation[Response[Int]]

Las clases definidas definen las siguientes operaciones: clase Configure, define la operación de configuración de Transactor de la base de datos; CreateSchema, creación del esquema de base de datos: Insert, inserción de una entidad Author; Select, seleccionar un autor por clave; Delete, eliminación de un autor por clave.

El ADT que define las operaciones de escritura de log se define mediante la siguiente estructura de clases:

sealed trait LogOperation[A]
case class Info(msg: String) extends LogOperation[Response[Unit]]
case class Debug(msg: String) extends LogOperation[Response[Unit]]

Las clases definidas definen las siguientes operaciones: Info, define la escritura de un mensaje de tipo informativo; Debug, define la escritura de un mensaje de tipo depuración.

En los dos ADT’s, se define como respuesta de la operación el tipo Response.

Definición de los DSL’s

Una FreeMonad permite a un Functor tener la funcionalidad de una mónada; añadir esta funcionalidad, la proporciona el tipo Free[_]. Además, para poder trabajar con otros DSL, es necesario añadir elementos que permitan la unión de los DSL; para ello se utilizan las siguientes herramientas:

  • InjectK[DBOperation, F].- Type class que permite inyectar el tipo DBOperation en el tipo F del contexto de ejecución.
  • Free.inject[DBOperation, F].- Función que permite enlazar el tipo DBOperation al tipo F del contexto de ejecución.

La definición de los DSL`s DBOperations y LogOperations se realiza definiendo una clase y su companion object el cual define una referencia implícita a la instancia de la clase. El código de los DSL`s se definen en el siguiente snippet:

class DBOperations[F[_]](implicit I: InjectK[DBOperation, F]){
  def configure(xa: Aux[IO, Unit]): Free[F, Response[Unit]] = Free.inject[DBOperation, F](Configure(xa))
  def createSchema(): Free[F, Response[Boolean]] = Free.inject[DBOperation, F](CreateSchema())
  def insert(elem: Author): Free[F, Response[Int]] = Free.inject[DBOperation, F](Insert(elem))
  def delete(key: Int): Free[F, Response[Int]] = Free.inject[DBOperation, F](Delete(key))
  def select(key: Int): Free[F, Response[Option[String]]] = Free.inject[DBOperation, F](Select(key))
}
object DBOperations{
  implicit def dboperations[F[_]](implicit I: InjectK[DBOperation, F]) = new DBOperations[F]()
}
class LogOperations[F[_]](implicit I: InjectK[LogOperation, F]){
  def infoLog(msg: String): Free[F, Response[Unit]] = Free.inject[LogOperation, F](Info(msg))
  def debugLog(msg: String): Free[F, Response[Unit]] = Free.inject[LogOperation, F](Debug(msg))
}
object LogOperations{
  implicit def logopearations[F[_]](implicit I: InjectK[LogOperation, F]) = new LogOperations[F]()
}

Definición de los intérpretes

Los intérpretes son los functores de transformación. Realizan la interpretación del DSL, interpretando el lenguaje para el que está definido, ejecuta la funcionalidad requerida y retorna el valor transformado.

El intérprete del DSL DBOperations se define en el objeto DBOperationInterprete el cual es un functor de transformación de un tipo DBOperation a OperationResponse. El tipo OperationResponse es un alias del tipo identidad definido en Cats. El alias se define de la siguiente forma:

type OperationResponse[A] = Id[A]

DBOperationsInterprete define una referencia a un transactor de la librería Doobie con una configuración por defecto, esta configuración es un valor mutable ya que puede ser configurada de forma dinámica. Para todo elemento del ADT, se utiliza el repositorio de Author y se trata el resultado como un tipo OperationResponse[A]. El snippet del código con el intérprete es el siguiente:

object DBOperationsInterpreter extends (DBOperation ~> OperationResponse){
  implicit val cs = IO.contextShift(ExecutionContexts.synchronous)
  // TODO var -> val?
  private var xa = Transactor.fromDriverManager[IO](
    s"com.mysql.jdbc.Driver", s"jdbc:mysql://host:port/doobie", s"user", s"pwd",
    Blocker.liftExecutionContext(ExecutionContexts.synchronous) // just for testing
  )
  override def apply[A](fa: DBOperation[A]) = fa match {
    case Configure(xaTransactor) => {
      this.xa = xaTransactor
      val result: OperationResponse[Response[Unit]] = Right(Unit)
      result
    }
    case CreateSchema() => {
      val resultCreateSchema: Response[Boolean] = AuthorRepository.createSchemaIntoMySqlB(xa)
      val result: OperationResponse[ Response[Boolean]] = resultCreateSchema
      result
    }
    case Insert(author) =>{
      val resultTask: Response[Int] = AuthorRepository.insertAuthorIntoMySql(xa, author)
      val result: OperationResponse[Response[Int]] = resultTask
      result
    }
    case Delete(key) => {
      val resultTask: Response[Int] = AuthorRepository.deleteAuthorById(xa, key)
      val result: OperationResponse[Response[Int]] = resultTask
      result
    }
    case Select(key) => {
      val resultTask: Response[Option[String]] = AuthorRepository.selectAuthorById(xa, key)
      val result: OperationResponse[Response[Option[String]]] = resultTask
      result
    }
  }
}

Para el lector interesado en el código de ejemplo del repositorio Author puede acceder a través del siguiente enlace.

El intérprete del DSL`s de operaciones de Log se define en el objeto LogOperationsInterprete el cual define un functor transformador de un tipo LogOperation a un tipo OperationResponse. El intérprete define las operaciones para los mensajes de tipo info y debug escribiendo mensajes en la salida estándar. El snippet de código con el intérprete es el siguiente:

object LogOperationsInterpreter extends (LogOperation ~> OperationResponse){
  override def apply[A](fa: LogOperation[A]) = fa match {
    case Info(msg) =>
      println(s"[*** INFO] ${msg}")
      val result: OperationResponse[Response[Unit]] = Right(Unit)
      result
    case Debug(msg) =>
      println(s"[*** DEBUG] ${msg}")
      val result: OperationResponse[Response[Unit]] = Right(Unit)
      result
  }
}

Un aspecto importante a destacar es que en los dos funtores de transformación, es decir, los intérpretes, tienen el mismo tipo de salida definido en el alias OperationResponse[A].

Llegado a este punto, tenemos definidos los dos intérpretes de forma individual pero, nos falta la definición del intérprete que comprenda los dos lenguajes; para ello, tenemos que definir un tipo que englobe a los dos ADT de los DSL definidos y, posteriormente, define un functor que transforma del tipo anterior al resultado final.

El tipo DoobiePureComposingApp es un alias que define un tipo EitherK con los tipos DBOperation, LogOperation (ADT de los DSL definidos) y el tipo A. Este tipo comprende los tipos de las gramáticas de los DSL.

type DoobiePureComposingApp[A] = EitherK[DBOperation, LogOperation, A]

El tipo que define el resultado es el tipo identidad de los DSL definidos.

type OperationResponse[A] = Id[A]

Así, la definición del intérprete que engloba a los dos DSL se define de la siguiente forma:

val interpreter: DoobiePureComposingApp ~> OperationResponse = DBOperationsInterpreter or LogOperationsInterpreter

Definición del programa de uso de los DSL

Llegado a este punto, estamos en disposición de declarar un programa que realice operaciones en base de datos de la entidad Author y con capacidad de escribir en un log. Como comentamos al inicio de la entrada, el programa declara un conjunto de operaciones representativas con un objetivo pedagógico. El snippet del código es el siguiente:

implicit val cs = IO.contextShift(ExecutionContexts.synchronous)
final case class DatabaseConfig( host: String, port: String, user: String, password: Secret[String])
def loadEnvironmentVariables(): DatabaseConfig = {
  val host: ConfigValue[String] = env("DDBB_HOST").or(prop("ddbb.host")).as[String]
  val port: ConfigValue[String] = env("DDBB_PORT").or(prop("ddbb.port")).as[String]
  val user: ConfigValue[String] = env("DDBB_USER").or(prop("ddbb.user")).as[String]
  val password: ConfigValue[Secret[String]] = env("DDBB_PWD").secret
  val configureEnv: ConfigValue[DatabaseConfig] = (host, port, user, password).parMapN(DatabaseConfig)
  configureEnv.load[IO].unsafeRunSync()
}
val dataConfigure: DatabaseConfig = loadEnvironmentVariables()
private val xa = Transactor.fromDriverManager[IO](
  "com.mysql.jdbc.Driver",
  s"jdbc:mysql://${dataConfigure.host}:${dataConfigure.port}/doobie",
  s"${dataConfigure.user}",
  s"${dataConfigure.password.value}",
  Blocker.liftExecutionContext(ExecutionContexts.synchronous) // just for testing
)
def programBusiness(xa: Aux[IO, Unit])(implicit DB: DBOperations[DoobiePureComposingApp],
L: LogOperations[DoobiePureComposingApp]): Free[DoobiePureComposingApp, Response[Option[String]]] = {
  import DB._
  import L._
  for {
    _ <- debugLog("Configuring database...")
    _ <- configure(xa)
    _ <- debugLog("Creating database...")
    result <- createSchema()
    numInsert1 <- insert(Author(0, "AuthorTest1"))
    numInsert2 <- insert(Author(0, "AuthorTest2"))
    numDelete1 <- delete(key = 1)
    nameAuthor <- select(key = 2)
    _ <- infoLog("Created Database.")
  } yield {
    nameAuthor
  }
}
val result = programBusiness(xa).foldMap(interpreter)
println(s"Result=${result}")

El código presentado, realiza la carga de la configuración de base de datos desde variables de entorno utilizando la librería Ciris; esta funcionalidad, queda definida en la función loadEnvironmentVariables la cual retorna una clase DatabaseConfig con la configuración; se define el Transactor de la librería Doobie con la configuración la cual es utilizada posteriormente en la declaración del programa; y, por último, se declara el programa en la función programBusiness el cual define como implícitos las referencias a las instancias de los DSL con el tipo DoobiePureComposingApp y, como retorno, un tipo Free con el tipo DoobiePureComposingApp y el tipo de retorno del programa Response[Option[String]].

La salida por consola es la siguiente:

[*** DEBUG] Configuring database…
[*** DEBUG] Creating database…
[*** INFO] Created Database.
Result=Right(Some(AuthorTest2))

Para el lector interesado en el código puede acceder a través del siguiente enlace y, para la entrada en donde explico el uso de la librería Ciris, puede acceder mediante el siguiente enlace

FreeMonad en Cats

Definimos FreeMonad como aquella construcción que permite construir una mónada desde un functor. En la presente entrada, FreeMonad en Cats, describiré cómo definir y utilizar una FreeMonad mediante un ejemplo.

Según Wikipedia, definimos funtor como sigue:

«En teoría de categorías un funtor o functor es una función de una categoría a otra que lleva objetos a objetos y morfismos a morfismos de manera que la composición de morfismos y las identidades se preserven».

Un functor cumple la propiedad de identidad y de composición.

Según Wikipedia, definimos mónada como sigue:

«En la programación funcional , una mónada (monad en inglés), es una estructura que representa cálculos definidos como una secuencia de pasos»

Una mónada cumple la propiedad de identidad y asociativa.

La FreeMonad permite lo siguiente:

  • Representar cálculos con estado como datos y ejecutarlos.
  • Ejecutar cálculos recursivos.
  • Definir un DSL.
  • Reorientar un cálculo a otro intérprete utilizando transformaciones naturales.

La implementación de FreeMonad que utilizaremos en el ejercicio es aquella que se ha definido en la librería Cats y, el módulo de cats empleado, es el paquete que se define en cats-free. Una FreeMonad se define como Free[_]

La definición de dependencias del módulo sbt en el fichero build.sbt es la siguiente:

lazy val catsEffect = (project in file("cats-effect"))
  .settings(
    name := "example-cats-effect",
    assemblySettings,
    scalacOptions ++= basicScalacOptions,
    libraryDependencies ++=
      catsEffectDependencies ++ Seq(
        scalaTest
      )
  )
lazy val catsEffectDependencies = Seq(
  cats_effect,
  cats_effect_laws
)

La definición de las coordenadas de los paquetes a utilizar en el módulo para la librería cats-free son las siguientes:

lazy val cats_effect_laws = "org.typelevel" %% "cats-effect-laws" % "2.2.0" % "test"
lazy val cats_free = "org.typelevel" %% "cats-free" % "2.2.0"

Presentación del problema

Supongamos que tenemos un aplicativo el cual maneja una entidad Autor y necesitamos persistir los datos de la entidad Autor en base de datos. Para ello, necesitamos realizar operaciones sobre base de datos como son: creación del esquema, insertar, borrar y buscar elementos. La definición de la entidad de dominio del problema se define en la siguiente case class:

case class Author(id:Long, name: String)

Para dar solución a este problema, definiremos un DSL que soporte las funcionalidades requeridas. Evidentemente, es un problema teórico con el objetivo describir la definición de una FreeMonad.

Definición de la gramática

La gramática del DSL queda definida en un ADT (Tipo de dato Algebraico) el cual define las operaciones a realizar. El ADT con las operaciones de base de datos OperationDB[A] queda definido en la siguiente estructura de clases:

sealed trait OperationDB[A]
case class CreateSchema() extends OperationDB[OperationDBResponse[Boolean]]
case class Insert(author: Author) extends OperationDB[OperationDBResponse[Int]]
case class Select(key: Int) extends OperationDB[OperationDBResponseOption[String]]
case class Delete(key: Int) extends OperationDB[OperationDBResponse[Int]]

Las operaciones de base de datos son: CreateSchema, operación para la creación del esquema de la entidad Autor; Insert, operación de inserción de un Autor; Select, búsqueda de un Autor en función de una clave; y, Delete, eliminación de un Autor por clave. El tipo parametrizado es el tipo de retorno de cada operación cuyos tipos son los representados en los siguiente alias:

type OperationDBResponse[A] = Either[Exception, A]
type OperationDBResponseOption[A] = Either[Exception, Option[A]]

Los alias descritos anteriormente define contenedores binarios de Tipo Either cuyo valor Left representa una excepción y, su valor Right, representa el resultado de la operación de tipo A u Option[A]

Definición del DSL

El DSL se define mediante una FreeMonad la cual, utilizando Cats, queda definida con el tipo Free[_]. Así, el alias del tipo de operación del DSL se define utilizando el siguiente alias:

type Operation[A] = Free[OperationDB, A]

El alias Operation[A] define una FreeMonad cuyo valor de entrada es una operación de tipo OperationDB y cuyo resultado es un tipo genérico A.

El DSL debe de definir funciones para las siguientes operaciones: creación de un esquema, inserción, borrado y consulta. Las funciones del DSL son las siguientes:

import cats.free.Free
import cats.free.Free.liftF
[...]
def createSchema(): Operation[OperationDBResponse[Boolean]] =
    liftF[OperationDB, OperationDBResponse[Boolean]](CreateSchema())
def insert(elem: Author): Operation[OperationDBResponse[Int]] =
    liftF[OperationDB, OperationDBResponse[Int]](Insert(elem))
def delete(key: Int): Operation[OperationDBResponse[Int]] =
    liftF[OperationDB, OperationDBResponse[Int]](Delete(key))
def select(key: Int): Operation[OperationDBResponseOption[String]] =
    liftF[OperationDB, OperationDBResponseOption[String]](Select(key))

Las funciones del DSL utilizan la función liftF que permite subir de contexto de una operación OperationDB con un tipo de respuesta a una FreeMonad, Free[_].

Definición del intérprete del DSL

Una vez definido el lenguaje de dominio es necesario definir aquel intérprete que sea capaz de traducir el lenguaje en operaciones de base de datos. En nuestro caso, utilizaremos una entidad repositorio el cual sea capaz de realizar las operaciones de base de datos, consiguiendo desacoplar las operaciones de interpretación del lenguaje con las operaciones directas de base de datos.

El repositorio es un objeto llamado AuthorRepository el cual utiliza la librería funcional Doobie para realizar las operaciones.

Para cada operación del lenguaje, el intérprete gestiona un estado. Los estados son los siguientes: Init, estado inicial que representa el estado previo a la creación del esquema; Created, estado que indica que el esquema ha sido creado; Deleted, estado que representa la eliminación del esquema, en la práctica no se utiliza.

Los estados quedan representados en la siguiente estructura:

sealed trait StateDatabase
case object Init extends StateDatabase
case object Created extends StateDatabase
case object Deleted extends StateDatabase

Dada la existencia de estados, el intérprete utilizará la mónada estado para gestionar el retorno de estado de cada operación.

El intérprete define un functor de transformación, representado por el símbolo ~> el cual transforma entidades de tipo OpearetionDB en OperationState. El functor intérprete recibe como parámetro un Transactor de Doobie de la base de datos con la que se opera. El snippet de código del intérprete es el siguiente:

import cats.~>
[...]
def pureInterpreter(xa: Aux[IO, Unit]): OperationDB ~> OperationState = new(OperationDB ~> OperationState){
override def apply[A](fa: OperationDB[A]): OperationState[A] = fa match {
  case CreateSchema() => {
    val resultCreateSchema: Either[Exception, Boolean] = AuthorRepository.createSchemaIntoMySqlB(xa)
    resultCreateSchema
      .fold( ex => State[StateDatabase, OperationDBResponse[Boolean]]{ state => (Init,  resultCreateSchema) },
        value => State[StateDatabase, OperationDBResponse[Boolean]]{ state => (Created,  resultCreateSchema) } )
  }
  case Insert(author) =>{
    State[StateDatabase, OperationDBResponse[Int]]{ state =>
      (Created,  AuthorRepository.insertAuthorIntoMySql(xa, author))
    }
  }
  case Delete(key) => {
    State[StateDatabase, OperationDBResponse[Int]]{ state =>
      (Created,  AuthorRepository.deleteAuthorById(xa, key))
    }
  }
  case Select(key) => {
    State[StateDatabase, OperationDBResponseOption[String]]{ state =>
      (Created,  AuthorRepository.selectAuthorById(xa, key))
    }
  }
 }
}

Definición del repositorio de la entidad Autor.

El repositorio con las operaciones de base de datos está definiado en un objeto AuthorRepository que utiliza la librería Doobie. El objeto define un conjunto de funciones con las operaciones que utiliza el intérprete del DSL; como ejemplo, la operación de creación del esquema es la siguiente:

val dropAuthor: ConnectionIO[Int] = sql"""DROP TABLE IF EXISTS Author""".update.run
val createAuthor: ConnectionIO[Int] = sql"""CREATE TABLE Author (id SERIAL, name text)""".update.run
def createSchemaIntoMySqlA[A](xa: Aux[IO, Unit]): Either[Exception, Unit] = {
  val creator: ConnectionIO[Unit] = for {
    _ <- dropAuthor
    _ <- createAuthor
  } yield ()
  try{
    val resultDatabase: Unit = creator.transact(xa).unsafeRunSync
    Right(resultDatabase)
  } catch {
    case e: java.sql.SQLException =>{
      println(s"Error create schema: ${e}")
      Left(e)
    }
  }
}

Para el lector interesado en el código del repositorio puede acceder en el siguiente enlace.

Definición de los programas

Una vez definida en un objeto AuthorDSL las funciones del DSL, alias e intérpretes, estamos en disposición de definir las declaraciones de los programas que pueden utilizar dicho lenguaje; para ello, necesitamos importar el DSL y definir el Transactor de base de datos, estas operaciones, se realizan de la siguiente forma:

import doobie.Transactor
import doobie.util.ExecutionContexts
import es.ams.freemonaddoobie.AuthorDSL._
[...]
implicit val cs = IO.contextShift(ExecutionContexts.synchronous)
val xa = Transactor.fromDriverManager[IO](
  "com.mysql.jdbc.Driver",
  "jdbc:mysql://localhost:3306/doobie",
  "root",
  "root",
  Blocker.liftExecutionContext(ExecutionContexts.synchronous) // just for testing
)

Para la correcta ejecución del programa es necesario tener una base de datos levantada en la configuración que se especifica en el Transactor; en mi caso, he utilizado un contenedor Docker con una base de datos MySQL.

La definición de un programa está compuesta por la secuencia de funciones del DSL definidas en un for comprehension y, para su ejecución, se utiliza  la función foldMap con el intérprete definido. Al utilizar la mónada estado, es necesario arrancar la ejecución con un estado.

En los siguientes apartados, se describen ejemplos de las declaraciones de programas del DSL definido

1.- Ejemplo de programa de creación de un esquema.

def createDatabase(): Operation[Either[Exception, Boolean]] = for {
  result <- createSchema()
} yield (result)
val resultCreate = createDatabase().foldMap(pureInterpreter(xa)).run(Init).value
println(s"Create database=${resultCreate}")
println

La salida por consola es la siguiente:

Create database=(Created,Right(true))

2.- Ejemplo de programa de inserción de un Autor en base de datos.

def insertAuthor(): Operation[Either[Exception, Int]] = for {
  num <- insert(Author(0, "Author1"))
} yield (num)
val resultInsertAuthor = insertAuthor().foldMap(pureInterpreter(xa)).run(Created).value
println(s"Insert Author=${resultInsertAuthor}")
Println

La salida por consola es la siguiente:

Insert Author=(Created,Right(1))

3.- Ejemplo de programa de eliminación de un Autor en base de datos.

def deleteAuthor(): Operation[Either[Exception, Int]] = for {
  numInsert10 <- insert(Author(0, "Author10"))
  numInsert11 <- insert(Author(0, "Author11"))
  numInsert12 <- insert(Author(0, "Author12"))
  numDeleted <- delete(2)
} yield (numDeleted)
val resultDeleteAuthor = deleteAuthor().foldMap(pureInterpreter(xa)).run(Created).value
println(s"Delete Author=${resultDeleteAuthor}")
println

La salida por consola es la siguiente:

Delete Author=(Created,Right(1))

4.- Ejemplo de programa de consulta de un autor por clave.

def selectAuthorOK(): Operation[Either[Exception, Option[String]]] = for {
  author <- select(1)
} yield (author)
val resultSelectAuthorOK = selectAuthorOK.foldMap(pureInterpreter(xa)).run(Created).value
println(s"Select Author=${resultSelectAuthorOK}")
println

La salida por consola es la siguiente:

Select Author=(Created,Right(Some(Author1)))

Para el lector interesado puede acceder al código de la entrada en el siguiente enlace.

Patrón Traverse en cats

En la entrada anterior, Patrón Fodable en Cats, realicé una descripción de cómo se realizaban morfismos con tipos de datos algebraicos (ADT) utilizando la implementación del tipo Foldable de la librería cats. En la presente entrada, Patrón Traverse en Cats, me centraré en el tipo Traverse.

El tipo Traverse tiene dos funciones: traverse y sequence; en los siguientes apartados, realizaré la descripción de cada una.

1.- Traverse

El tipo Traverse define la función traverse la cual permite realizar lo siguiente: dado un tipo de entrada y dada una función de transformación; la función traverse permite: la iteración sobre el tipo de entrada, aplica la función a cada elemento de la entrada, acumular el resultado y retornar su resultado. Un ejemplo de una definición de función traverse puede ser el siguiente:

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import cats.syntax.applicative._
import cats.syntax.apply._
def getFutureTest(msg: String): Future[Int] = 
   Future{msg.length * 10 }
def myTraverse[A,B](list: List[A])(f: A => Future[B]): Future[List[B]] =
  list.foldLeft(Future(List.empty[B])){ (acc, elem) => {
      val resultElem = f(elem)
      for{
         acc <- acc
         elem <- resultElem
      }yield{ acc :+ elem }
    }
  }
val listExample1 = List ("a", "aa", "aaa")
val resultExample1 = myTraverse(listExample1)(getFutureTest)
println(s"myTraverse(List ('a', 'aa', 'aaa'))-->${Await.result( resultExample1, 5.seconds )}")

La salida por consola es la siguiente:

myTraverse(List ('a', 'aa', 'aaa'))-->List(10, 20, 30)

El snippet anterior define lo siguiente: getFutureTest, función que retorna un Future de enteros que retorna la longitud del string pasado por parámetro multiplicado por 10; myTraverse, función traverse implementado con foldLeft la cual opera con una lista y una función f que retorna un Futuro del tipo B a partir del tipo A; listExample1, una lista de pruebas; y, por último, el mensaje con la función traverse y su visualización por pantalla.

1.1.- Traverse con Applicative

La función traverse podemos simplificarla utilizando tipos que cumplan el patrón Applicative la cual contiene operaciones del patrón Semigroupal como la función mapN; así, la función traverse, se puede redefinir de la siguiente manera:

def myTraverse2[F[_]: Applicative, A,B](list: List[A])(f: A => F[B]): F[List[B]] =
  list.foldLeft( List.empty[B].pure[F] ){
      (acc, elem) => (acc, f(elem)).mapN(_ :+ _)
  }
import cats.instances.option._
def process(list: List[Int]) = {
   myTraverse2(list)(n => if(n%2==0) Some(n) else None)
}
println(s"--Ejemplo3--")
println(s"process(List(2,4,6))==>>${process(List(2,4,6))}")
println(s"process(List(1,2,3))==>>${process(List(1,2,3))}")

La salida por consola es la siguiente:

process(List(2,4,6))==>>Some(List(2, 4, 6))
process(List(1,2,3))==>>None

1.2.- Traverse con Validated

El siguiente ejemplo, permite la validación de los elementos de una lista en función de un criterio: los elementos pares son válidos y, los impares, son inválidos. El snippet con la solución es la siguiente:

import cats.data.Validated
import cats.instances.list._
type ErrorOn[A] = Validated[ List[String] ,A]
def myTraverse2[F[_]: Applicative, A,B](list: List[A])(f: A => F[B]): F[List[B]] =
   list.foldLeft( List.empty[B].pure[F] ){
      (acc, elem) => (acc, f(elem)).mapN(_ :+ _)
    }
def process(list: List[Int]): ErrorOn[List[Int]] = {
   myTraverse2(list){ n =>
     if(n%2==0){
        Validated.valid(n)
     }else{
        Validated.invalid(List(s"$n no está incluido."))
     }
   }
}
println(s"process(List(2,4,6))==>>${process(List(2,4,6))}")
println(s"process(List(1,2,3))==>>${process(List(1,2,3))}")
println(s"process(List(2,4,5,6))==>>${process(List(2,4,5,6))}")

La salida por consola es la siguiente:

process(List(2,4,6))==>>Valid(List(2, 4, 6))
process(List(1,2,3))==>>Invalid(List(1 no está incluido., 3 no está incluido.))
process(List(2,4,5,6))==>>Invalid(List(5 no está incluido.))

1.3.- Función traverse con el tipo traverse

En los apartados anteriores, me he centrado en mostrar ejemplos de la función traverse con una implementación propia. En el siguiente ejemplo, muestro un ejemplo con la función traverse del tipo Traverse. La funcionalidad del ejemplo consiste en procesar una lista de futuros, el snippet es el siguiente:

import cats.Traverse
import cats.instances.all._
val listExample1 = List ("a", "aa", "aaa")
def getFutureTest(msg: String): Future[Int] = 
  Future{msg.length * 10}
val result1:Future[List[Int]] = Traverse[List].traverse(listExample1)(getFutureTest)
println(s"Traverse1=${Await.result( result1, 2.seconds )}")
val listExampleSequence1 = List( Future(1), Future(2), Future(3))
val result2: Future[List[Int]] = Traverse[List].sequence(listExampleSequence1)
println(s"Sequence1=${Await.result( result2, 2.seconds )}")

La salida por consola es la siguiente:

Traverse1=List(10, 20, 30)
Sequence1=List(1, 2, 3)

2.- Sequence

Por otro lado, el tipo Traverse define la función sequence la cual permite recorrer los elementos de un tipo de entrada y realizar los cambios de tipos. Un ejemplo de una función sequence puede ser la siguiente:

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import cats.syntax.applicative._
import cats.syntax.apply._
def getFutureTest(msg: String): Future[Int] = 
    Future{msg.length * 10 }
def mySequence[B](list:List[Future[B]]): Future[List[B]] =
    myTraverse(list)(identity)
val listExampleSequence1 = List( getFutureTest("a"), getFutureTest("aa"), getFutureTest("aaa"))
val resultExample2 = mySequence(listExampleSequence1)
println(s"myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->${Await.result( resultExample2, 5.seconds )}")

La salida por consola es la siguiente:

myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->List(10, 20, 30)

El snippet anterir define lo siguiente: getFutureTest, función que retorna un Future de enteros que retorna la longitud del string pasado por parámetro multiplicado por 10; mySequence,  función que emplea la función traverse para realizar la transformación; listExampleSequence1, lista con los datos de prueba; y, por último, el mensaje con la función sequence y su visualización.

La funcionalidad del ejemplo anterior implemantado con la función sequence de Traverse queda descrito en el siguiente enjemplo:

import cats.Traverse
import cats.instances.all._
val listExampleSequence1 = List( getFutureTest("a"), getFutureTest("aa"), getFutureTest("aaa"))
val result1:Future[List[Int]] = Traverse[List].sequence(listExampleSequence1)
println(s"myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->${Await.result( result1, 5.seconds )}")

La salida por consola es la siguiente:

myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->List(10, 20, 30)

3.- Definición formal de Traverse

La definición formal del trait con la funcionalidad Traverse es la siguiente:

package cats
trait Traverse[F[_]] {
  def traverse[G[_]: Applicative, A, B] (inputs: F[A])(func: A => G[B]): G[F[B]]
  def sequence[G[_]: Applicative, B] (inputs: F[G[B]]): G[F[B]] = 
traverse(inputs)(identity)
}

Para finalizar la entrada y como conclusión final, el tipo Traverse es un patrón conseguido y comprensible a partir del patrón Foldable y la función fold. Traverse permite realizar la iteración y operación sobre colecciones de tipos y, además, realizar acumuladores de resultados de dichas colecciones.

Patrón Fodable en Cats

En la programación funcional uno de los conceptos base son los tipos de datos algebráicos (ADT) Los ADT son estructuras de datos basadas en las matemáticas cuyas operaciones se realizan mediante morfismos; y, los mosfirmos, se realizan mediante la función fold y sus derivados: foldRight y foldLeft. En la entrada de hoy, Patrón Foldable en Cats, realizaré la descripción de los morfismos utilizando la type class Foldable de la librería Cats.

1.- Definición de un ADT de tipo List

Un ADT es aquel tipo de dato con el que podemos realizar unas operaciones, como por ejemplo: la operación suma y producto; y, además, cumple unas propiedades  matemáticas como pueden ser la propiedad asociativa, distributiva, o bien, de identidad.

En el siguiente ejemplo, se muestra la definición del ADT de tipo MyList, el cual equivale al ADT de tipo List.

sealed trait MyList[+A]
case object Nil extends MyList[Nothing]
case class Cons[+A](elem: A, lista: MyList[A]) extends MyList[A]

La operación suma es aquella operación que, a nivel de programación, se corresponde con las relaciones de herencia entre la clase Cons y el objeto Nil con el trait MyList. La operación producto es aquella operación que, a nivel de programación, se corresponde con los parámetros de la clase Cons: elem y lista.

Una vez definido el ADT una de las formas de manipular dicha estructura es utilizando morfismos, función fold y sus derivados. La función fold equivale a la función foldRight. La definición de la función foldRight y foldLeft con el ADT MyList son los siguientes:

  • Morfismo foldRight para el ADT MyList.
def foldRight[A, B](lista: MyList[A], elem: B)(f: (A, B) => B): B = lista match {
  case Nil => elem
  case Cons(head, tail) => f(head, foldRight(tail, elem)(f))
}
  • Morfismo foldLeft para el ADT MyList.
@annotation.tailrec
def foldLeft[A, B](lista: MyList[A], elem: B)(f: (B, A) => B): B = lista match {
  case Nil => elem
  case Cons(head, tail) => foldLeft(tail, f(elem, head))(f)
}

fold, foldRight, foldLeft

En los siguientes apartados, realizaremos la descripción de ejemplos de uso de las operaciones fold con el type class que proporciona la librería Cats y con la librería estándar.

2.- Ejemplos de morfismos con el tipo List

En el presente apartado, realizaré la descripción de ejemplos con la función fold del ADT List de la librería estándar.

  • Ejemplos básicos de morfismo foldRight.- Definición de una construcción de una lista y suma de sus elementos con una lista de tipos de enteros y foldRight.
println(s"1.- foldRight=${ List(1,2,3).foldRight(List.empty[Int])( (e, acc) => e :: acc) }")
println(s"2.- foldRight=${ List(1,2,3).foldRight(0)( (e, acc) => e + acc ) }")
println(s"Suma con foldRight=${List(1, 2, 3, 4).foldRight(0)(_ + _)}")

La salida por consola es la siguiente:

1.- foldRight=List(1, 2, 3)
2.- foldRight=6
Suma con foldRight=10
  • Ejemplos básicos de morfismo foldLeft.- Definición de una construcción de una lista y suma de sus elementos con una lista de tipos de enteros y foldLeft.
println(s"1.- foldLeft=${ List(1,2,3).foldLeft(List.empty[Int])((acc, e) => e :: acc) }")
println(s"2.- foldLeft=${ List(1,2,3).foldLeft(0)( (acc, e) => acc + e ) }")

La salida por consola es la siguiente:

1.- foldLeft=List(3, 2, 1)
2.- foldLeft=6
  • FoldRight y el tipo Numeric.- Definición de una función suma empleando una lista de enteros y el tipo Numeric.
import scala.math.Numeric
def sumaConNumeric[A](list:List[A])(implicit numeric: Numeric[A]): A =
list.foldRight(numeric.zero)(numeric.plus)
println(s"Suma con Numeric=${sumaConNumeric(List(1, 2, 3, 4))}")
println

La salida por consola es la siguiente:

Suma con Numeric=10
  • FoldRight y monoides.- Definición de la operación suma sobre una lista de enteros empleando monoides.
import cats.Monoid
import cats.instances.int._ // for Monoid
def sumaConMonoid[A](list:List[A])(implicit monoid: Monoid[A]): A =
list.foldRight(monoid.empty)(monoid.combine)
println(s"Suma con Momoid=${sumaConMonoid(List(1, 2, 3, 4))}")

La salida por consola es la siguiente:

Suma con Momoid=10
  • FoldRight y definición de filtros.- Definición de unos filtros sobre una lista de enteros
val elemFilter1: Int = 3
println(s"List(1, 2, 3, 4) existe el 3?=${List(1, 2, 3, 4).foldRight(false)( (elem, resul) => resul || elem.equals(elemFilter1))}")
val elemFilter2: Int = 5
println(s"List(1, 2, 3, 4) existe el 5?=${List(1, 2, 3, 4).foldRight(false)( (elem, resul) => resul || elem.equals(elemFilter2))}")
def myfilter[A](list: List[A])(func: A => Boolean): List[A] =
list.foldRight(List.empty[A]) { (item, accum) => if(func(item)) item :: accum else accum }
println(s"List(1, 2, 3, 4) filtra los pares.=${ myfilter(List(1, 2, 3, 4))(_%2==0) }")

La salida por consola es la siguiente:

List(1, 2, 3, 4) existe el 3?=true
List(1, 2, 3, 4) existe el 5?=false
List(1, 2, 3, 4) filtra los pares.=List(2, 4)
  • FoldRight y definición de función map.- Definición de una función map con foldRight.
def myMap[A,B](list: List[A])(f: A => B): List[B] = list.foldRight(List.empty[B])( (elem, result) => f(elem) :: result )
println(s"List(1, 2, 3) map to String=${List(1, 2, 3).foldRight(List.empty[String])( (elem, resul) => s"-${elem.toString}-" :: resul)}")
println(s"List(1, 2, 3) map to String=${ myMap(List(1, 2, 3))( (elem:Int) => s"*${elem.toString}*" ) }")
println

La salida pos consola es la siguiente:

List(1, 2, 3) map to String=List(-1-, -2-, -3-)
List(1, 2, 3) map to String=List(*1*, *2*, *3*)
  • FoldRight y definición de función flatMap.- Definición de una función flatMap con foldRight.
def flatMap[A, B](list: List[A])(func: A => List[B]): List[B] =
list.foldRight(List.empty[B]) { (item, accum) => func(item) ::: accum }
println(s"-->>${flatMap(List(1, 2, 3))(a => List(a, a * 10, a * 100))}")
println

La salida por consola es la siguiente:

-->>List(1, 10, 100, 2, 20, 200, 3, 30, 300)

3.- Ejemplos con Foldable de cats.

Para poder operar con el tipo Foldable es necesario, al menos, realizar la importación de los siguientes tipos:

import cats.Foldable
import cats.instances.all._
  • Ejemplo de Foldable con función foldLeft con los tipos List, Vector, Stream y Option.
println(s"Suma List(1, 2, 3)=${Foldable[List].foldLeft(List(1, 2, 3), 0)( _ + _ )}")
println(s"Suma Vector(1, 2, 3)=${Foldable[Vector].foldLeft(Vector(1, 2, 3), 0)( _ + _ )}")
println(s"Suma Stream(1, 2, 3)=${Foldable[Stream].foldLeft(Stream(1, 2, 3), 0)( _ + _ )}")
println(s"Suma Option(10) + 5=${Foldable[Option].foldLeft(Option(10), 0)( (acc, elem) => elem + 5 )}")

La salida por consola es la siguiente:

Suma List(1, 2, 3)=6
Suma Vector(1, 2, 3)=6
Suma Stream(1, 2, 3)=6
Suma Option(10) + 5=15
  • StackOverflowError con función foldRight.

Supongamos que queramos realizar la suma de una estructura de tipo Stream de 100000 elementos, la definición de la solución sería la siguiente:

val lista = (1 to 100000).toStream
println(s"Suma (1 to 100000).toStream->${ lista.foldRight(0L)(_ + _) }")

El resultado de la ejecución del snippet anterior es errónea porque se produce un desbordamiento de la pila del sistema y nos aparece en consola un error de tipo StackOverflowError. Una solución a este problema utilizando la función foldRight es utilizando la mónada Eval. Si el lector está interesado en la mónada Eval, pude ir a las siguientes enlace.El snippet es el siguiente:

import cats.Eval
val resultEvalStream = Foldable[Stream].foldRight(lista, Eval.now(0L)) ((num, acc) => acc.map( _ + num))
println(s"Suma (1 to 100000).toStream->${ resultEvalStream.value }")

La salida por consola es la siguiente:

Suma (1 to 100000).toStream->5000050000
  • Ejemplos de funciones básicos de Foldable con tipo Option.
println(s"Foldable[Option].nonEmpty(Option(42))=${Foldable[Option].nonEmpty(Option(42))}" )
println(s"Foldable[Option].isEmpty(Option(42))=${Foldable[Option].isEmpty(Option(42))}" )
println(s"Foldable[Option].size(Option(42))=${Foldable[Option].size(Option(42))}" )
println(s"Foldable[Option].get(Option(42))(0)=${Foldable[Option].get(Option(42))(0) }" )
println(s"Foldable[Option].find(Option(42))( elem => elem>30)=${Foldable[Option].find(Option(42))( elem => elem>30) }" )
println

La salida por consola es la siguiente:

Foldable[Option].nonEmpty(Option(42))=true
Foldable[Option].isEmpty(Option(42))=false
Foldable[Option].size(Option(42))=1
Foldable[Option].get(Option(42))(0)=Some(42)
Foldable[Option].find(Option(42))( elem => elem>30)=Some(42)
  • Ejemplos de funciones básicas de Foldable con tipo List.
println(s"Foldable[Option].nonEmpty(List(1, 2, 3)=${Foldable[List].nonEmpty(List(1, 2, 3))}" )
println(s"Foldable[Option].isEmpty(List(1, 2, 3))=${Foldable[List].isEmpty(List(1, 2, 3))}" )
println(s"Foldable[Option].size(List(1, 2, 3)=${Foldable[List].size(List(1, 2, 3))}" )
println(s"Foldable[Option].get(List(1, 2, 3)(0)=${Foldable[List].get(List(1, 2, 3))(0)}" )
println(s"Foldable[Option].get(List(1, 2, 3)(1)=${Foldable[List].get(List(1, 2, 3))(1)}" )
println(s"Foldable[Option].get(List(1, 2, 3)(4)=${Foldable[List].get(List(1, 2, 3))(4)}" )
println(s"Foldable[Option].find(List(1, 2, 3)(4)=${Foldable[List].find(List(1, 2, 3))( elem => (elem%2==0) )}" )
println(s"Foldable[Option].find(List(1, 2, 3)(4)=${Foldable[List].find(List(1, 2, 3))( elem => (elem%2!=0) )}" )
println

La salida por consola es la siguiente:

Foldable[Option].nonEmpty(List(1, 2, 3)=true
Foldable[Option].isEmpty(List(1, 2, 3))=false
Foldable[Option].size(List(1, 2, 3)=3
Foldable[Option].get(List(1, 2, 3)(0)=Some(1)
Foldable[Option].get(List(1, 2, 3)(1)=Some(2)
Foldable[Option].get(List(1, 2, 3)(4)=None
Foldable[Option].find(List(1, 2, 3)(4)=Some(2)
Foldable[Option].find(List(1, 2, 3)(4)=Some(1)
  • Ejemplo de Foldable con monoides. El tipo Foldable define operaciones con monoides.
import cats.instances.all._
println(s"Foldable[Option].combineAll(List(1, 2, 3))=${Foldable[List].combineAll(List(1, 2, 3))}" )
println

La salida por consola es la siguiente:

Foldable[Option].combineAll(List(1, 2, 3))=6
  • Ejemplo de Foldable con función map. El tipo Foldable define la función foldMap para definir funciones con la funcionalidad de fold y la función map.
import cats.instances.all._
println(s"Foldable[List].foldMap(List(1, 2, 3))( elem => elem + 20) =${Foldable[List].foldMap(List(1, 2, 3))( elem => elem + 20) }")
println

La salida por consola es la siguiente:

Foldable[List].foldMap(List(1, 2, 3))( elem => elem + 20) =66

El entendimiento y el uso de los  morfismos facilita y simplifica el código; y, la utilización de Foldable, permite una versatilidad para cualquier operación.

Patrón Type Class

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:

  1. Definición del trait con la definición del API.
  2. Definición del trait con las instancias de los elementos que implementa el API en función del tipo.
  3. Definición del trait con la sintaxis.
  4. 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.