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();
}
E podemos implementar essa interface da seguinte forma:
class TaxCalculator {
public double calculate(double value, Tax tax) {
return tax.calculate(value);
}
}
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
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
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)
}
}
Recebemos o seguinte problema de compilação:
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
}
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 }
E podemos simplificar a implementação da calculadora:
class TaxCalculator {
fun calculate(
value: Double,
calculate: (Double) -> Double
): Double {
return calculate(value)
}
}
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
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 = { }) {
}
}
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,
...
) {
...
}
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
}
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")
}
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)
}
}
)
}
}
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()
})
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.
Top comments (0)