Cierres en Groovy

1. Información general

En este tutorial introductorio, exploraremos el concepto de cierres en Groovy, una característica clave de este dinámico y poderoso lenguaje JVM.

Muchos otros lenguajes, incluidos Javascript y Python, admiten el concepto de cierres. Sin embargo, las características y el funcionamiento de los cierres varían de un idioma a otro.

Tocaremos aspectos clave de los cierres Groovy y mostraremos ejemplos de cómo se utilizan a lo largo del camino.

2. ¿Qué es un cierre?

Un cierre es un bloque de código anónimo. En Groovy, es una instancia de la clase Closure . Los cierres pueden tomar 0 o más parámetros y siempre devolver un valor.

Además, un cierre puede acceder a las variables circundantes fuera de su alcance y usarlas, junto con sus variables locales, durante la ejecución.

Además, podemos asignar un cierre a una variable o pasarlo como parámetro a un método. Por lo tanto, un cierre proporciona funcionalidad para la ejecución retrasada.

3. Declaración de cierre

Un Groovy Closure contiene parámetros, la flecha -> y el código para ejecutar. Los parámetros son opcionales y, cuando se proporcionan, están separados por comas.

3.1. Declaración básica

def printWelcome = { println "Welcome to Closures!" }

Aquí, el cierre printWelcome imprime una declaración cuando se invoca. Ahora, escriba un ejemplo rápido de un cierre unario:

def print = { name -> println name }

Aquí, la impresión de cierre toma un parámetro, el nombre , y lo imprime cuando se invoca.

Dado que la definición de un cierre se parece a un método , comparémoslos:

def formatToLowerCase(name) { return name.toLowerCase() } def formatToLowerCaseClosure = { name -> return name.toLowerCase() } 

Aquí, el método y el cierre correspondiente se comportan de manera similar. Sin embargo, existen diferencias sutiles entre un cierre y un método, que discutiremos más adelante en la sección Cierres vs Métodos.

3.2. Ejecución

Podemos ejecutar un cierre de dos formas: podemos invocarlo como si fuera cualquier otro método, o podemos utilizar el método de llamada .

Por ejemplo, como método habitual:

print("Hello! Closure") formatToLowerCaseClosure("Hello! Closure") 

Y ejecutando con el método de llamada :

print.call("Hello! Closure") formatToLowerCaseClosure.call("Hello! Closure")

4. Parámetros

Los parámetros de los cierres Groovy son similares a los de los métodos habituales.

4.1. Parámetro implícito

Podemos definir un cierre unario sin un parámetro porque cuando los parámetros no están definidos, Groovy asume un parámetro implícito llamado " eso" :

def greet = { return "Hello! ${it}" } assert greet("Alex") == "Hello! Alex"

4.2. Múltiples parámetros

Aquí hay un cierre que toma dos parámetros y devuelve el resultado de multiplicarlos:

def multiply = { x, y -> return x*y } assert multiply(2, 4) == 8

4.3. Tipos de parámetros

En los ejemplos hasta ahora, no se ha proporcionado ningún tipo con nuestros parámetros. También podemos establecer el tipo de parámetros de cierre. Por ejemplo, reescribamos el método de multiplicar para considerar otras operaciones:

def calculate = {int x, int y, String operation -> def result = 0 switch(operation) { case "ADD": result = x+y break case "SUB": result = x-y break case "MUL": result = x*y break case "DIV": result = x/y break } return result } assert calculate(12, 4, "ADD") == 16 assert calculate(43, 8, "DIV") == 5.375

4.4. Varargs

Podemos declarar un número variable de argumentos en cierres, similar a los métodos regulares. Por ejemplo:

def addAll = { int... args -> return args.sum() } assert addAll(12, 10, 14) == 36

5. Un cierre como argumento

Podemos pasar un Closure como argumento a un método Groovy normal. Esto permite que el método llame a nuestro cierre para completar su tarea, permitiéndonos personalizar su comportamiento.

Analicemos un caso de uso simple: el cálculo del volumen de cifras regulares.

En este ejemplo, el volumen se define como el área multiplicada por la altura. Sin embargo, el cálculo del área puede variar para diferentes formas.

Por lo tanto, escribiremos el método de volumen , que toma una calculadora de área de cierre como argumento, y pasaremos la implementación del cálculo de área durante la invocación:

def volume(Closure areaCalculator, int... dimensions) { if(dimensions.size() == 3) { return areaCalculator(dimensions[0], dimensions[1]) * dimensions[2] } else if(dimensions.size() == 2) { return areaCalculator(dimensions[0]) * dimensions[1] } else if(dimensions.size() == 1) { return areaCalculator(dimensions[0]) * dimensions[0] } } assert volume({ l, b -> return l*b }, 12, 6, 10) == 720 

Encontremos el volumen de un cono usando el mismo método:

assert volume({ radius -> return Math.PI*radius*radius/3 }, 5, 10) == Math.PI * 250

6. Cierres anidados

Podemos declarar e invocar cierres dentro de un cierre.

Por ejemplo, agreguemos una capacidad de registro al cierre de cálculo ya discutido :

def calculate = {int x, int y, String operation -> def log = { println "Performing $it" } def result = 0 switch(operation) { case "ADD": log("Addition") result = x+y break case "SUB": log("Subtraction") result = x-y break case "MUL": log("Multiplication") result = x*y break case "DIV": log("Division") result = x/y break } return result }

7. Evaluación diferida de cadenas

Los Groovy String se suelen evaluar e interpolar en el momento de la creación. Por ejemplo:

def name = "Samwell" def welcomeMsg = "Welcome! $name" assert welcomeMsg == "Welcome! Samwell"

Even if we modify the value of the name variable, the welcomeMsg is not going to change:

name = "Tarly" assert welcomeMsg != "Welcome! Tarly"

Closure interpolation allows us to provide lazy evaluation of Strings, recalculated from the current values around them. For example:

def fullName = "Tarly Samson" def greetStr = "Hello! ${-> fullName}" assert greetStr == "Hello! Tarly Samson"

Only this time, changing the variable affects the interpolated string's value as well:

fullName = "Jon Smith" assert greetStr == "Hello! Jon Smith"

8. Closures in Collections

Groovy Collections use closures in many of their APIs. For example, let's define a list of items and print them using the unary closure each, which has an implicit parameter:

def list = [10, 11, 12, 13, 14, true, false, "BUNTHER"] list.each { println it } assert [13, 14] == list.findAll{ it instanceof Integer && it >= 13 }

Often, based on some criterion, we may need to create a list from a map. For instance:

def map = [1:10, 2:30, 4:5] assert [10, 60, 20] == map.collect{it.key * it.value} 

9. Closures vs Methods

So far, we've seen the syntax, execution, and parameters of closures, which are fairly similar to methods. Let's now compare closures with methods.

Unlike a regular Groovy method:

  • We can pass a Closure as an argument to a method
  • Unary closures can use the implicit it parameter
  • We can assign a Closure to a variable and execute it later, either as a method or with call
  • Groovy determines the return type of the closures at runtime
  • We can declare and invoke closures inside a closure
  • Closures always return a value

Hence, closures have benefits over regular methods and are a powerful feature of Groovy.

10. Conclusion

In this article, we’ve seen how to create closures in Groovy and explored how they are used.

Los cierres proporcionan una forma eficaz de inyectar funcionalidad en objetos y métodos para retrasar la ejecución.

Como siempre, el código y las pruebas unitarias de este artículo están disponibles en GitHub.