Funciones lambda con receptores

Las funciones lambdas son funciones que permiten recibir funciones como parámetros, o bien, retornar una función. Este tipo de función cuya utilización es común en lenguajes con paradigma funcional como son los lenguajes Kotlin, Scala o Haskell. En lengueje Kotlin existe una variante de función lambda la cual permite recibir una referencia al objeto al que se le aplica la función; este tipo de función, se conoce como funciones lambda con receptores.

Para comprender las funciones lambda con receptores, mostraré unos ejemplos de funciones lambda: el primero, es un ejemplo de construcción de una cadena de texto; el segundo, realiza una transformación del tipo de entrada.

Ejemplo 1, función lambda.

Se define una función buildString que recibe como parámetro una función lambda cuyo parámetro de entrada es un elemento de tipo StringBuilder y su salida es un tipo Unit. En el siguiente ejemplo, la función buildString realiza la siguiente funcionalidad: instancia un objeto de la clase StringBuilder identificado como sb, aplicamos la función lambda pasada por parámetros con el objeto sb y, por último, retornamos el String resultante. Para finalizar el ejemplo, se define la invocación de la función y la impresión por consola del resultado.

El snippet del código es el siguiente:

fun buildString(
  builderAction: (StringBuilder) -> Unit
):String{
  val sb = StringBuilder()
  builderAction(sb)
  return sb.toString()
}
val s = buildString {
  it.append("Hello, ")
  it.append("World! ")
}
println("1.- Function Lambda=${s}")

La salida por consola es la siguiente:

1.- Function Lambda=Hello, World!

Ejemplo 2, función lambda

Se define una entidad con nombre Account, se define una función calculate30 la cual recibe como parámetro un elemento de tipo Account y una función lambda que transforma un tipo de entrada Account en otro tipo Account. La función calculate30 realiza las siguientes operaciones: creación de una instancia Account a partir del objeto pasado por parámetro de entrada y, por último, aplicación de la función lambda pasada por parámetro con el objeto Account creado previamente. Para finalizar el ejemplo, se realiza la creación del objeto account1, la invocación a la función calculate30 y la impresión por consola del resultado.

El snippet del código es el siguiente:

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30(
  init: Account,
  func: (Account) -> Account): Account{
    val account = init.copy(amount = 30)
    return func(account)
}
val account1 = Account(id = 1, amount = 0, result = 0, status = "INIT")
val CTE = 3
val fLambda = calculate30(account1){
  it.copy( result = (it.amount * 6) * CTE, status = "END" )
}
println("1.- Account30 (lambda function)=${fLambda}")

La salida por consola es la siguiente:

1.- Account30 (lambda function)=Account(id=1, amount=30, result=540, status=END)

En los ejemplos anteriores, se puede apreciar que la definición de la función lambda se utiliza la palabre reservada “it” para poder trabajar con el objeto con el que se opera.

Funciones lambda con receptores

Las funciones lambda con receptores en Kotlin son como las funciones lambda a diferencia que la función trabaja con las funciones del tipo de entrada. La declaración de la función, se realiza como si operase con el objeto del tipo de entrada, pudiendo usar el operador this.

Ejemplo 1, función lambda con receptor.

Se define una función buildStringReceiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente: creación del objeto s de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

fun buildStringReceiver(
  builderAction: StringBuilder.() -> Unit
): String{
    val s = StringBuilder()
    s.builderAction()
    return s.toString()
}
val r = buildStringReceiver{
  this.append("Hello, ")
  this.append("World! ")
}
println("2.- Function Lambda receiver=${r}")

La salida por consola es la siguiente:

2.- Function Lambda receiver=Hello, World!

Ejemplo 2, función lambda con receptor.

Se define una función calculate30Receiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente:
creación del objeto de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30Receiver(
  init: Account,
  func: Account.() -> Account
): Account{
    val account = init.copy(amount = 30)
    return account.func()
}
val fLReceiver = calculate30Receiver(account1){
  this.copy(result = (this.amount * 6) * CTE, status = "END" )
}
println("2.- Account30Receiver(lambda with receiver)=${fLReceiver}")

La salida por consola es la siguiente:

2.- Account30Receiver(lambda with receiver)=Account(id=1, amount=30, result=540, status=END)

Un ejemplo de función lambda con receptor en kotlin pueden ser las funciones apply o use.

Para el lector interesado, el código completo del ejemplo se encuentra en el siguiente enlace

Objetos invocables como funciones en lenguaje Kotlin

En lenguaje Kotlin tenemos la posibilidad de definir una clase la cual funciona como un función; es decir, una vez creado el objeto de la clase, operamos con la instancia como si fuera una función. En la presente entrada, Objetos invocables como funciones en lenguaje Kotlin, mostraré unos ejemplos básicos de uso de esta características de clase.

Los objetos invocables en Kotlin me traen como recuerdo las first class en Scala. Las first class son funciones que se definen como  una variable, un argumento de función, o bien, el resultado de una función. El compilador de Scala, realiza la transformación de la función a una clase transparente al desarrollador. Un ejemplo de first class en Scala es el siguiente:

val multiplyBy2 = (elem: Int) => (elem * 2)

Los ejemplos de objetos invocados que presento en la entrada son tres:

  1. Instanciación de una clase invocable básica.
  2. Instanciación de una clase invovable a partir de un interface.
  3. Definición de un predicado en una clase invocable.

Una clase invocable es aquella clase que define una función específica defina en un método con nombre invoke el cual está definida como un operador. La clase define un constructor y, para la invocación de la función invoke, no es necesario especificar el nombre de la función.

Instancia de una clase invocable básica

En el siguiente ejercicio presento un ejemplo básico. Defino una clase Greeter con un único método invoke que recibe un parámetro; la funcionalidad del método, es la escritura por pantalla del atributo de la clase y el parámetro. El snippet del código es el siguiente:

class Greeter(val greeting: String){
  operator fun invoke(name: String){
    println("Greeting=${greeting} Name=${name}")
  }
}
val obj1 = Greeter("Test1")
obj1("ParamInvoke")

La instancia de la clase Greeter se realiza como cualquier clase típica; pero, la invocación del método, no se especifica sino que se emplea la referencia de la instancia con los parámetros requeridos en la firma del método invoke.

La salida por consola es la siguiente:

Greeting=Test1 Name=ParamInvoke

Instanciación de una clase invovable a partir de un interface

Incrementando el nivel de dificultad, presento una clase SpecialFunction con su método invoke. El método invoke tiene una definición especial porque se utiliza un interface donde se definen los tipos de entrada y de salida de forma genérica; en concreto, se definen dos parámetros de entrada: p1 de tipo X y p2 de tipo Y; y, por último, se define un tipo de salida de tipo Z. El snippet del código es el siguiente:

interface MyFuntion2<in X, in Y, out Z>{
  operator fun invoke(p1:X, p2: Y): Z
}
class SpecialFunction(): MyFuntion2<Int, Int, String>{
  override fun invoke(p1: Int, p2: Int): String {
    return StringBuilder()
      .append(p1)
      .append("+")
      .append(p2)
      .append("=")
      .append((p1+p2).toString())
      .toString()
  }
}
val objectFunctionSum = SpecialFunction()
println("Suma=>${objectFunctionSum(p1 =2,p2=3)}")

De la misma manera que el anterior caso, se realiza la instancia de la clase y, con ésta, realizamos la invocación del método invoke con los parámetros requeridos en su firma.

La salida por consola es la siguiente:

Suma=>2+3=5

Definición de un predicado en una clase invocable

El último caso que presento es una clase que representa un predicado semántico dentro de un contexto funcional. El escenario es el siguiente: dado un sistema que trabaja con proyectos en los cuales se presentan problemas o incidencias representadas por la entidad Issue; la entidad Issue está compuesta por los siguientes campos: un identificador, un nombre de proyecto, un tipo, una prioridad y una descripción del problema. La clase pretende definir lo siguiente: para un proyecto determinado, se quiere determinar si una entidad Issue es de tipo project y si es importante o no. Un elemento Issue es importante si es de tipo tiene valor “BUG” y su prioridad es Critical. El snippet del código es el siguiente:

data class Issue(
  val id:String,
  val project: String,
  val type: String,
  val priority: String,
  val description: String
)
class ImportantIssuesPredicative(val project: String): (Issue) -> Boolean{
  override fun invoke(p1: Issue): Boolean {
    return p1.project == project && p1.isImportant()
  }
  private fun Issue.isImportant(): Boolean{
    return this.type == "BUG" && this.priority == "Critical"
  }
}
val issue1 = Issue(id = "1", project = "project1", type = "PBI", priority = "Medium", description = "description1")
val issue2 = Issue(id = "2", project = "project2", type = "PBI", priority = "High", description = "description1")
val issue3 = Issue(id = "3", project = "project1", type = "BUG", priority = "Critical", description = "description1")
val listIssues = listOf(issue1, issue2, issue3)
val predicate = ImportantIssuesPredicative("project1")
val result = listIssues.filter(predicate)
println("Result->${result}")

La definición y la instancia del predicado se realiza de la misma forma que en los ejemplos anteriores, residiendo la diferencia en la funcionalidad que se añade en el mótodo invoke; en ella, se utiliza una extensión de la clase String definida en la misma clase.

La salida por consola es la siguiente:

Result->[Issue(id=3, project=project1, type=BUG, priority=Critical, description=description1)]

Para finalizar, destacar el parecido con las funciones first class de Scala y la verbosidad de código Kotlin en comparación a Scala.

Al lector interesado, puede acceder al código completo del ejemplo en el siguiente enlace.

Azure Function en Python

La plataforma cloud de Azure ofrece un servicio para definir funciones, el servicio se llama “Function App”. Las funciones permiten definir funciones para la construcción de arquitecturas Serverless. Las funciones pueden ser definidas en distintos lenguajes, como pueden ser: Java, Python, F#, C#,… En la presente entrada, Azure Function en Python, me centraré en cómo crear funciones Python y su despliegue en Azure.

La estructura de la entrada es la siguiente:

  1. Instalación de Azure Functions Tools
    • Creación del proyecto
    • Creación de una función
    • Ficheros de configuración
  2. Función Functionbase
  3. Función Functionstore
  4. Ejecución de las funciones en local
  5. Publicación en Azure

Azure no proporciona un intarfaz en donde se puede desarrollar una función, es necesario  crear un entorno con unas herramientas determinadas, desarrollar la función y, una vez creada, se realiza el despliegue a la plataforma.

Para definir funciones en Azure es necesario realizar las siguientes tareas en un entorno local:

  1. Instalar el paquete Azure Functions Tools.
  2. Desarrollar en local la función.
  3. Desplegar el código en Azure.

1.- Instalación de Azure Functions Tools.

Azure Functions Tools es un conjunto de herramientas para el desarrollo de funciones en Python. En mi caso, al ser un entorno Linux, necesito ejecutar la siguiente secuencia de comandos:

curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
sudo apt-get update
sudo apt-get install azure-functions-core-tools
sudo apt-get install python3-venv

La versión de Python es la versión 3.6 y, para asegurar la creación de entornos virtuales, he ejecutado el comando de instalación del paquete python3-venv.

Para asegurar que el paquete se ha instalado correctamente, podemos ejecutar en la línea de comandos el comando func -h; el resultado, es la visualización de la ayuda del comando.

Creación del proyecto

La creación de un proyecto consiste en la creación de una estructura de directorios con los elementos necesarios para poder trabajar. El comando de creación del proyecto es el
siguiente:

func init MyFunctionProj

Una vez ejecutado, se tendrá que seleccionar el lenguaje del proyecto, en nuestro caso, opción 3 Python y se creará un directorio con nombre MyFunctionProj con los elementos necesarios para poder trabajar. El contenido de la carpeta será el siguiente:

  • host.json.- Fichero JSON de configuración de extensiones y confuguración.
  • local.settings.json.- Fichero con la configuración de acceso a los servicios Azure.
  • requirements.txt.- Fichero con las dependencias de Python.

Creación de una función

Para realizar la creación de una función, desde la línea de comandos, nos ubicaremos en la carpeta raíz del proyecto; y, una vez allí, ejecutamos el siguiente comando:

func new

El comando nos obliga a seleccionar una plantilla de función entre las nueve existentes las cuáles son:

Select a template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Azure Event Grid trigger
4. Azure Event Hub trigger
5. HTTP trigger
6. Azure Queue Storage trigger
7. Azure Service Bus Queue trigger
8. Azure Service Bus Topic trigger
9. Timer trigger
Choose option:

Llegado a este punto es necesario realizar una parada para definir el significado del concepto de trigger. Un trigger es aquel elemento que permite desencadenar el funcionamiento de una función; un ejemplo de trigger, es una petición HTTP, es decir, cuando se realiza una petición HTTP se inicia la ejecución de una función; otro tipo de trigger puede ser Blob Storage Trigger, es aquel desencadenador que ejecuta una función por la existencia de un fichero en un blob determinado. De los posibles desencadenadores tenemos los siguiente tipos: HTTP, Cosmos DB, Blob Storage, un Timer, Event Grid, Event Hub, Service Bus Queue, Service Bus topic y Queue Storage. Una vez seleccionado el tipo, se deberá de informar del nombre de la función.

Los ejemplos que describiré son dos funciones:

  1. FunctionBase.- Función sencilla con desencadenador HTTP
  2. FunctionStore.- Función con desencadenador HTTP, un Blob Storage de entrada y otro de salida para realizar lecturas y escrituras en un Store de Azure.

La creación de cada función realiza la creación de un directorio con dos ficheros los cuáles son los siguientes:

  • __init__.py.- Fichero con el código Python de la función.
  • function.json.- Fichero con la configuración de la función: definición del desencadenador binding, route, nombre de la función a ejecutar en el fichero __init__.py

La estructura del proyecto con las dos funciones y los ficheros de configuración tiene un aspecto como el siguiente:

__app__
| - FunctionBase
| | - __init__.py
| | - function.json
| - FunctionStore
| | - __init__.py
| | - function.json
| - host.json
| - local.settings.json
| - requirements.txt
tests

Ficheros de configuración

  • host.json.- Fichero con la configuración de las funciones. El contenido es el siguiente:
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}
  • local.settings.json.- Fichero con las conexiones a los servicios de Azure.
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsStorage": "<CONNECTION_STRING>",
    "FUNCTIONS_EXTENSION_VERSION": "~2",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "a66ab777-1fa1-222c-3333-4e44d4c4444f"
  },
  "ConnectionStrings": {}
}
  • requirements.txt.- Definición de las dependencias de las librerías Python.
azure-functions
azure-functions-worker
azure-storage-blob
azure-cosmos
azure-storage
azure-storage-blob
pillow>=6.2.0

Para ejecutar el entorno virtual local desde la carpeta del proyecto en una consola, ejecutamos la siguiente secuencia de comandos:

virtualenv .venv -p python3
source .venv/bin/activate
pip install -r requirements.txt

2.- Función Functionbase

La función FunctionBase es una función sencilla que realiza la definición de un desencadenador de tipo HTTP y, su salida, es una respuesta HTTP. Realiza la lectura de petición HTTP y responde en función de los parámetros de entrada.

El contenido del fichero function.json es el siguiente:

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

Del snippet anterior hay que destacar lo siguiente:

  • El elemento “scriptFile” define el fichero con el código de la función.
  • El elemento “type” define el tipo de elemento, en nuestro caso, trigger de tipo HTTP o HTTP.
  • El elemento “direction” determina si es de entrada o de salida.
  • El elemento “name” identifica la referencia en el código.
  • El elemento “methods” identifica los métodos HTTP que define la función, en nuestro caso, GET o POST.

El contenido del fichero __init__.py es el siguiente:

import logging
import azure.functions as func
from datetime import datetime
def main(req: func.HttpRequest) -> func.HttpResponse:
  """
  + curl -v -w '\n' -d '{"name":"pp"}' -H 'DateData: 2019/11/26' -H 'Content-Type: application/json' -X POST http://localhost:7071/api/FunctionBase
  + curl -v -w '\n' -d '{"name":"pp"}' -H 'DateData: 2019/11/26' -H 'Content-Type: application/json' -X POST https://<URL-AZURE>.net/api/FunctionBase?code=<FUNCTION_KEY>
  """
  logging.info('Python HTTP trigger function processed a request.')
  datetime_object = datetime.strptime(req.headers.get('DateData'), '%Y/%m/%d')
  name = req.params.get('name')
  if not name:
    try:
      req_body = req.get_json()
    except ValueError:
      pass
  else:
    name = req_body.get('name')
  if name:
    return func.HttpResponse('{ "test": "' + name + '", "date": "' + str(datetime_object) + '"}')
  else:
    return func.HttpResponse(
      "Please pass a name on the query string or in the request body", status_code=400)

El código define una función main que recibe la referencia de la petición HTTP, realiza un procesamiento conforme a los parámetros de entrada y, por último, crea la respuesta.

3.- Función  Functionstore

La función FunctioStore realiza el copiado de un fichero en un Store de Azure cuyo nombre es pasado en una petición HTTP.

El contenido del fichero function.json es el siguiente:

{
  "disabled": false,
  "scriptFile": "__init__.py",
  "bindings": [
  {
    "authLevel": "function",
    "type": "httpTrigger",
    "direction": "in",
    "name": "request",
    "methods": [
      "get",
      "post"
    ],
    "route": "FunctionStore/{name:alpha?}"
  },
  {
    "name": "inputblob",
    "type": "blob",
    "path": "staging/{name}.png",
    "connection": "AzureWebJobsStorage",
    "direction": "in"
  },
  {
    "name": "blobout",
    "type": "blob",
    "direction": "out",
    "path": "staging/{name}-copy.txt",
    "connection": "AzureWebJobsStorage"
  },
  {
    "type": "http",
    "direction": "out",
    "name": "$return"
  }
 ]
}

En el código anterior y partiendo de la primera función, se añaden los dos blob de lectura y escritura, respectivamente, inputblob y outputblob. El elemento “path”, corresponde con el path de la ubicación de los ficheros; el elemento “type”, corresponde con el tipo blob; el elemento “connection”, corresponde con la referencia a la conexión del blob; y, un detalle importante es el elemento “route”, define el nombre del fichero pasado en la URL de la petición y dicho parámetro es usado en los blob de entrada y salida para referenciar el nombre del fichero de lectura y  de escritura.

El contenido del fichero con el código Python es el siguiente:

import logging
import azure.functions as func
from datetime import datetime
def main(request: func.HttpRequest, inputblob: func.InputStream, blobout: func.Out[func.InputStream], context: func.Context) -> func.HttpResponse:
  """
  curl -v -w '\n' -H 'DateData: 2019/11/26' -d '{ "param1": "value1", "param2": "value2" }' -X GET http://localhost:7071/api/FunctionStore/watermark?name=11
  """
  logging.info('Python HTTP trigger function processed a request.')
  logging.info(f'context.function_directory={context.function_directory}.')
  logging.info(f'context.function_name={context.function_name}.')
  logging.info(f'context.invocation_id={context.invocation_id}.')
  logging.info(f"Params: {request.params}")
  logging.info(f"Route Params: {request.route_params}")
  logging.info(f"Body: {request.get_body()}")
  logging.info(f"Headers: {request.headers}")
  logging.info(f"Headers: {request.headers.get('DateData')}")
  datetime_object = datetime.strptime(request.headers.get('DateData'), '%Y/%m/%d')
  logging.info(f"Fecha: {datetime_object}")
  name = datetime_object
  if not name:
    try:
      req_body = request.get_json()
    except ValueError:
      pass
  else:
    name = req_body.get('name')
  blobout.set(inputblob)
  if name:
    return func.HttpResponse(f"Hello {name}!")
  else:
    return func.HttpResponse(
      "Please pass a name on the query string or in the request body",status_code=400)

Del código anterior, destaca la siguiente línea: blobout.set(inputblob) ; ésta línea, realiza la lectura del fichero definido en inputblob y su copiado en blobout. El resto del código es la visualización de los parámetros de la petición HTTP y su tratamiento.

4.- Ejecución de las funciones en local

Para ejecutar las funciones en un entorno local es necesario ejecutar desde la carpeta del proyecto el siguiente comando:

func host start

Para ejecutar las funciones ejecutamos el comando curl desde la línea de comandos. Respectivamente, para la función base y Store, ejecutamos los siguientes comandos:

curl -v -w '\n' -d '{"name":"pp"}' -H 'DateData: 2019/11/26' -H 'Content-Type: application/json' -X POST http://localhost:7071/api/FunctionBase
curl -v -w '\n' -H 'DateData: 2019/11/26' -d '{ "param1": "value1", "param2": "value2" }' -X GET http://localhost:7071/api/FunctionStore/watermark?name=11

5.- Publicación en Azure

Para realizar el despliegue, es necesario crear una función en Azure. El código Python será el código del proyecto. El proceso de creación es sencillo desde el portal de Azure,  simplemente, hay que seleccionar el grupo de recurso, lenguaje, nombre,…

Una vez que la tengamos creada, desde la línea de comando realizaremos la operación de login con el comando “az login” y, una vez logueado, ejecutamos el comando de despliegue el cuál es el siguiente:func azure functionapp publish <APP_NAME>. Para nuestro caso, si la Function App tiene nombre MyFunctionProj el comando es el siguiente:

func azure functionapp publish MyFunctionProj

Para realizar las pruebas, se puede utilizar el comando curl, Postman o el propio interfaz de pruebas que proporciona Azure.

En la siguiente imagen, realizo un resumen de la entrada de los apartados anteriores representados como un sketchnoting.

 

Si el lector está interesado en el código, lo puede encontrar en el siguiente enlace.

Inyección de dependencias en programación funcional III. Mónada Reader

Llegamos a la la última entrada de la sería de inyección de dependencias con la entrada, Inyección de dependencias en programación funcional III. Mónada Reader. El objetivo de la misma es mostrar al lector cómo se realiza la inyección de dependencias con la mónada Reader en lenguaje Scala. Para el lector interesado, las entradas de la serie son las siguientes:

La diferencia conceptual respecto a las otras dos es el uso de la mónada Reader. La mónada Reader es aquella mónada la cual puede leer un determinado componente; en dicho  componente, es donde definimos los elementos con las referencias de las funciones a inyectar. Así, necesitamos definir un elemento, en nuestro caso una case class, con las referencias a las funciones las cuáles están definidas en los componentes. Por otro lado, el servicio de negocio lo definimos a partir de un trait con un constructor de tipos.

Desde un punto de vista gráfico, la vista estática de los componentes queda definida como sigue:

Los tipos utilizados son los siguientes:

import cats.data.Reader
import cats.syntax.either._
import scala.language.higherKinds
object typesEjem3{
  type MensajeError = String
  type GetComponent1 = (String) => Either[MensajeError, String]
  type GetComponent2 = (Int) => Either[MensajeError, Int]
  type ResponseService = Either[MensajeError, String]
  type ParameterString = String
  type ParameterInt = Int
  type ServiceOperation[A] = Reader[ServiceContext, A]
  case class ServiceContext( funcComponent1: GetComponent1, funcComponent2: GetComponent2 )
}

La definición de los componentes de negocio del ejemplo son los representados por el objeto Component1Ejem3 y Component2Ejem3. El snippet del código de los componentes es el siguiente:

object Component1Ejem3{
  import typesEjem3._
  val response1: MensajeError = "Error en Response1"
  val doSomething: GetComponent1 = (elem: String) => {
    elem.length match {
      case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
      case _ => response1.asLeft
    }
  }
}
object Component2Ejem3{
  import typesEjem3._
  val response2: MensajeError = "Error en Response2"
  val doSomething: GetComponent2 = (num: Int) => {
    num match {
      case elem: Int if elem > 0 => elem.asRight
      case _ => response2.asLeft
    }
  }
}

La definición del servicio de negocio se realiza con un type class empleando un trait Service3 y el objeto ServiceImpl. Para el lector interesado en conocer lo que es un Type Class en los siguientes enlaces describo cómo se define y describe dicho patrón. Los enlaces son los siguientes:

El snippet del código del servicio es el siguiente:

trait Service3[ F[_] ]{
  def doBusiness(msg: typesEjem3.ParameterString): F[ Either[typesEjem3.MensajeError, String] ]
}
object ServiceImpl extends Service3[typesEjem3.ServiceOperation]{
  override def doBusiness(msg: typesEjem3.ParameterString): typesEjem3.ServiceOperation[Either[typesEjem3.MensajeError, String]] = Reader{ ctx =>
    for{
      response1 <- ctx.funcComponent1(msg).right
      response2 <- ctx.funcComponent2(msg.length).right
    }yield{
      response1 + "-" + response2
    }
  }
}

Como se muestra en el snippet anterior la función doBusiness del objeto ServiceImpl define la funcionalidad del servicio y es donde se utiliza la mónada Reader. La Mónada Reader se define de la siguiente manera : Reader[ServiceContext, A]; siendo la entrada de tipo ServiceContext; y, como salida, el tipo A el cual en nuestro caso es de tipo Either. Analizando la función, el objeto de entrada es de tipo ServiceContext con las referencias a los componentes que se inyectan y, como resultado, se retorna un elemento de tipo Either.

La aplicación que usa los anteriores elementos es la siguiente:

object Ejem3DependencyInyector extends App{
  import typesEjem3._
  def ejemplo1(): Unit = {
    val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
    val message1 = "Mensaje de prueba"
    ServiceImpl.doBusiness(message1).run(context) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    println
  }
  ejemplo1()
}

En la aplicación anterior, se muestra cómo usar un servicio con una mónada Reader: lo primero, es definir una clase ServiceContext con las funciones de los componentes; segunda, crear e invocar la clase con la mónada usando la función run; y, para finalizar, tratar el resultado con un pattern matching.

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17

La definición de los test del servicio de negocio descrito en el ejemplo es el siguiente:

import cats.syntax.all._
import es.ams.dependencyinyector.typesEjem3.{ GetComponent1, GetComponent2, ServiceContext} //
import org.scalatest.{Matchers, WordSpec}
class Ejem3DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
    "Example OK: mock component1" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val context = ServiceContext(funcGetResponse1Mock, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-6"))
    }
    "Example OK: mock component2" in {
      val funcComponent2: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(Component1Ejem3.doSomething,funcComponent2)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
    }
    "Example OK: mock component1 and mock component2" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(funcGetResponse1Mock, funcGetResponse2Mock)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-0"))
    }
  }
}

La inyección de dependencias desde un punto de vista funcional sigue la misma filosofía que la inyección de dependencias de objetos. La primera consecuencia es la desaparición de la utilización de framework de Mock necesarios en otros paradigmas como el utilizado en los lenguajes Java o Python. La utilización del paradigma funcional permite la composición de elementos más intuitiva aunque, evidentemente, la curva de aprendizaje es mayor.

Inyección de dependencias en programación funcional II

En la entrada anterior, Inyección de dependencias en programación funcional I, realicé la descripción de cómo se realizaba la inyección de funciones en programación funcional en lenguaje Scala; en la presente entrada, Inyección de dependencias en programación funcional II, modularizaré el código existente en la primera entrada organizando el código con una perspectiva orientada a objetos sin perder el aspecto funcional.

La vista estática del problema es la definida en el diagrama de clases de la siguiente imagen:

 

Los tipos utilizados en el ejemplo son los siguientes:

import cats.syntax.either._
object typesEjem2{
  type MensajeError = String
  type GetComponent1 = (String) => Either[MensajeError, String]
  type GetComponent2 = (Int) => Either[MensajeError, Int]
  type ResponseService = Either[MensajeError, String]
  type ParameterString = String
  type ParameterInt = Int
  type BusinessService = (GetComponent1, GetComponent2) => ParameterString => ResponseService
}

La definición de los componentes de negocio del ejemplo son los representados por los objetos Component1 y Component2. Respecto al ejemplo de la entrada anterior, se han definido las funciones dentro de un objeto con lo cual modularizamos la funcionalidad. El snippet del código de los componentes es el siguiente:

object Component1{
  import typesEjem2._
  val response1: MensajeError = "Error en Response1"
  val doSomething: GetComponent1 = (elem: String) => {
    elem.length match {
      case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
      case _ => response1.asLeft
    }
  }
}
object Component2{
  import typesEjem2._
  val response2: MensajeError = "Error en Response2"
  val doSomething: GetComponent2 = (num: Int) => {
    num match {
      case elem: Int if elem > 0 => elem.asRight
      case _ => response2.asLeft
    }
  }
}

La definición del servicio de negocio del ejemplo es el definido por el objeto Service. La estrategia de modularización es la misma que con los componentes. El snippet del código del servicio es el siguiente:

object Service{
  import typesEjem2._
  val doBusinessActivity: BusinessService = (objComp1, objComp2) => (msg) => {
    for {
      respon1 <- objComp1 (msg)
      respon2 <- objComp2(msg.length)
    } yield {
      respon1 + "-" + respon2
    }
  }
}

La aplicación de ejemplo que usa los anteriores elementos es la siguiente:

object Ejem2DependencyInyectorApp extends App {
  def ejemplo1(): Unit = {
    val message1 = "Mensaje de prueba"
    Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(message1) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    val message2 = ""
    Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(message2) match {
      case Right(msg) => println(s"Test2=${msg}")
      case Left(error) => println(error)
    }
  }
  ejemplo1()
}

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17
Error en Response1

La definición de los test del servicio de negocio descrito en el ejemplo es el siguiente:

import org.scalatest.{Matchers, WordSpec}
import es.ams.dependencyinyector.typesEjem2.{GetComponent1, GetComponent2}
import cats.syntax.all._
class Ejem2DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val msg: String = "prueba"
      val result: String = Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(msg) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
  "Example OK: mock component1" in {
    val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
    val msg: String = "prueba"
    val result: String = Service.doBusinessActivity(funcGetResponse1Mock, Component2.doSomething)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
    assert(result.equals("mock-6"))
  }
  "Example OK: mock component2" in {
    val funcComponent2: GetComponent2 = (num: Int) => 0.asRight
    val msg: String = "prueba"
    val result: String = Service.doBusinessActivity(Component1.doSomething, funcComponent2)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
  }
  "Example OK: mock component1 and mock component2" in {
    val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
    val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
    val msg: String = "prueba"
    val result: String =Service.doBusinessActivity(funcGetResponse1Mock, funcGetResponse2Mock)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
    assert(result.equals("mock-0"))
    }
  }
}

En esta entrada he realizado la modularización del código definido en la entrada, Inyección de dependencias en programación funcional I; en la siguiente entrada, subiré el nivel de abstracción y describiré el mismo problema utilizando la mónada Reader.

Inyección de dependencias en programación funcional I

La inyección de dependencias es un patrón que en otros paradigmas y lenguajes es un patrón muy utilizado; por ejemplo en Java, el framework Spring, se basa en el patrón de inyección de dependencias de objetos. En la programación funcional, la inyección de dependencias se realiza inyectando funciones, no objetos. En la presente entrada, Inyección de dependencias en programación funcional I, describiré la forma de inyectar funciones en lenguja Scala.

Supongamos que tenemos dos funciones que implementan la siguiente funcionalidad: la primera, dada un valor de tipo String de entrada, realiza la transformación de dicho parámetro concatenándole el valor “modificado”; la segunda función, dado un valor entero de entrada si es mayor a cero retorna dicho valor; en otro caso para las dos funciones, retorna un mensaje de error. El valor de retorno es un contenedor binario de tipo Either.

Por otro lado, definimos una función servicio que realiza una operación de negocio la cual utiliza las dos funciones anteriores descritas previamente. A esta función, para realizar su funcionalidad, necesitará que se le inyecten las funciones.

El objetivo del ejemplo es entender la inyección, no en definir una solución a un problema complejo.

Definición de tipos

Los tipos GetComponent1 y GetComponent2 definen las funciones básicas; el tipo Service, define la función de negocio; ResponseService, define el contenedor binario de respuesta del servicio; y, los tipos Parameter y MensajeError, otros tipos básicos necesarios.

En el siguiente snippet se define la definición en lenguaje Scala.

type MensajeError = String
type GetComponent1 = (String) => Either[MensajeError, String]
type GetComponent2 = (Int) => Either[MensajeError, Int]
type ResponseService = Either[MensajeError, String]
type Parameter = String
type Service = (GetComponent1, GetComponent2) => (Parameter) => (ResponseService)

Definición de componentes

Teniendo la definición de tipos, necesitamos la implementación de las funciones. La primera función, definida con el tipo GetComponent1, define una función cuyo parámetro de entrada es de tipo String, si el parámetro de entrada es un string cuya longitud es mayor a 0, retorna un elemento Right del tipo Either con la concatenación de la propia cadena de entrada y la palabra ” modificado”.

La segunda función, definida con el tipo GetComponent2, define una función cuyo parámetro de entrada es de tipo entero, si el valor de entrada es mayor a cero, retorna el mismo valor,en otro caso, retorno un elemento Right de tipo entero con el valor de entrada.

El código con la definición de las funciones es la siguiente:

val response1: MensajeError = "Error en Response1"
val funcGetResponse1: GetComponent1 = (elem: String) => {
  elem.length match {
    case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
    case _ => response1.asLeft
  }
}
val response2: MensajeError = "Error en Response2"
val funcGetResponse2: GetComponent2 = (num: Int) => {
  num match {
    case elem: Int if elem > 0 => elem.asRight
    case _ => response2.asLeft
  }
}

Definición del servicio

La definición de la función de servicio tiene un formado tipo Curry. Los primeros parámetros corresponde con las funciones de los tipo GetComponent1 y 2; el segundo grupo, corresponde con el parámetro que se empleará en las función; y, por último, se define la funcionalidad propiamente de negocio; en esta última parte, es donde se define la funcionalidad de negocio con las funciones inyectadas y los parámetros, así como, el resultado parcial, si fuera necesario, de las funciones.

El código con la definición de la función de servicio es la siguiente:

val funcService: Service = (getComponent1, getComponent2) => (msg) => {
  for {
    respon1 <- getComponent1(msg)
    respon2 <- getComponent2(msg.length)
  } yield {
    respon1 + "-" + respon2
  }
}

La función del servicio, funcService, recibe por parámetro aquellas funciones que le son necesarias, es decir, se le inyecta los elementos necesarios para realizar su operativa.

Dados las descripciones de las funciones de los apartados anteriores, una ejemplo básico de uso de la función servicio con la inyección de funciones es el siguiente:

object Ejem1DependecyInyectorApp extends App {
  import Ejem1DependecyInyector._
  def ejemplo1(): Unit = {
    val message1 = "Mensaje de prueba"
    funcService(funcGetResponse1, funcGetResponse2)(message1) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    val message2 = ""
    funcService(funcGetResponse1, funcGetResponse2)(message2) match {
      case Right(msg) => println(s"Test2=${msg}")
      case Left(error) => println(error)
    }
  }
  ejemplo1()
}

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17
Error en Response1

Test

Una tarea fundamental en el desarrollo del software es la realización de pruebas unitarias de aquellos componentes realizados. En nuestro caso, las pruebas unitarias de la función servicio dependerán de los resultado de las funciones inyectadas;y, para sus pruebas, es necesario moquear aquellas funciones inyectadas. En programación función con el patrón que presento, las pruebas se simplifican porque no hay que utilizar un framework específico, simplemente, necesitamos definir una función con un determinado valor la cual se inyecta a la función servicio.

Los test unitarios de la función servicio presentada son los siguientes:

class Ejem1DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val msg: String = "prueba"
      val result: String = funcService(funcGetResponse1, funcGetResponse2)(msg) match {
         case Right(msg) => { println(msg); msg}
         case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
    "Example OK: mock component1" in {
       val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
       val msg: String = "prueba"
       val result: String = funcService(funcGetResponse1, funcGetResponse2Mock )(msg) match {
          case Right(msg) => { println(msg); msg}
          case Left(error) => error
       }
       assert(result.length > 0)
    }
    "Example OK: mock component2" in {
       val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
       val msg: String = "prueba"
       val result: String = funcService(funcGetResponse1Mock, funcGetResponse2 )(msg) match {
          case Right(msg) => { println(msg); msg}
          case Left(error) => error
       }
       assert(result.length > 0)
       assert(result.equals("mock-6"))
    }
    "Example OK: mock component1 and mock component2" in {
       val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
       val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
       val msg: String = "prueba"
       val result: String = funcService(funcGetResponse1Mock, funcGetResponse2Mock )(msg) match {
          case Right(msg) => { println(msg); msg}
          case Left(error) => error
       }
       assert(result.length > 0)
       assert(result.equals("mock-0"))
    }
  }
}

En las próximas entregas, continuaré profundizando en la inyección de dependencias.

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.