Usar tipos de função e expressões lambda no Kotlin

1. Introdução

Este codelab ensina sobre os tipos de função, como eles podem ser usados e a sintaxe específica para expressões lambda.

No Kotlin, as funções são consideradas construções de primeira classe. Isso significa que as funções podem ser tratadas como um tipo de dado. Você pode armazenar funções em variáveis, fazer a transmissão delas para outras funções como argumentos e retornar essas funções.

Assim como outros tipos de dados que você pode expressar com valores literais, como um tipo Int de valor 10 e um tipo String de valor "Hello", também é possível declarar literais de função, que são chamados de expressões lambda ou lambdas. Expressões lambda são usadas extensivamente no desenvolvimento Android e, de forma geral, na programação em Kotlin.

Pré-requisitos

  • Familiaridade com a programação em Kotlin, incluindo funções, instruções if/else e nulidade.

O que você vai aprender

  • Como definir uma função com sintaxe de lambda.
  • Como armazenar funções em variáveis.
  • Como transmitir funções como argumentos para outras funções.
  • Como retornar funções de outras funções.
  • Como usar tipos de função com nulidade.
  • Como deixar as expressões lambda mais concisas.
  • O que é uma função de ordem superior.
  • Como usar a função repeat().

O que é necessário

  • Um navegador da Web com acesso ao Playground Kotlin.

2. Assistir ao vídeo de orientações (opcional)

Se você quiser acompanhar a conclusão deste codelab por um dos instrutores do curso, assista ao vídeo abaixo.

Recomendamos abrir o vídeo em tela cheia usando o ícone Símbolo com quatro cantos destacados em um quadrado para indicar o modo de tela cheia. no canto do vídeo. Assim, você possa conferir o Playground Kotlin e o código com maior clareza.

Esta etapa é opcional. Você pode pular o vídeo e começar a seguir as instruções do codelab.

3. Armazenar uma função em uma variável

Até agora, você aprendeu a declarar funções com a palavra-chave fun. Uma função declarada com fun pode ser chamada, o que faz com que o código no corpo dela seja executado.

Por ser uma construção de primeira classe, as funções também são tipos de dados. Então, você pode armazenar funções em variáveis, fazer a transmissão delas para outras funções e retornar essas funções. Talvez você queira mudar o comportamento de um elemento do app no momento da execução ou aninhar funções de composição para criar layouts, como já feito em codelabs anteriores. Tudo isso é possível com expressões lambda.

Para explicar melhor, vamos usar a tradição doces ou travessuras (link em inglês), que ocorre no Halloween em vários países e na qual crianças fantasiadas pedem doces de porta em porta.

Armazene uma função em uma variável:

  1. Acesse o Kotlin Playground.
  2. Após a função main(), defina uma trick() sem parâmetros e sem valor de retorno para mostrar a mensagem "No treats!" (Sem doces!). A sintaxe é a mesma de outras funções mostradas em codelabs anteriores.
fun main() {

}

fun trick() {
    println("No treats!")
}
  1. No corpo da função main(), crie uma variável chamada trickFunction e a defina como igual a trick (travessuras). Os parênteses depois de trick não são incluídos porque você não quer chamar a função, mas sim que ela seja armazenada em uma variável.
fun main() {
    val trickFunction = trick
}

fun trick() {
    println("No treats!")
}
  1. Execute o código. Ele gera um erro porque o compilador Kotlin reconhece trick como o nome da função trick(), mas espera que você chame a função em vez de a atribuir a uma variável.
Function invocation 'trick()' expected

Você tentou armazenar trick na variável trickFunction. No entanto, para se referir a uma função como um valor, é necessário usar o operador de referência de função (::). A sintaxe é ilustrada nesta imagem:

a9a9bfa88485ec67.png

  1. Para fazer referência à função como um valor, reatribua trickFunction a ::trick.
fun main() {
    val trickFunction = ::trick
}

fun trick() {
    println("No treats!")
}
  1. Execute o código para verificar se não há mais erros. Você vai receber um aviso informando que trickFunction não foi usada, mas isso será corrigido na próxima seção.

Redefinir a função com uma expressão lambda

As expressões lambda fornecem uma sintaxe concisa para definir uma função sem a palavra-chave fun. Você pode armazenar uma expressão lambda diretamente em uma variável sem uma referência de função em outra função.

Antes do operador de atribuição (=), adicione a palavra-chave val ou var seguida pelo nome da variável, que é o que você usa ao chamar a função. Depois do operador de atribuição (=), está a expressão lambda, que consiste em um par de chaves que formam o corpo da função. A sintaxe está ilustrada nesta imagem:

5e25af769cc200bc.png

Ao definir uma função com uma expressão lambda, você tem uma variável que se refere à função. Você também pode atribuir o valor a outras variáveis, como qualquer outro tipo, e chamar a função com o nome da nova variável.

Atualize o código para usar uma expressão lambda:

  1. Reprograme a função trick() com uma expressão lambda. O nome trick agora se refere ao nome de uma variável. O corpo da função nas chaves agora é uma expressão lambda.
fun main() {
    val trickFunction = ::trick
}

val trick = {
    println("No treats!")
}
  1. Na função main(), remova o operador de referência de função (::), já que trick agora se refere a uma variável em vez de um nome de função.
fun main() {
    val trickFunction = trick
}

val trick = {
    println("No treats!")
}
  1. Execute o código. Não há erros e você pode referenciar a função trick() sem o operador de referência de função (::). Não há saída porque você ainda não chamou a função.
  2. Na função main(), chame a trick(), mas agora inclua os parênteses, como faria ao chamar qualquer outra função.
fun main() {
    val trickFunction = trick
    trick()
}

val trick = {
    println("No treats!")
}
  1. Execute o código. O corpo da expressão lambda é executado.
No treats!
  1. Na função main(), chame a variável trickFunction como se ela fosse uma função.
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
}

val trick = {
    println("No treats!")
}
  1. Execute o código. A função é chamada duas vezes: uma para a chamada de função trick() e outra para a trickFunction().
No treats!
No treats!

As expressões lambda permitem criar variáveis que armazenam funções, fazer a chamada delas como funções e as armazenar em outras variáveis que podem ser chamadas como funções.

4. Usar funções como um tipo de dado

Você aprendeu em um codelab anterior que o Kotlin tem inferência de tipo. Ao declarar uma variável, geralmente não é necessário especificar explicitamente o tipo. No exemplo anterior, o compilador Kotlin conseguiu inferir que o valor de trick era uma função. No entanto, se você quer especificar o tipo de parâmetro de função ou retorno, precisa saber a sintaxe para expressar os tipos de função. Os tipos de função consistem em um conjunto de parênteses com uma lista de parâmetros opcional, o símbolo -> e um tipo de retorno. A sintaxe está ilustrada nesta imagem:

5608ac5e471b424b.png

O tipo de dado da variável trick que você declarou anteriormente seria () -> Unit. Os parênteses estão vazios porque a função não tem parâmetros. O tipo de retorno é Unit, porque a função não retorna nada. Se você tivesse uma função que usasse dois parâmetros Int e retornasse um Int, o tipo de dado dela seria (Int, Int) -> Int.

Declare outra função com uma expressão lambda que especifique explicitamente o tipo da função:

  1. Depois da variável trick, declare uma variável chamada treat igual a uma expressão lambda com um corpo que mostra a mensagem "Have a treat!" ("Pegue um doce!").
val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. Especifique o tipo de dado da variável treat como () -> Unit.
val treat: () -> Unit = {
    println("Have a treat!")
}
  1. Na função main(), chame a treat().
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
    treat()
}
  1. Execute o código. A função treat() se comporta como a trick(). As duas variáveis têm o mesmo tipo de dado, mesmo que apenas a variável treat o declare explicitamente.
No treats!
No treats!
Have a treat!

Usar uma função como um tipo de retorno

Uma função é um tipo de dado e, por isso, pode ser usada como qualquer outro. Você pode até mesmo retornar funções de outras funções. A sintaxe está ilustrada nesta imagem:

f16dd6ca0c1588f5.png

Crie uma função que retorne outra.

  1. Exclua o código da função main().
fun main() {

}
  1. Após a função main(), defina uma trickOrTreat() que aceite um parâmetro isTrick do tipo Boolean.
fun main() {

}

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

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

val treat = {
    println("Have a treat!")
}
  1. No corpo da função trickOrTreat(), adicione uma instrução if que retorne a função trick() caso isTrick seja true e a função treat() caso isTrick seja false.
fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}
  1. Na função main(), crie uma variável chamada treatFunction e a atribua ao resultado da chamada de trickOrTreat(), transmitindo false para o parâmetro isTrick. Em seguida, crie uma segunda variável, chamada trickFunction, e a atribua ao resultado da chamada de trickOrTreat(), desta vez transmitindo true para o parâmetro isTrick.
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
}
  1. Chame treatFunction() e depois trickFunction() na próxima linha.
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. Execute o código. Você verá a resposta de cada função. Mesmo que não tenha chamado as funções trick() ou treat() diretamente, elas ainda podem ser chamadas, já que você armazenou os valores de retorno cada vez que chamou a função trickOrTreat() e usou as variáveis trickFunction e treatFunction para chamar as funções.
Have a treat!
No treats!

Agora, você sabe como as funções podem retornar outras funções. Você também pode transmitir uma função como argumento para outra. Talvez você queira fornecer algum comportamento personalizado para que a função trickOrTreat() faça algo além de retornar uma das duas strings. Uma função que usa outra como argumento permite transmitir uma função diferente sempre que ela é chamada.

Transmitir uma função para outra como argumento

Em algumas partes do mundo que celebram o Halloween, as crianças recebem trocados em vez de doces, ou recebem as duas coisas. Você vai modificar a função trickOrTreat() para permitir que um presente diferente, representado por uma função, seja fornecido como argumento.

A função que trickOrTreat() usa como parâmetro também precisa usar um parâmetro próprio. Ao declarar tipos de função, os parâmetros não são rotulados. Você só precisa especificar os tipos de dados de cada parâmetro, separados por vírgula. A sintaxe está ilustrada nesta imagem:

8372d3b83d539fac.png

Quando você programa uma expressão lambda para uma função que usa um parâmetro, os parâmetros recebem nomes na ordem em que ocorrem. Os nomes dos parâmetros são listados depois das chaves iniciais, e cada nome é separado por uma vírgula. Uma seta (->) separa os nomes dos parâmetros do corpo da função. A sintaxe está ilustrada nesta imagem:

938d2adf25172873.png

Atualize a trickOrTreat() para usar uma função como parâmetro:

  1. Depois do parâmetro isTrick, adicione um extraTreat do tipo (Int) -> String.
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
  1. No bloco else, antes da instrução return, chame println(), transmitindo uma chamada para a função extraTreat(). Transmita 5 na chamada para extraTreat().
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}
  1. Agora, ao chamar trickOrTreat(), você precisa definir uma função com uma expressão lambda e a transmitir ao parâmetro extraTreat. Na função main(), antes das chamadas para a trickOrTreat(), adicione uma coins(). A função coins() dá ao parâmetro Int o nome quantity e retorna uma String. Perceba a ausência da palavra-chave return, que não pode ser usada em expressões lambda. Em vez disso, o resultado da última expressão na função vai se tornar o valor de retorno.
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. Depois da função coins(), adicione uma cupcake(), conforme mostrado. Nomeie a quantity de parâmetros Int e a separe do corpo da função usando o operador ->. Você já pode transmitir a função coins() ou cupcake() para a 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. Na função cupcake(), remova o parâmetro quantity e o símbolo ->. Eles não são usados, então podem ser omitidos.
val cupcake: (Int) -> String = {
    "Have a cupcake!"
}
  1. Atualize as chamadas para a função trickOrTreat(). Para a primeira chamada, quando isTrick for false, transmita a função coins(). Para a segunda chamada, quando isTrick for true, transmita a função 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. Execute o código. A função extraTreat() só é chamada quando o parâmetro isTrick é definido como um argumento false. Então, a saída inclui cinco moedas de 25 centavos, mas nenhum cupcake.
5 quarters
Have a treat!
No treats!

Tipos de função anuláveis

Assim como outros tipos de dados, os tipos de função podem ser declarados como anuláveis. Nesses casos, uma variável pode conter uma função ou ser null.

Para declarar uma função como anulável, coloque o tipo de função entre parênteses, com um símbolo ? depois do parêntese final. Por exemplo, se você quiser tornar o tipo () -> String anulável, ele precisa ser declarado como um tipo (() -> String)?. A sintaxe está ilustrada nesta imagem:

c8a004fbdc7469d.png

Torne o parâmetro extraTreat anulável para que não seja necessário fornecer uma função extraTreat() sempre que chamar a trickOrTreat():

  1. Mude o tipo de parâmetro extraTreat para (() -> String)?.
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. Modifique a chamada da função extraTreat() para usar uma instrução if e chamar a função apenas se ela não for nula. A função trickOrTreat() agora terá esta aparência:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}
  1. Remova a função cupcake() e substitua o argumento cupcake por null na segunda chamada para a função trickOrTreat().
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Execute o código. A saída não será modificada. Agora que você pode declarar tipos de função como anuláveis, não é mais necessário transmitir uma função para o parâmetro extraTreat.
5 quarters
Have a treat!
No treats!

5. Programar expressões lambda com sintaxe abreviada

As expressões lambda oferecem várias maneiras de deixar seu código mais conciso. Algumas delas vão aparecer nesta seção, já que a maioria das expressões lambda que você encontra e programa têm sintaxe abreviada.

Omitir o nome do parâmetro

Ao programar a função coins(), você declarou explicitamente o nome quantity para o parâmetro Int da função. No entanto, como você viu com a função cupcake(), é possível omitir totalmente o nome do parâmetro. Quando uma função tem um único parâmetro e você não fornece um nome, o Kotlin atribui o nome it implicitamente. Assim, você pode omitir o nome do parâmetro e o símbolo ->, deixando suas expressões lambda mais concisas. A sintaxe está ilustrada nesta imagem:

332ea7bade5062d6.png

Atualize a função coins() para usar a sintaxe abreviada para parâmetros:

  1. Na função coins(), remova o nome do parâmetro quantity e o símbolo ->.
val coins: (Int) -> String = {
    "$quantity quarters"
}
  1. Mude o modelo de string "$quantity quarters" para se referir ao parâmetro único usando $it.
val coins: (Int) -> String = {
    "$it quarters"
}
  1. Execute o código. O Kotlin reconhece o nome do parâmetro it do Int e ainda mostra o número de moedas de 25 centavos.
5 quarters
Have a treat!
No treats!

Transmitir uma expressão lambda diretamente para uma função

No momento, a função coins() é usada apenas em um lugar. E se você pudesse simplesmente transmitir uma expressão lambda para a função trickOrTreat() sem a necessidade de criar uma variável?

As expressões lambda são simplesmente literais de função, assim como 0 é um literal de número inteiro ou "Hello" é um literal de string. Você pode transmitir uma expressão lambda diretamente para uma chamada de função. A sintaxe está ilustrada nesta imagem:

39dc1086e2471ffc.png

Modifique o código para remover a variável coins:

  1. Mova a expressão lambda para que ela seja transmitida diretamente para a função trickOrTreat() na chamada. Você também pode condensar a expressão lambda em uma única linha.
fun main() {
    val coins: (Int) -> String = {
        "$it quarters"
    }
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Remova a variável coins, que não será mais usada.
fun main() {
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. Execute o código. A compilação e execução ainda funcionam como esperado.
5 quarters
Have a treat!
No treats!

Usar a sintaxe de lambda final

É possível usar outra opção abreviada para programar lambdas quando um tipo de função é o último parâmetro de uma função. Nesse caso, você pode colocar a expressão lambda após os parênteses para chamar a função. A sintaxe está ilustrada nesta imagem:

3ee3176d612b54.png

Isso deixa seu código mais legível, porque ele separa a expressão lambda dos outros parâmetros, mas não muda o que o código faz.

Atualize o código para usar a sintaxe de lambda final:

  1. Na variável treatFunction, mova a expressão lambda {"$it quarters"} após os parênteses na chamada para trickOrTreat().
val treatFunction = trickOrTreat(false) { "$it quarters" }
  1. Execute o código. Tudo continua funcionando.
5 quarters
Have a treat!
No treats!

6. Usar a função repeat()

Quando uma função retorna outra ou usa uma função como argumento, ela é chamada de função de ordem superior. A função trickOrTreat() é um exemplo de função de ordem superior, já que usa uma função do tipo ((Int) -> String)? como parâmetro e retorna outra do tipo () -> Unit. O Kotlin oferece várias funções de ordem superior úteis que podem ser aproveitadas com seu novo conhecimento sobre lambdas.

A função repeat() é uma dessas funções de ordem superior. A repeat() é uma maneira concisa de expressar uma repetição for com funções. Você vai usar essa e outras funções de ordem superior com frequência nas próximas unidades. A função repeat() tem esta assinatura de função:

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

O parâmetro times é o número de vezes que a ação precisa ocorrer. O parâmetro action é uma função que usa um único parâmetro Int e retorna um tipo Unit. O parâmetro Int da função action é o número de vezes que a ação foi executada até o momento, como um argumento 0 na primeira iteração ou um 1 na segunda. Você pode usar a função repeat() para repetir o código um número específico de vezes, semelhante a uma repetição for. A sintaxe está ilustrada nesta imagem:

519a2e0f5d02687.png

Em vez de chamar a função trickFunction() apenas uma vez, você pode fazer isso várias vezes com a função repeat().

Atualize seu código de doces ou travessuras para ver a repeat() em ação:

  1. Na função main(), chame a repeat() entre as chamadas para treatFunction() e trickFunction(). Transmita 4 para o parâmetro times e use a sintaxe de lambda final para a função action. Não é necessário fornecer um nome para o parâmetro Int da expressão lambda.
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
    repeat(4) {

    }
}
  1. Mova a chamada para a função treatFunction() na expressão lambda da função repeat().
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    repeat(4) {
        treatFunction()
    }
    trickFunction()
}
  1. Execute o código. A string "Have a treat" precisa ser mostrada quatro vezes.
5 quarters
Have a treat!
Have a treat!
Have a treat!
Have a treat!
No treats!

7. Conclusão

Parabéns! Você aprendeu as noções básicas dos tipos de função e expressões lambda. Conhecer bem esses conceitos ajuda no aprendizado da linguagem Kotlin. O uso de tipos de função, funções de ordem superior e sintaxe abreviada também deixa o código mais conciso e fácil de ler.

Resumo

  • As funções no Kotlin são construções de primeira classe e podem ser tratadas como tipos de dados.
  • As expressões lambda fornecem uma sintaxe abreviada para programar funções.
  • É possível transmitir tipos de função para outras funções.
  • Você pode retornar um tipo de função de outra função.
  • Uma expressão lambda retorna o valor da última expressão.
  • Se um rótulo de parâmetro for omitido em uma expressão lambda com um único parâmetro, ele será referenciado com o identificador it.
  • É possível programar lambdas in-line sem um nome de variável.
  • Se o último parâmetro de uma função for um tipo de função, você vai poder usar a sintaxe de lambda final para mover a expressão lambda após o último parêntese ao chamar uma função.
  • As funções de ordem superior são aquelas que usam outras funções como parâmetros ou retornam uma função.
  • A função repeat() é de ordem superior e funciona de maneira semelhante a uma repetição for.

Saiba mais