Expressão lambda, tipo função e funções de alta ordem no Kotlin

alexfelipe

Alex Felipe

Posted on June 12, 2023

Expressão lambda, tipo função e funções de alta ordem no Kotlin

Antes de começar a estudar Kotlin, eu tive uma boa experiência com Java, e uma das dúvidas que mais me pegou foi:

"Como implemento um método/função que recebe uma expressão lambda?"

Se pensarmos em um código Java, basicamente, precisamos receber uma interface com apenas uma assinatura, como por exemplo, uma interface para calcular uma taxa:

interface Tax {

    double calculate();

}
Enter fullscreen mode Exit fullscreen mode

E podemos implementar essa interface da seguinte forma:

class TaxCalculator {
    public double calculate(double value, Tax tax) {
        return tax.calculate(value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma calculadora de taxa que recebe o valor e oferece a interface para fazer o cálculo. Então podemos implementar a interface via expressão lambda passando uma regra de 10% adicionais:

var value = 100.0;
var calculator = new TaxCalculator();
var calculatedValue = calculator.calculate(value,
        (v) -> v + (v * 0.1)
);
System.out.println(calculatedValue);
// 110.0
Enter fullscreen mode Exit fullscreen mode

Esse é um dos exemplos de expressão lambda no Java, mas existe uma série de possibilidades, principalmente se considermos a classe Stream introduzida desde o Java 8...

Utilizando expressão lambda no Kotlin a partir de código Java

Com esse mesmo código podemos utilizar expressões lambda no Kotlin:

val value = 100.0
val calculator = TaxCalculator()
val calculatedValue = calculator
    .calculate(value) { v: Double -> v + v * 0.1 }
println(calculatedValue)
// 110.0
Enter fullscreen mode Exit fullscreen mode

E isso é possível, pois interfaces de um único método abstrado (Single Abstract Method - SAM) ou também conhecidas como interfaces funcionais, podem ser convertidas em expressões lambda.

O grande detalhe é que a interface "precisa ser implementada em Java", ou seja, se a calculadora e interface de taxa forem em Kotlin:

interface Tax {
    fun calculate(value: Double): Double
}

class TaxCalculator {
    fun calculate(value: Double, tax: Tax): Double {
        return tax.calculate(value)
    }
}
Enter fullscreen mode Exit fullscreen mode

Recebemos o seguinte problema de compilação:

Mensagem de tipo incompatível, espera Tax mas foi enviado (Double) -> Double

Isso mesmo! Uma incompatibilidade de tipos indicando que é esperado um Tax ao invés de (Double) -> Double.

Nesse momento você pode se perguntar:

"Que tipo estranho é esse?" 🤔

Inicialmente posso indicar que é um tipo especial do Kotlin, mas, antes de falarmos dele, vamos entender como podemos usar interfaces funcionais e expressões lambda apenas no Kotlin.

Interfaces funções

A partir da versão 1.4 do Kotlin, foram introduzidas as interfaces funcionais! Para implementá-las, basta apenas adicionar o fun no início da assinatura:

fun interface Tax {
    fun calculate(value: Double): Double
}
Enter fullscreen mode Exit fullscreen mode

Pronto! Apenas com esse ajuste podemos usar a expressão lambda sem problemas 😄

Só que não vamos parar por aqui! Agora chegou o momento de entender aquele tipo estranho que vimos anteriormente 😅

O tipo função

Esse tipo com essa sintax diferente, é uma notação do tipo função ou Function Type. Basicamente, essa notação simplifica a implementação de funções permitindo armazená-las em variáveis ou parâmetros.

Em outras palavras, a expressão lambda que escrevemos em Kotlin poderia ser representada pela seguinte variável:

val calculateTax: (Double) -> Double = { v: Double -> v + v * 0.1 }
Enter fullscreen mode Exit fullscreen mode

E podemos simplificar a implementação da calculadora:

class TaxCalculator {
    fun calculate(
        value: Double,
        calculate: (Double) -> Double
    ): Double {
        return calculate(value)
    }
}
Enter fullscreen mode Exit fullscreen mode

E com apenas esse ajuste, ainda mantemos a mesma chamada via expressão lambda! Se preferir, pode até mesmo enviar a variável do tipo (Double) -> Double:

val value = 100.0
val calculateTax: (Double) -> Double = { v: Double -> v + v * 0.1 }
val calculator = TaxCalculator()
val calculatedValue = calculator
    .calculate(value, calculateTax)
println(calculatedValue)
// 110.0
Enter fullscreen mode Exit fullscreen mode

Funções de alta ordem

A partir do momento que declaramos parâmetros ou retorno do tipo função, significa que criamos uma função de alta ordem ou Higher-Order Functions - HOF.

HOF são bastante comuns em diversos códigos em Kotlin, principalmente no ambiente do Android! Se você já teve a experiência com o Jetpack Compose, muito provavelmente já viu códigos similares a esses:

Column {
    Row {

    }
    Button(onClick = { }) {

    }
}
Enter fullscreen mode Exit fullscreen mode

E se eu te disser que todos esses composables são HOF? Vamos verificar a implementação de cada um:

@Composable
inline fun Column(
    ...,
    content: @Composable ColumnScope.() -> Unit
) {
    ...
}

@Composable
inline fun Row(
    ...,
    content: @Composable RowScope.() -> Unit
) {
    ...
}

@Composable
fun Button(
    onClick: () -> Unit,
    ...
) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Veja que todos esperam uma função via parâmetro! O único detalhe é que o Button não é capaz de receber a lambda à direita (famoso trailing lambda) assim como vemos no Column ou Box. O motivo dessa diferença é, apenas as HOFs com o último parâmetro sendo uma função, podem implementar o trailing lambda.

Embora você viu o que é o tipo função, HOF etc, pode ser que não seja tão claro a utilidade no dia a dia, concorda? Sendo assim, vamos fazer algumas conclusões e ver mais exemplos. 😎

Quando usar o tipo função?

HOFs em geral, são utilizadas para códigos que devem ser executados em um escopo específico ou em eventos. Um dos exemplos bastante utilizados são em funções de escopo (scope functions) em valores não nulos:

val words = listOf(
    "alex",
    "felipe",
    "instrutor"
)
val name = words
    .find { it == "alex" }
    ?.let { name ->
        println(name)
        // alex
    }
Enter fullscreen mode Exit fullscreen mode

Nesta amostra de código, só vai chegar no println(), quando o valor não for nulo. Outro caso são listeners, como onClick do Button:

Button(
    onClick = {
        authenticate()
    }) {
    Text(text = "Login")
}
Enter fullscreen mode Exit fullscreen mode

O authenticate() só vai ser executado quando o evento de clique acontecer! E essa técnica pode ser estendida para códigos que implementamos! Um dos casos muito comuns são os famosos callbacks:

class UserRepository(
    private val userAPI: UserAPI
) {
    fun findUserById(
        id: String,
        onSuccess: (User) -> Unit = {},
        onFailure: (Throwable) -> Unit = {}
    ) {
        userAPI.findUserById(id).enqueue(
            object : Callback<User?> {
                override fun onResponse(
                    call: Call<User?>,
                    response: Response<User?>
                ) {
                    if (response.isSuccessful) {
                        response.body()?.let { user ->
                            onSuccess(user)
                        }
                    }
                }
                override fun onFailure(
                    call: Call<User?>,
                    t: Throwable
                ) {
                    onFailure(t)
                }
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Essa é uma chamada assíncrona com o Retrofit, basicamente, chamamos o enqueue() de Call que exige a implementação de Callback e nele temos 2 eventos:

  • onResponse() -> quando ocorre resposta
  • onFailure() -> quando ocorre a falha

Então, você pode fazer as validações que deseja e rodar a função onSuccess enviando o usuário necessário, como também, no momento da falha, pode simplesmente chamar a função onFailure enviando o Throwable. O uso desse método fica da seguinte maneira:

val repository = UserRepository(
     //some implementation of UserAPI
)
repository.findUserById("some id",
    onSuccess = { user ->
        println("user found: $user")
    },
    onFailure = { t ->
        println("failure to find user")
        t.printStackTrace()
    })
Enter fullscreen mode Exit fullscreen mode

E é por conta dessas possibilidades que o uso do tipo função, higher-order functions e expressão lambda é BASTANTE comum em códigos Kotlin 😄

O que achou dessas técnicas? Já usa no seu dia a dia? Compartilhe nos comentários.

💖 💪 🙅 🚩
alexfelipe
Alex Felipe

Posted on June 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related