En este artículo se mostrarán los conocimientos de Kotlin necesarios para el desarrollo de un DSL en este lenguaje. Si para ti es «hace 3 años que no se me muere una planta» estos conceptos no dejes de leerlo ya que nunca está de más un repasito o una lectura rápida. Si aún así consideras que no te hace falta puedes avanzar directamente al artículo Kotlin DSL: A Stardew valley story II
, donde aplicaremos estos conocimientos para mostrar la información de nuestra granja. Y si eres de las personas que se les muere hasta los cactus, no te preocupes, esta sencilla explicación te podrá ayudar a entenderlo mejor. Se usarán ejemplos cogiendo de base el mundo del juego de Stardew Valley.
Conceptos básicos de Kotlin para el desarrollo de un DSL
¿Qué es un DSL?
DSL es el acrónimo de «Domain Specific Language» es un lenguaje de programación específico para un dominio concreto. Es decir, un DSL es un lenguaje de programación diseñado para resolver problemas en un ámbito particular, como el desarrollo web (HTML), la automatización de tareas, etc. Los DSLs te ayudan a simplificar la escritura y la legibilidad del código.
Por otro lado, GSL (General Purpose Language) es un lenguaje de programación general que se utiliza para resolver problemas en una amplia variedad de dominios. Probablemente cualquiera de los lenguajes de programación que usas en tu día a día Java, Python o Kotlin.
Ahora, quizás, estarás pensando por qué me estás contando todo esto y qué puedo hacer con ello. No te preocupes que te lo explico y no pretendas plantar una chirivía cuando todavía estás a día 27 de invierno. Muy sencillo, los DSLs se pueden implementar en GSLs, como en Kotlin, para definir acciones (dominios específicos) o configuraciones de forma más sencilla (builds scripts de Gradle y dejar de lado a Groovy) y legible.
En el caso concreto de Kotlin, un DSL puede ser utilizado para definir acciones o configuraciones de forma más sencilla y legible. Por ejemplo, un DSL para definir acciones en un juego podría tener una sintaxis similar a la siguiente:
character {
player {
name = "Alice"
level = 10
live = 9
damage = 3
}
enemy {
name = "Dragon"
level = 20
live = 15
damage = 5
}
}
Seguramente esta estructura que acabamos de ver en el ejemplo te suene de algo, ¿verdad? Pues sí, es muy similar a la estructura de un archivo XML o JSON. Y es que los DSLs se pueden implementar de diferentes maneras como, por ejemplo, utilizando funciones de ámbito, funciones de extensión, lambdas con receptor, entre otros. Pero no adelantemos acontecimientos.
Entonces, centrándonos en la materia y asumiendo que en este punto ya conocemos la programación orientada a objetos, para crear nuestro DSL en Kotlin será necesario tener estos conocimientos:
- Lambdas y lambdas con receptor
- Funciones de orden superior, de extensión y ámbito
- Patrón de diseño Builder
Y si además queremos hacerlo más idiomático y eficiente es recomendable conocer:
- Notaciones infix
- Sobrecarga de operadores
- Funciones inline
- Patrón (o anti-patrón) de diseño Singleton
Lambdas
Las lambdas son funciones anónimas que pueden ser pasadas como argumentos a otras funciones. En términos generales, una lambda se centra en describir un algoritmo, comportamiento, acción o procedimiento. No es más que el cuerpo de una función tradicional que hemos hecho miles de veces.
En conclusión, una lambda puede definirse como un proceso que, a partir de determinadas entradas, produce un resultado.
La estructura de las lambdas en Kotlin se definen entre llaves {}
y se pueden asignar a variables.
val energyProvider: (String) -> String = { element -> "$element provides energy!" }
fun main() {
val result = energyProvider("Barn")
println(result)
}
// Result: Barn provides energy!
Y en el caso de que la lambda tenga más de un parámetro, se definirán entre paréntesis ()
. ¿Se pueden tener 20 parámetros? Sí, pero más no significa mejor. Tanto aquí como cuando desarrollamos en cualquier lenguaje, la legibilidad y mantenibilidad del código es lo más importante.
val energyProvider: (String, Int) -> String = { element, value -> "$element provides $value energy!" }
fun main() {
val result = energyProvider("Barn", 10)
println(result)
}
// Result: Barn provides 10 energy!
Funciones de extensión
Las funciones de extensión permiten añadir nuevas funciones a una clase sin modificar su código fuente. Una función de extensión es una función común cuyo nombre lleva como prefijo el nombre de la clase que se quiere extender, conectados ambos con un punto CLASS.function
.
Para Kotlin, las funciones de extensión se definen con la palabra clave fun
seguida del nombre de la clase a la que se quiere añadir la función y el cuerpo de la función fun CLASS.function
. Estas funciones se definen fuera de la clase de la que se está extendiendo, pero se pueden invocar como si fueran métodos de la instancia de la clase. Esta particularidad nos viene bien cuando queremos añadir funcionalidades a clases que no controlamos, como las clases de las librerías que usamos.
val provide: (String, Int) -> String = { element, value ->
"$element provides $value energy!"
} // Lambda que proporciona energía
val consume: (String, Int) -> String = { element, value ->
"$element consumes $value energy!"
} // Lambda que consume energía
fun String.provideEnergy(energy: Int): String {
return "$this provides $energy energy!"
} // Función de extensión para String que proporciona energía
fun String.consumeEnergy(energy: Int): String {
return "$this consumes $energy energy!"
} // Función de extensión para String que consume energía
fun main() {
val barnEnergy = "Barn".provideEnergy(50)
val fieldEnergy = "Field".consumeEnergy(20)
println("Barn Energy: $barnEnergy")
println("Field Energy: $fieldEnergy")
}
// Barn Energy: Barn provides 50 energy!
// Field Energy: Field consumes 20 energy!
Si te das cuenta, en nuestro desarrollo habitual podemos encontrar múltiples funciones de extensión, como String.toByteArray()
, String.toUpperCase()
, String.toLowerCase()
, etc. ¿Qué otras funciones de extensión conoces?
Funciones de orden superior
Las funciones de orden superior son funciones que reciben otras funciones como parámetros y pueden devolver funciones como resultado. En otras palabras, son funciones que tratan a otras funciones como si fueran datos. Es decir, estas funciones pueden declararse sin la necesidad de crear una clase que las contenga y pueden ser pasadas como argumentos a otras funciones. Esta característica permite que Kotlin pueda ser un fantástico híbrido de lenguaje ya que permite tanto la programación orientada a objetos como la programación funcional.
Otra particularidad de las funciones de orden superior es que pueden ser almacenadas en variables, pasadas como argumentos a otras funciones y devueltas como resultado de otras funciones.
fun manageEnergy(element: String, energy: Int, operation: (String, Int) -> String): String {
println("Calculating energy...")
return operation(element, energy)
} // Función de orden superior que recibe una lambda
val provide: (String, Int) -> String = { element, value ->
println("Providing energy...")
"$element provides $value energy!"
} // Lambda que proporciona energía
val consume: (String, Int) -> String = { element, value ->
println("Consuming energy...")
"$element consumes $value energy!"
} // Lambda que consume energía
fun main() {
val barnEnergy = manageEnergy("Barn", 50, provide)
val fieldEnergy = manageEnergy("Field", 20, consume)
println("Barn Energy: $barnEnergy")
println("Field Energy: $fieldEnergy")
}
// Barn Energy: Barn provides 50 energy!
// Field Energy: Field consumes 20 energy!
Y te preguntarás, ¿pero esto no se parece mucho a una lambda? Sí, pero no o no pero sí. La diferencia, una de ellas, entre una función de orden superior y una lambda es que la primera es una función que recibe o devuelve otra función, mientras que la segunda es una función anónima que puede ser pasada como argumento a otra función.
Algunos de los operadores que solemos usar cuando trabajamos con colecciones como map
, filter
, reduce
, forEach
, entre otros, son funciones de orden superior.
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
println(squaredNumbers) // [1, 4, 9, 16, 25]
Lambdas con receptor
Las lambdas con receptor son un tipo especial, como un Fruto Qi, de lambda que permiten acceder a las funciones y propiedades de un objeto receptor sin necesidad de hacer referencia explícita. Dicho de otra forma y para entender este trabalenguas, las lambdas con receptor permiten acceder a las funciones y propiedades de un objeto receptor como si fueran miembros de la lambda.
Por ejemplo, si tenemos una clase EnergyElement
con una propiedad element
y una función showStatus()
lo que podemos hacer es uso de una lambda con receptor para acceder a estas propiedades. También podemos hacer funciones sin hacer referencia explícita a la clase y sin necesidad de pasar el objeto como argumento quedando la función de la siguiente forma:
class EnergyElement(var element: String, var energy: Int) {
fun showStatus(): String {
return "$element has $energy energy!"
}
}
val manageEnergy: EnergyElement.() -> String = {
if (energy > 0) {
"$element provides $energy energy!"
} else {
"$element has no energy left!"
}
}
fun main() {
val barnEnergy = barn.manageEnergy()
val fieldEnergy = field.manageEnergy()
println("Barn Energy: $barnEnergy")
println("Field Energy: $fieldEnergy")
}
// Barn Energy: Barn provides 50 energy!
// Field Energy: Field has no energy left!
Continuando la explicación anterior al ejemplo, la lambda manageEnergy
es una lambda con receptor que se aplica a la clase EnergyElement
. Al invocar la lambda manageEnergy
sobre una instancia de EnergyElement
, se puede acceder a las propiedades y funciones de la clase EnergyElement
sin necesidad de hacer referencia explícita a ella.
En conclusión, son la fusión de lambdas con funciones con extensión -también denominado como tipo de función de extensión-, así que como ya hemos visto anteriormente estos conceptos nos será más amigable aprender este.
Referencias
- Codersee- Kotlin on the backend [@codersee]. (s/f-a). Kotlin type-safe builders explained. Implement your own DSL. Youtube. Recuperado el 4 de septiembre de 2024, de https://www.youtube.com/watch?v=g4ioA_LcBWE
- Codersee- Kotlin on the backend [@codersee]. (s/f-b). Scope control with @DslMarker annotation. Kotlin DSLs. Youtube. Recuperado el 4 de septiembre de 2024, de https://www.youtube.com/watch?v=uItQGNnbUXo
- DslMarker. (s/f). Kotlin. Recuperado el 4 de septiembre de 2024.
- Leiva, A. (2022, December 5). Builder – Patrones de Diseño. DevExpert. https://devexpert.io/builder-patrones-diseno/
- Osipov, I. (s/f). Kotlin DSL: From theory to practice. Jmix.Io. Recuperado el 4 de septiembre de 2024, de https://www.jmix.io/cuba-blog/kotlin-dsl-from-theory-to-practice/
- Sandoval, G. (2021a, mayo 31). Kotlin DSL. Kotlin En Android. https://medium.com/kotlin-en-android/kotlin-dsl-construccion-del-dsl-1-893eaf900cc
- Sandoval, G. (2021b, julio 16). Kotlin DSL. Kotlin and Kotlin for Android. https://medium.com/kotlin-and-kotlin-for-android/kotlin-dsl-coding-a-dsl-6-ee355be81106
- Type-safe builders. (n.d.). Kotlin Help. Retrieved September 4, 2024.