37 - Expresiones lambda

Una expresión lambda es cuando enviamos a una función de orden superior directamente una función anónima.

Es más común enviar una expresión lambda en lugar de enviar la referencia de una función como vimos en el concepto anterior.

Problema 1

Definir una función de orden superior llamada operar. Llegan como parámetro dos enteros y una función. En el bloque de la función llamar a la función que llega como parámetro y enviar los dos primeros parámetros.

Desde la función main llamar a operar y enviar distintas expresiones lambdas que permitan sumar, restar y elevar el primer valor al segundo .

Proyecto149 - Principal.kt

fun operar(v1: Int, v2: Int, fn: (Int, Int) -> Int) : Int{
    return fn(v1, v2)
}

fun main(parametro: Array<String>) {
    val suma = operar(2, 3, {x, y -> x + y})
    println(suma)
    val resta = operar(12, 2, {x, y -> x - y})
    println(resta)
    var elevarCuarta = operar(2, 4, {x, y ->
        var valor = 1
        for(i in 1..y)
            valor = valor * x
        valor
    })
    println(elevarCuarta)
}

La función de orden superior operar es la que vimos en el concepto anterior:

fun operar(v1: Int, v2: Int, fn: (Int, Int) -> Int) : Int{
    return fn(v1, v2)
}

La primer expresión lambda podemos identificarla en la primer llamada a la función operar:

    val suma = operar(2, 3, {x, y -> x + y})
    println(suma)

Una expresión lambda está compuesta por una serie de parámetros (en este caso x e y), el signo -> y el cuerpo de una función:

{x, y -> x + y}

Como podemos comprobar toda expresión lambda va encerrada entre llaves.

Podemos indicar el tipo de dato de los parámetros de la función pero normalmente no se los dispone:

val suma = operar(2, 3, {x: Int, y: Int -> x + y})

El nombre de los parámetros se llaman x e y, pero podrían tener cualquier nombre:

    val suma = operar(2, 3, {valor1, valor2: Int -> valor1 + valor2})

Siempre hay que indicar el tipo de dato que devuelve la función, en este caso retorna un Int que lo indicamos después de los dos puntos. Si no retorna dato la función se dispone Unit.

El algoritmo de la función lo expresamos después del signo ->

x + y

Se infiere que la suma de x e y es el valor a retornar.

La segunda llamada a operar le pasamos una expresión lambda similar a la primer llamada:

    val resta = operar(12, 2, {x, y -> x - y})
    println(resta)

La tercer llamada a operar le pasamos una expresión lambda que en su algoritmo procedemos a elevar el primer valor según el dato que llega en el segundo valor:

    var elevarCuarta = operar(2, 4, {x, y ->
        var valor = 1
        for(i in 1..y)
            valor = valor * x
        valor
    })

Dentro de la expresión lambda podemos implementar un algoritmo más complejo como es este caso donde elevamos el parámetro x al exponente indicado con y.

Es importante notar que la función de orden superior recibe como parámetro una función con dos parámetros de tipo Int y retorna un Int:

fn: (Int, Int) -> Int

La expresión lambda debe pasar una función con la misma estructura, es decir dos parámetros enteros y retornar un entero:

{x, y ->
        var valor = 1
        for(i in 1..y)
            valor = valor * x
        valor
    }

Podemos identificar los dos parámetros x e y, y el valor que devuelve es el dato indicado después del for.

Un último cambio que implementaremos a nuestro programa es que cuando una expresión lambda es el último parámetro de una función podemos indicar la expresión lambda después de los paréntesis para que sea más legible el código de nuestro programa:

fun main(parametro: Array<String>) {
    val suma = operar(2, 3) {x, y -> x + y}
    println(suma)
    val resta = operar(12, 2)  {x, y -> x - y}
    println(resta)
    var elevarCuarta = operar(2, 4) {x, y ->
        var valor = 1
        for(i in 1..y)
            valor = valor * x
        valor
    }
    println(elevarCuarta)
}

Lo más común es utilizar esta sintaxis para pasar una expresión lambda cuando es el último parámetro de una función.

Problema 2

Confeccionar una función de orden superior que reciba un arreglo de enteros y una función con un parámetro de tipo Int y que retorne un Boolean.

La función debe analizar cada elemento del arreglo llamando a la función que recibe como parámetro, si retorna un true se pasa a mostrar el elemento.

En la función main definir un arreglo de enteros de 10 elementos y almacenar valores aleatorios comprendidos entre 0 y 99.

Imprimir del arreglo:

  • Los valores múltiplos de 2
  • Los valores múltiplos de 3 o de 5
  • Los valores mayores o iguales a 50
  • Los valores comprendidos entre 1 y 10, 20 y 30, 90 y 95

Proyecto150 - Principal.kt

fun imprimirSi(arreglo: IntArray, fn:(Int) -> Boolean) {
    for(elemento in arreglo)
        if (fn(elemento))
            print("$elemento ")
    println();
}

fun main(parametro: Array<String>) {
    val arreglo1 = IntArray(10)
    for(i in arreglo1.indices)
        arreglo1[i] = ((Math.random() * 100)).toInt()
    println("Imprimir los valores múltiplos de 2")
    imprimirSi(arreglo1) {x -> x % 2 == 0}
    println("Imprimir los valores múltiplos de 3 o de 5")
    imprimirSi(arreglo1) {x -> x % 3 == 0 || x % 5 ==0}
    println("Imprimir los valores mayores o iguales a 50")
    imprimirSi(arreglo1) {x -> x >= 50}
    println("Imprimir los valores comprendidos entre 1 y 10, 20 y 30, 90 y 95")
    imprimirSi(arreglo1) {x -> when(x) {
        in 1..10 -> true
        in 20..30 -> true
        in 90..95 -> true
        else -> false
    }}
    println("Imprimir todos los valores")
    imprimirSi(arreglo1) {x -> true}
}

La función de orden superior ImprimirSi recibe un arreglo de enteros y una función:

fun imprimirSi(arreglo: IntArray, fn:(Int) -> Boolean) {

Dentro de la función recorremos el arreglo de enteros y llamamos a la función que recibe como parámetro para cada elemento del arreglo, si retorna un true pasamos a mostrar el valor:

    for(elemento in arreglo)
        if (fn(elemento))
            print("$elemento ")
    println();
}

La ventaja de la función imprimirSi es que podemos utilizarla para resolver una gran cantidad de problemas, solo debemos pasar una expresión lambda que analice cada entero que recibe.

En la función main creamos el arreglo de 10 enteros y cargamos 10 valores aleatorios comprendidos entre 0 y 99:

fun main(parametro: Array<String>) {
    val arreglo1 = IntArray(10)
    for(i in arreglo1.indices)
        arreglo1[i] = ((Math.random() * 100)).toInt()

Para imprimir los valores múltiplos de 2 nuestra expresión lambda recibe como parámetro un Int llamado x y el algoritmo que verifica si es par o no consiste en verificar si el resto de dividirlo por 2 es cero:

    println("Imprimir los valores múltiplos de 2")
    imprimirSi(arreglo1) {x -> x % 2 == 0}

Es importante entender que nuestra expresión lambda no tiene una estructura repetitiva, sino que desde la función imprimirSi llamará a esta función anónima tantas veces como elementos tenga el arreglo.

El algoritmo de la función varía si queremos identificar si el número es múltiplo de 3 o de 5:

    println("Imprimir los valores múltiplos de 3 o de 5")
    imprimirSi(arreglo1) {x -> x % 3 == 0 || x % 5 ==0}

Para imprimir todos los valores mayores o iguales a 50 debemos verificar si el parámetro x es >= 50:

    println("Imprimir los valores mayores o iguales a 50")
    imprimirSi(arreglo1) {x -> x >= 50}

Para analizar si está la componente en distintos rangos podemos utilizar la instrucción when:

    println("Imprimir los valores comprendidos entre 1 y 10, 20 y 30, 90 y 95")
    var cant = 0
    imprimirSi(arreglo1) {x -> when(x) {
        in 1..10 -> true
        in 20..30 -> true
        in 90..95 -> true
        else -> false
    }}

En forma muy sencilla si queremos imprimir todo el arreglo retornamos para cada elemento que analiza nuestra expresión lambda el valor true:

    println("Imprimir todos los valores")
    imprimirSi(arreglo1) {x -> true}

Este es un problema donde ya podemos notar las potencialidades de las funciones de orden superior y las expresiones lambdas que le pasamos a las mismas.

Acotaciones

Dijimos que uno de los principios de Kotlin es permitir escribir código conciso, para esto cuando tenemos una expresión lambda cuya función recibe un solo parámetro podemos obviarlo, inclusive el signo ->
Luego por convención ese único parámetro podemos hacer referencia al mismo con la palabra "it"

Entonces nuestro programa definitivo conviene escribirlo con la siguiente sintaxis:

Proyecto150 - Principal.kt

fun imprimirSi(arreglo: IntArray, fn:(Int) -> Boolean) {
    for(elemento in arreglo)
        if (fn(elemento))
            print("$elemento ")
    println();
}

fun main(parametro: Array<String>) {
    val arreglo1 = IntArray(10)
    for(i in arreglo1.indices)
        arreglo1[i] = ((Math.random() * 100)).toInt()
    println("Imprimir los valores múltiplos de 2")
    imprimirSi(arreglo1) {it % 2 == 0}
    println("Imprimir los valores múltiplos de 3 o de 5")
    imprimirSi(arreglo1) {it % 3 == 0 || it % 5 ==0}
    println("Imprimir los valores mayores o iguales a 50")
    imprimirSi(arreglo1) {it >= 50}
    println("Imprimir los valores comprendidos entre 1 y 10, 20 y 30, 90 y 95")
    imprimirSi(arreglo1) {when(it) {
        in 1..10 -> true
        in 20..30 -> true
        in 90..95 -> true
        else -> false
    }}
    println("Imprimir todos los valores")
    imprimirSi(arreglo1) {true}
}

Tener en cuenta que cuando llamamos desde la función imprimirSi:

        if (fn(elemento))

La expresión lambda recibe un parámetro llamado por defecto "it" y se almacena el valor pasado "elemento":

    imprimirSi(arreglo1) {it % 2 == 0}

Problema 3

Confeccionar una función de orden superior que reciba un String y una función con un parámetro de tipo Char y que retorne un Boolean.

La función debe analizar cada elemento del String llamando a la función que recibe como parámetro, si retorna un true se agrega dicho caracter al String que se retornará.

En la función main definir un String con una cadena cualquiera.

Llamar a la función de orden superior y pasar expresiones lambdas para filtrar y generar otro String con las siguientes restricciones:

  • Un String solo con las vocales
  • Un String solo con los caracteres en minúsculas
  • Un String con todos los caracteres no alfabéticos

Proyecto151 - Principal.kt

fun filtrar(cadena: String, fn: (Char) -> Boolean): String
{
    val cad = StringBuilder()
    for(ele in cadena)
        if (fn(ele))
            cad.append(ele)
    return cad.toString()
}

fun main(parametro: Array<String>) {
    val cadena="¿Esto es la prueba 1 o la prueba 2?"
    println("String original")
    println(cadena)
    val resultado1 = filtrar(cadena) {
        if (it == 'a' || it == 'e' || it == 'i' || it == 'o' || it == 'u' ||
            it == 'A' || it == 'E' || it == 'I' || it == 'O' || it == 'U' )
            true
        else
            false
    }
    println("Solo las vocales")
    println(resultado1)
    var resultado2 = filtrar(cadena) {
        if (it in 'a'..'z')
            true
        else
            false
    }
    println("Solo los caracteres en minúsculas")
    println(resultado2)
    var resultado3 = filtrar(cadena) {
        if (it !in 'a'..'z' && it !in 'A'..'Z')
            true
        else
            false
    }
    println("Solo los caracteres no alfabéticos")
    println(resultado3)
}

Nuestra función de orden superior se llama filtrar y recibe un String y una función:

fun filtrar(cadena: String, fn: (Char) -> Boolean): String

La función que recibe tiene un parámetro de tipo Char y retorna un Boolean.

El algoritmo de la función filtrar consiste en recorrer el String que llega como parámetro y llamar a la función fn que es la que informará si el caracter analizado debe formar parte del String final a retornar.

Mediante un objeto de la clase StringBuilder almacenamos los caracteres a devolver, previo a retornar extraemos como String el contenido del StringBuilder:

    val cad = StringBuilder()
    for(ele in cadena)
        if (fn(ele))
            cad.append(ele)
    return cad.toString()

En la función main definimos un String con una cadena cualquiera:

fun main(parametro: Array<String>) {
    val cadena="¿Esto es la prueba 1 o la prueba 2?"
    println("String original")
    println(cadena)

La primer llamada a la función de orden superior la hacemos enviando una expresión lambda que considere parte del String a generar solo las vocales minúsculas y mayúsculas:

    val resultado1 = filtrar(cadena) {
        if (it == 'a' || it == 'e' || it == 'i' || it == 'o' || it == 'u' ||
            it == 'A' || it == 'E' || it == 'I' || it == 'O' || it == 'U' )
            true
        else
            false
    }
    println("Solo las vocales")
    println(resultado1)

Recordemos que utilizamos "it" ya que la función anónima tiene un solo parámetro y nos permite expresar un código más conciso, en la forma larga debería ser:

    val resultado1 = filtrar(cadena) { car ->
        if (car == 'a' || car == 'e' || car == 'i' || car == 'o' || car == 'u' ||
            car == 'A' || car == 'E' || car == 'I' || car == 'O' || car == 'U' )
            true
        else
            false
    }
    println("Solo las vocales")
    println(resultado1)

Para generar un String solo con las letras minúsculas debemos verificar si el parámetro de la función anónima se encuentra en el rango 'a'..'z':

    var resultado2 = filtrar(cadena) {
        if (it in 'a'..'z')
            true
        else
            false
    }
    println("Solo los caracteres en minúsculas")
    println(resultado2)

Por último para recuperar todos los símbolos que no sean letras expresamos la siguiente condición:

    var resultado3 = filtrar(cadena) {
        if (it !in 'a'..'z' && it !in 'A'..'Z')
            true
        else
            false
    }
    println("Solo los caracteres no alfabéticos")
    println(resultado3)

Con este problema podemos seguir apreciando las grandes ventajas que nos proveen las expresiones lambdas para la resolución de algoritmos.