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.

Creación de un proyecto Kotlin con Gradle

Las herramientas de gestión de ciclo de vida difieren en cada lenguaje, o bien, hay herramientas que soportan la gestión para diferentes lenguajes; por ejemplo: sbt, permite la creación y gestión de proyectos en Scala y Java; maven, permite la creación de proyectos Java, Scala o Kotlin; y, Gradle, permite la creación y gestión de proyectos en Scala, Java o Kotlin. En la entrada de hoy, Creación de un proyecto Kotlin con Gradle, me centraré en la definición de un proyecto base en lenguaje Kotlin con Gradle.

1.- Gradle

Gradle es una herramienta open source para la automatización y gestión de ciclo de vida software con el cual podemos definir script en lenguaje Groovy o con un DSL Kotlin. Es una herramienta flexible, rápida, customizable y permite la gestión de proyectos de distinto lenguajes, como pueden ser: Java, Android, Scala, Kootlin,…

Para el lector interesado, la versión actual es la 5.4.1 y la la referencia a la documentación es la siguiente.

1.1.- Instalación de Gradle

El proceso de instalación es un proceso característico en función del entorno de trabajo, ya sea Linux, Windows o Mac. La referencia documental de instalación es la siguiente. 

En mi caso, el entorno de trabajo es sobre sistema Linux, en el cual el proceso de instalación consiste en lo siguiente: realizar la descarga de Gradle, descompresión del fichero descargado sobre un carpeta; y, por último, definir la variable de entorno. Para confirmar la instalación, se ejecuta el siguiente comando en una terminal:

gradle -v

La salida del comando tiene el siguiente aspecto:

 ------------------------------------------------------------
Gradle 5.4.1
------------------------------------------------------------

Build time: 2019-04-26 08:14:42 UTC
Revision: 261d171646b36a6a28d5a19a69676cd098a4c19d

Kotlin: 1.3.21
Groovy: 2.5.4
Ant: Apache Ant(TM) version 1.9.13 compiled on July 10 2018
JVM: 1.8.0_201 (Oracle Corporation 25.201-b09)
OS: Linux 4.6.7-040607-generic amd64

 

2.- Creación del proyecto

Una vez que tenemos Gradle instalado en la máquina, el proceso de creación del proyecto es sencillo y parecido a otras herramientas.

El proceso se define en los siguientes pasos:

  1. Creación de una carpeta del proyecto. La creación del directorio lo realizamos creando desde la línea de comandos una carpeta; en el presente caso, se ejecuta el siguiente comando: mkdir ejem2-kotlin. Una vez ejecutado, nos situamos en el interior de la carpeta mediante el comando cd ejem2-kotlin.
  2. Inicialización del proyecto. La inicialización del proyecto consiste en aquel proceso en el cual se realiza la creación de la estructura del proyecto. Dicho proceso, se realiza ejecutando el siguiente comando: gradle init. La consecuencia de este comando es la ejecución de un wizard en el cual se solicita una serie de cuestiones, entre ellos, selección de lenguaje (permite la creación de proyectos en Java, Korlin o Scala), nombre del proyecto y estructura de carpetas.

La estructura de carpetas del proyecto desde el visor de proyectos del IDE IntelliJ es el siguiente:

La estructura de carpetas y ficheros creados a destacar son los siguientes:

  • src.- Carpeta de código fuente con el aspecto típico, contiene la carpeta src/main/kotlin y src/test/kotlin; además, de las carpetas resources.
  • build.- carpeta con el contenido generado por el compilador kotlin.
  • out.- carpeta con el resultado de la compilación del proyecto.
  • gradle.- carpeta con los wrapper de gradle.
  • .gitignore.- fichero git.
  • build.gradle.kts.- fichero gradle con la configuración.
  • settings.gradle.kts.- fichero con propiedades.
  • gradlew.bat y gradlew.sh.- Fichero de ejecución Gradle del proyecto.

3.- Fichero de construcción build.gradle.kts

El fichero build.gradle.kts es aquel fichero en donde definimos la configuración necesaria para trabajar con el proyecto, como por ejemplo: las dependencias, repositorios y la clase principal.

La estructura del fichero está compuesta por los siguientes apartados:

  • plugins.- definición de las referencias de plugins existentes.
  • repositories.- definición de las referencias a los repositorios de artefactos.
  • dependencies.- definición de los artefactos dependientes en el proyecto.
  • application.- definición de la configuración de la aplicación, en concreto, la definición de la clase principal.

Un ejemplo de fichero build.gradle.kts para un proyecto en lenguaje Kotlin con las dependencias de la librería Arrow es la siguiente:

 import org.jetbrains.kotlin.kapt3.base.Kapt.kapt
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Kotlin application project to get you started.
*/
plugins {
  // Apply the Kotlin JVM plugin to add support for Kotlin on the JVM.
  id("org.jetbrains.kotlin.jvm").version("1.3.21")
  kotlin("kapt") version "1.3.31"
  // Apply the application plugin to add support for building a CLI application.
  application
}
repositories {
  // Use jcenter for resolving your dependencies.
  // You can declare any Maven/Ivy/file repository here.
  jcenter()
  mavenCentral()
}
val arrow_version = "0.9.0"
dependencies {
  // Use the Kotlin JDK 8 standard library.
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
  // Use the Kotlin test library.
  testImplementation("org.jetbrains.kotlin:kotlin-test")
  // Use the Kotlin JUnit integration.
  testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
  compile( "io.arrow-kt:arrow-core-data:$arrow_version")
  compile( "io.arrow-kt:arrow-core-extensions:$arrow_version" )
  compile( "io.arrow-kt:arrow-syntax:$arrow_version")
  compile( "io.arrow-kt:arrow-typeclasses:$arrow_version")
  compile( "io.arrow-kt:arrow-extras-data:$arrow_version")
  compile( "io.arrow-kt:arrow-extras-extensions:$arrow_version")
  kapt( "io.arrow-kt:arrow-meta:$arrow_version")
  compile( "io.arrow-kt:arrow-query-language:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-free-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-free-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-mtl:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-io-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-rx2-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-rx2-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-reactor-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-reactor-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-optics:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-generic:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-recursion-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-recursion-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-query-language:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version") //optional
}
application {
  // Define the main class for the application.
  mainClassName = "es.ams.AppKt"
}

4.- Comandos Gradle

Gradle es una herramienta con la cual se ejecutan tareas con una funcionalidad determinada. En el presente apartado, identificaré un conjunto de comandos para la ejecución de ciertas tareas mínimas.

Un conjunto de tareas básicas son las siguientes:

  • gradle clean.- Eliminación de la carpeta build.
  • gradle build.- Construcción de los componentes binarios del proyecto.
  • gradle tasks.- visualización de las tareas definidas en el proyecto.
  • gradle properties.- visualización de las propiedades definidas en el proyecto.
  • gradle run.– Ejecución del proyecto.
  • gradle projects.- visualización de la información del proyecto.
  • gradle tests.- Ejecución de los test definidos en el proyecto.

Si se utiliza un IDE como IntelliJ, una vez importado y configurado la ubicación de Gradle, se pueden ejecutar desde la propia herramienta. Un aspecto de la estructura, contenido de clases y dependencias es el que se representa en la siguiente imagen:

5.- Conclusiones

La entrada define aspectos básicos y genéricos de Gradle. El objetivo de la entrada es la creación de un proyecto básico de Gradle en lenguaje Kotlin que permita a una persona realizar la creación de un proyecto y empezar a trabajar en dicho entorno.

Si realizamos un proceso comparativo a alto nivel con otros herramientas, el proceso es similar y, en concreto, me ha resultado una forma de trabajo parecida a la herramienta sbt para la gestión de proyectos en lenguaje Scala.