Cómo usar tipos de funciones y expresiones lambda en Kotlin

1. Introducción

En este codelab, aprenderás sobre los tipos de funciones, cómo usarlos y la sintaxis específica de las expresiones lambda.

En Kotlin, las funciones se consideran construcciones de primera clase. Esto significa que las funciones se pueden tratar como un tipo de datos. Puedes almacenar funciones en variables, pasarlas a otras funciones como argumentos y mostrarlas desde otras funciones.

Al igual que otros tipos de datos que puedes expresar con valores literales, como un tipo Int de un valor 10 y un tipo String de un valor "Hello", también puedes declarar literales de funciones, que se llaman expresiones lambda o, simplemente, lambdas. Las expresiones lambda se usan en gran medida en el desarrollo de Android y, de manera más general, en la programación en Kotlin.

Requisitos previos

  • Estar familiarizado con la programación en Kotlin, incluidas las funciones, las sentencias if/else y la nulabilidad

Qué aprenderás

  • Cómo definir una función con sintaxis lambda
  • Cómo almacenar funciones en variables
  • Cómo pasar funciones como argumentos a otras funciones
  • Cómo mostrar funciones de otras funciones
  • Cómo usar tipos de funciones anulables
  • Cómo hacer que las expresiones lambda sean más concisas
  • Qué es una función de orden superior
  • Cómo usar la función repeat()

Requisitos

  • Un navegador web con acceso a Playground de Kotlin

2. Mira el video con instrucciones para compilar (opcional)

Si quieres ver cómo uno de los instructores del curso completa el codelab, reproduce el siguiente video.

Se recomienda expandir el video a pantalla completa (con el ícono Este símbolo muestra 4 esquinas en un cuadrado destacado para indicar el modo de pantalla completa. en la esquina del video) para que puedas ver Playground de Kotlin y el código con mayor claridad.

Este paso es opcional. También puedes omitir el video y comenzar con las instrucciones del codelab de inmediato.

3. Cómo almacenar una función en una variable

Hasta ahora, aprendiste a declarar funciones con la palabra clave fun. Se puede llamar a una función declarada con la palabra clave fun, lo que hace que se ejecute el código en el cuerpo de la función.

Como construcción de primera clase, las funciones también son tipos de datos, por lo que puedes almacenar funciones en variables, pasarlas a funciones y mostrarlas a partir de funciones. Quizás necesites la capacidad de cambiar el comportamiento de una parte de tu app en el tiempo de ejecución o anidar funciones de componibilidad para compilar diseños como lo hiciste en codelabs anteriores. Todo esto es posible gracias a las expresiones lambda.

Puedes verlo en acción con dulce o truco, que se refiere a la tradición de Halloween en muchos países cuando niños disfrazados van de puerta en puerta y preguntan: "¿Dulce o truco?", por lo general, a cambio de golosinas.

Almacena una función en una variable:

  1. Ve a Playground de Kotlin.
  2. Después de la función main(), define una función trick() sin parámetros y sin valor de retorno que imprima "No treats!". La sintaxis es la misma que la de otras funciones que viste en codelabs anteriores.
fun main() {

}

fun trick() {
    println("No treats!")
}
  1. En el cuerpo de la función main(), crea una variable llamada trickFunction y establécela en trick. No incluyas los paréntesis después de trick, ya que quieres almacenar la función en una variable en lugar de llamar a la función.
fun main() {
    val trickFunction = trick
}

fun trick() {
    println("No treats!")
}
  1. Ejecuta tu código. Genera un error porque el compilador de Kotlin reconoce trick como el nombre de la función trick(), pero espera que la llames en lugar de asignarla a una variable.
Function invocation 'trick()' expected

Intentaste almacenar trick en la variable trickFunction. Sin embargo, para hacer referencia a una función como un valor, debes usar el operador de referencia de función (::). La sintaxis se ilustra en esta imagen:

a9a9bfa88485ec67.png

  1. Para hacer referencia a la función como un valor, reasigna trickFunction a ::trick.
fun main() {
    val trickFunction = ::trick
}

fun trick() {
    println("No treats!")
}
  1. Ejecuta tu código para verificar que no haya más errores. Verás una advertencia que indica que nunca se usa trickFunction, aunque ese error se corrigió en la siguiente sección.

Cómo volver a definir la función con una expresión lambda

Las expresiones lambda usan una sintaxis concisa para definir una función sin la palabra clave fun. Puedes almacenar una expresión lambda directamente en una variable sin una referencia de función en otra función.

Antes del operador de asignación (=), debes agregar la palabra clave val o var seguida del nombre de la variable, que es lo que usas cuando llamas a la función. Después del operador de asignación (=) se encuentra la expresión lambda, que consiste en un par de llaves que forman el cuerpo de la función. La sintaxis se ilustra en esta imagen:

5e25af769cc200bc.png

Cuando defines una función con una expresión lambda, tienes una variable que hace referencia a la función. También puedes asignar su valor a otras variables como cualquier otro tipo y llamar a la función con el nombre de la variable nueva.

Actualiza el código para usar una expresión lambda:

  1. Vuelve a escribir la función trick() con una expresión lambda. El nombre trick ahora hace referencia al nombre de una variable. El cuerpo de la función en las llaves ahora es una expresión lambda.
fun main() {
    val trickFunction = ::trick
}

val trick = {
    println("No treats!")
}
  1. En la función main(), quita el operador de referencia de función (::), ya que trick ahora hace referencia a una variable en lugar de a un nombre de función.
fun main() {
    val trickFunction = trick
}

val trick = {
    println("No treats!")
}
  1. Ejecuta tu código. No hay errores y puedes consultar la función trick() sin el operador de referencia de función (::). No hay un resultado porque aún no llamaste a la función.
  2. En la función main(), llama a la función trick(), pero esta vez incluye los paréntesis como lo harías cuando llamas a cualquier otra función.
fun main() {
    val trickFunction = trick
    trick()
}

val trick = {
    println("No treats!")
}
  1. Ejecuta tu código. Se ejecuta el cuerpo de la expresión lambda.
No treats!
  1. En la función main(), llama a la variable trickFunction como si fuera una función.
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
}

val trick = {
    println("No treats!")
}
  1. Ejecuta tu código. Se llama a la función dos veces, una para la llamada a función trick() y otra para la llamada a función trickFunction().
No treats!
No treats!

Con las expresiones lambda, puedes crear variables que almacenen funciones, llamar a estas variables como funciones y almacenarlas en otras variables a las que puedes llamar como funciones.

4. Cómo usar funciones como un tipo de datos

En un codelab anterior, aprendiste que Kotlin tiene inferencia de tipo. Cuando declaras una variable, a menudo no es necesario que especifiques el tipo de forma explícita. En el ejemplo anterior, el compilador de Kotlin pudo inferir que el valor de trick era una función. Sin embargo, si deseas especificar el tipo de parámetro de función o un tipo de datos que se muestra, debes conocer la sintaxis para expresar tipos de función. Los tipos de funciones consisten en un conjunto de paréntesis que contienen una lista de parámetros opcional, el símbolo -> y un tipo de datos que se muestra. La sintaxis se ilustra en esta imagen:

5608ac5e471b424b.png

El tipo de datos de la variable trick que declaraste antes sería () -> Unit. Los paréntesis están vacíos porque la función no tiene ningún parámetro. El tipo de datos que se muestra es Unit porque la función no muestra nada. Si tienes una función que toma dos parámetros Int y muestra un Int, el tipo de datos será (Int, Int) -> Int.

Declara otra función con una expresión lambda que especifique el tipo de función de forma explícita:

  1. Después de la variable trick, declara una variable llamada treat igual a una expresión lambda con un cuerpo que imprima "Have a treat!".
val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. Especifica el tipo de datos de la variable treat como () -> Unit.
val treat: () -> Unit = {
    println("Have a treat!")
}
  1. En la función main(), llama a la función treat().
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
    treat()
}
  1. Ejecuta el código. La función treat() se comporta como la función trick(). Ambas variables tienen el mismo tipo de datos, pero solo la variable treat lo declara explícitamente.
No treats!
No treats!
Have a treat!

Cómo usar una función como tipo de datos que se muestra

Una función es un tipo de datos, por lo que puedes usarla como cualquier otro tipo de datos. Incluso puedes mostrar funciones de otras funciones. La sintaxis se ilustra en esta imagen:

f16dd6ca0c1588f5.png

Crea una función que muestre una función.

  1. Borra el código de la función main().
fun main() {

}
  1. Después de la función main(), define una función trickOrTreat() que acepte un parámetro isTrick de tipo Boolean.
fun main() {

}

fun trickOrTreat(isTrick: Boolean): () -> Unit {
}

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. En el cuerpo de la función trickOrTreat(), agrega una sentencia if que muestre la función trick() si isTrick es true y que muestre la función treat() si isTrick es falso.
fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}
  1. En la función main(), crea una variable llamada treatFunction y asígnala al resultado de la llamada a trickOrTreat(). Para ello, pasa false para el parámetro isTrick. Luego, crea una segunda variable, llamada trickFunction, y asígnala al resultado de la llamada a trickOrTreat(). Esta vez, pasa true para el parámetro isTrick.
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
}
  1. Llama a treatFunction() y, luego, a trickFunction() en la siguiente línea.
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. Ejecuta tu código. Deberías ver el resultado de cada función. Aunque no llamaste a las funciones trick() o treat() directamente, aún puedes llamarlas porque almacenaste los valores que se muestran cada vez que llamaste a la función trickOrTreat(), y llamaste a las funciones con las variables trickFunction y treatFunction.
Have a treat!
No treats!

Ahora sabes cómo las funciones pueden mostrar otras funciones. También puedes pasar una función como argumento a otra función. Tal vez quieras brindar un comportamiento personalizado a la función trickOrTreat() para que haga algo más que mostrar cualquiera de las dos strings. Una función que toma otra función como argumento te permite pasar una función diferente cada vez que se la llama.

Cómo pasar una función a otra como un argumento

En algunas partes del mundo que celebran Halloween, los niños reciben algo de dinero en lugar de dulces (o reciben ambos). Modificarás tu función trickOrTreat() para permitir que un regalo adicional, representado por una función, se proporcione como argumento.

La función que trickOrTreat() usa como parámetro también debe tomar un parámetro propio. Cuando declaras tipos de funciones, los parámetros no se etiquetan. Solo debes especificar los tipos de datos de cada parámetro, separados por coma. La sintaxis se ilustra en esta imagen:

8372d3b83d539fac.png

Cuando escribes una expresión lambda para una función que toma un parámetro, los parámetros reciben nombres en el orden en que ocurren. Los nombres de los parámetros se muestran después de las llaves de apertura y cada nombre está separado por una coma. Una flecha (->) separa los nombres de los parámetros del cuerpo de la función. La sintaxis se ilustra en esta imagen:

938d2adf25172873.png

Actualiza la función trickOrTreat() para que tome una función como parámetro:

  1. Después del parámetro isTrick, agrega un parámetro extraTreat de tipo (Int) -> String.
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
  1. En el bloque else, antes de la sentencia return, llama a println() y pasa una llamada a la función extraTreat(). Pasa 5 a la llamada a extraTreat().
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}
  1. Ahora, cuando llames a la función trickOrTreat(), deberás definir una función con una expresión lambda y pasarla para el parámetro extraTreat. En la función main() antes de las llamadas a la función trickOrTreat(), agrega una función coins(). La función coins() le asigna el nombre quantity al parámetro Int y muestra un objeto String. Observarás la ausencia de la palabra clave return, que no se puede usar en expresiones lambda. En cambio, el resultado de la última expresión en la función se convierte en el valor que se muestra.
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. Después de la función coins(), agrega una función cupcake() como se muestra. Asigna el nombre quantity al parámetro Int y sepáralo del cuerpo de la función con el operador ->. Ahora puedes pasar la función coins() o cupcake() a la función trickOrTreat().
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = { quantity ->
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. En la función cupcake(), quita el parámetro quantity y el símbolo ->. Como no se usan, puedes omitirlos.
val cupcake: (Int) -> String = {
    "Have a cupcake!"
}
  1. Actualiza las llamadas a la función trickOrTreat(). Para la primera llamada, cuando isTrick es false, pasa la función coins(). Para la segunda llamada, cuando isTrick es true, pasa la función cupcake().
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = {
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, cupcake)
    treatFunction()
    trickFunction()
}
  1. Ejecuta tu código. Solo se llama a la función extraTreat() cuando el parámetro isTrick se establece en un argumento false, por lo que el resultado incluye 5 trimestres, pero no magdalenas.
5 quarters
Have a treat!
No treats!

Tipos de funciones anulables

Al igual que otros tipos de datos, se pueden declarar los tipos de funciones como anulables. En estos casos, una variable podría contener una función o podría ser null.

Para declarar una función como anulable, encierra el tipo de función entre paréntesis seguido de un símbolo ? fuera del paréntesis de cierre. Por ejemplo, si deseas que el tipo () -> String sea anulable, decláralo como un tipo (() -> String)?. La sintaxis se ilustra en esta imagen:

c8a004fbdc7469d.png

Haz que el parámetro extraTreat sea anulable para que no tengas que proporcionar una función extraTreat() cada vez que llames a la función trickOrTreat():

  1. Cambia el tipo del parámetro extraTreat a (() -> String)?.
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. Modifica la llamada a la función extraTreat() para usar una sentencia if a fin de llamar a la función solo si no es nula. La función trickOrTreat() debería verse de la siguiente manera:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}
  1. Quita la función cupcake() y, luego, reemplaza el argumento cupcake por null en la segunda llamada a la función trickOrTreat().
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Ejecuta tu código. El resultado no debería cambiar. Ahora que puedes declarar tipos de funciones como anulables, ya no necesitas pasar una función para el parámetro extraTreat.
5 quarters
Have a treat!
No treats!

5. Cómo escribir expresiones lambda con sintaxis abreviada

Las expresiones lambda sirven para que tu código sea más conciso. En esta sección, explorarás algunas de ellas, ya que la mayoría de las expresiones lambda que encuentras y escribes están escritas con sintaxis abreviada.

Cómo omitir el nombre del parámetro

Cuando escribiste la función coins(), declaraste de forma explícita el nombre quantity para el parámetro Int de la función. Sin embargo, como viste con la función cupcake(), puedes omitir el nombre del parámetro por completo. Cuando una función tiene un solo parámetro y no proporcionas un nombre, Kotlin le asigna de forma implícita el nombre it, de manera que puedes omitir el nombre del parámetro y el símbolo ->, lo que hace que tus expresiones lambda sean más concisas. La sintaxis se ilustra en esta imagen:

332ea7bade5062d6.png

Actualiza la función coins() para usar la sintaxis abreviada para los parámetros:

  1. En la función coins(), quita el nombre del parámetro quantity y el símbolo ->.
val coins: (Int) -> String = {
    "$quantity quarters"
}
  1. Cambia la plantilla de string "$quantity quarters" para hacer referencia al parámetro único con $it.
val coins: (Int) -> String = {
    "$it quarters"
}
  1. Ejecuta tu código. Kotlin reconoce el nombre del parámetro it del parámetro Int y aún imprime la cantidad de trimestres.
5 quarters
Have a treat!
No treats!

Cómo pasar una expresión lambda directamente a una función

Por el momento, la función coins() solo se usa en un lugar. ¿Qué ocurriría si pudieras pasar una expresión lambda directamente a la función trickOrTreat() sin tener que crear una variable primero?

Las expresiones lambda son solo literales de función, al igual que 0 es un literal de número entero o "Hello" es un literal de cadena. Puedes pasar una expresión lambda directamente a una llamada a función. La sintaxis se ilustra en esta imagen:

39dc1086e2471ffc.png

Modifica el código para que puedas quitar la variable coins:

  1. Mueve la expresión lambda para que se pase directamente a la llamada a la función trickOrTreat(). También puedes condensar la expresión lambda en una sola línea.
fun main() {
    val coins: (Int) -> String = {
        "$it quarters"
    }
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Quita la variable coins porque ya no se usa.
fun main() {
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Ejecuta el código. Se compila y ejecuta según lo esperado.
5 quarters
Have a treat!
No treats!

Cómo usar la sintaxis de expresión lambda al final

Puedes usar otra opción abreviada para escribir lambdas cuando un tipo de función es el último parámetro de una función. De ser así, puedes colocar la expresión lambda después del paréntesis de cierre para llamar a la función. La sintaxis se ilustra en esta imagen:

3ee3176d612b54.png

Esto hace que el código sea más legible porque separa la expresión lambda de los otros parámetros, pero no cambia lo que hace el código.

Actualiza el código para usar la sintaxis lambda final:

  1. En la variable treatFunction, mueve la expresión lambda {"$it quarters"} después del paréntesis de cierre en la llamada a trickOrTreat().
val treatFunction = trickOrTreat(false) { "$it quarters" }
  1. Ejecuta tu código. ¡Todo funciona!
5 quarters
Have a treat!
No treats!

6. Cómo usar la función repeat()

Cuando una función muestra una función o toma una función como argumento, se denomina función de orden superior. La función trickOrTreat() es un ejemplo de una función de orden superior porque toma una función de tipo ((Int) -> String)? como parámetro y muestra una función de tipo () -> Unit. Kotlin ofrece varias funciones útiles de orden superior, que puedes aprovechar con todo lo que aprendiste recientemente sobre lambdas.

La función repeat() es una de esas funciones de orden superior. La función repeat() es una forma concisa de expresar un bucle for con funciones. Usarás esta y otras funciones de orden superior con frecuencia en unidades posteriores. La función repeat() tiene la siguiente firma de función:

repeat(times: Int, action: (Int) -> Unit)

El parámetro times es la cantidad de veces que debe ocurrir la acción. El parámetro action es una función que toma un solo parámetro Int y muestra un tipo Unit. El parámetro Int de la función action es la cantidad de veces que se ejecutó la acción hasta el momento, como un argumento 0 para la primera iteración o un argumento 1 para la segunda. Puedes usar la función repeat() para repetir el código una cantidad especificada de veces, de manera similar a un bucle for. La sintaxis se ilustra en esta imagen:

519a2e0f5d02687.png

En lugar de llamar a la función trickFunction() solo una vez, puedes llamarla varias veces con la función repeat().

Actualiza el código de dulce o truco para ver la función repeat() en acción:

  1. En la función main(), llama a la función repeat() entre las llamadas a treatFunction() y trickFunction(). Pasa 4 para el parámetro times y usa la sintaxis lambda al final de la función action. No es necesario que proporciones un nombre para el parámetro Int de la expresión lambda.
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
    repeat(4) {

    }
}
  1. Mueve la llamada a la función treatFunction() en la expresión lambda de la función repeat().
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    repeat(4) {
        treatFunction()
    }
    trickFunction()
}
  1. Ejecuta tu código. La string "Have a treat" debería imprimirse cuatro veces.
5 quarters
Have a treat!
Have a treat!
Have a treat!
Have a treat!
No treats!

7. Conclusión

¡Felicitaciones! Aprendiste los conceptos básicos de los tipos de funciones y las expresiones lambda. Estar familiarizado con estos conceptos te ayudará a obtener más información sobre el lenguaje Kotlin. El uso de tipos de funciones, funciones de orden superior y sintaxis abreviada también hace que tu código sea más conciso y fácil de leer.

Resumen

  • Las funciones en Kotlin son construcciones de primer nivel y se pueden tratar como tipos de datos.
  • Las expresiones lambda proporcionan una sintaxis abreviada para escribir funciones.
  • Puedes pasar tipos de funciones a otras funciones.
  • Puedes mostrar un tipo de función desde otra.
  • Una expresión lambda muestra el valor de la última expresión.
  • Si se omite una etiqueta de parámetro en una expresión lambda con un solo parámetro, se hace referencia a ella con el identificador it.
  • Las expresiones lambda se pueden escribir intercaladas sin un nombre de variable.
  • Si el último parámetro de una función es un tipo de función, puedes usar la sintaxis lambda al final para mover la expresión lambda después del último paréntesis cuando llamas a una función.
  • Las funciones de orden superior son funciones que toman otras funciones como parámetros o muestran una función.
  • La función repeat() es una función de orden superior que funciona de manera similar a un bucle for.

Más información