Programação funcional em Kotlin
Programação funcional é um paradigma de programação, assim como programação imperativa e programação orientada a objetos, mas diferencia-se por se basear no uso e na composição de funções para a construção dos programas[1]. Enquanto na programação imperativa desenhamos um algoritmo como uma sequência de passos lógicos que alteram o estado de execução, na programação funcional essencialmente nos preocupamos em transformar uma entrada em uma saída através de transformações realizadas por funções, em geral sem mudanças de estado.
A linguagem de programação Kotlin, sendo multi paradigma, suporta a lógica funcional e fornece uma série de recursos que são úteis para o desenvolvimento de soluções[2], como por exemplo na manipulação de coleções.
Funções de alta ordem
editarEm Kotlin, funções são de primeira classe, ou seja, podem ser atribuídas a nomes, armazenadas em estruturas de dados, passadas como argumentos para outras funções ou mesmo retornadas como resultado de outras funções, assim como qualquer outro tipo de dado. Esse comportamento permite o uso de funções de alta ordem, funções que podem receber outras funções como argumentos ou retorná-las como resultado, recurso essencial para a programação funcional.
Tipos e literais de função
editarPara declarações que lidam com funções, a linguagem Kotlin define os tipos envolvidos através de uma lista de tipos que serão recebidos como parâmetros entre parênteses e o tipo de retorno, como em (A, B) -> C
, onde A
e B
indicam os tipos dos parâmetros e C
o tipo do retorno. A lista de tipos de parâmetros pode ser vazia, mas o tipo de retorno sempre deve ser definido, devendo-se utilizar o tipo Unit
no caso de funções que não possuem retorno.
Pode-se ainda opcionalmente definir um tipo de receptor, que indicará sobre quais objetos tal função pode ser invocada. Deve ser especificado antes da notação usual e separado por um ponto: A.(B) -> C
, indicando que objetos do tipo A
permitem que a função seja invocada com o parâmetro B
e retorne um valor do tipo C
.
Para instanciar uma função, podemos utilizar literais de funções, como expressões lambda e funções anônimas. Literais de funções permitem representar funções diretamente como uma expressão, sem a necessidade de declará-las.
Expressões lambda
editarExpressões lambda possuem a seguinte sintaxe:
val paridade: (Int) -> Int = { x: Int -> x%2 }
Expressões lambdas sempre devem estar cercadas por chaves, sendo o operador ->
o delimitador entre a declaração dos parâmetros e o corpo da função. A declaração de tipos precedendo a expressão lambda pode ser omitida se mantidos os tipos na definição dos nomes das variáveis:
val paridade = { x: Int -> x%2 }
O tipo de retorno será inferido da última (possivelmente única) expressão do corpo da função, nunca sendo admitido Unit
.
No caso de expressões lambdas com apenas um único parâmetro, podemos ainda omiti-lo na assinatura da função e referir-se a ele pelo nome it
, declarado implicitamente:
val paridade: (Int) -> Int = { it%2 }
Lidando com coleções, o tipo dos parâmetros pode ser inferido diretamente do tipo da coleção, dispensando também a declaração de tipos que precede a expressão, como por exemplo na manipulação de listas.
Para declarações em que queremos informar o tipo de receptor, utilizamos a palavra reservada this
para nos referir ao objeto sobre o qual a função está sendo invocada:
val modulo: Int.(Int) -> Int = { mod -> this%mod }
Funções anônimas
editarFunções anônimas se assemelham muito a uma declaração regular de funções, mas com a exceção de que omitimos seu nome. Sua sintaxe é a seguinte:
val paridade = fun(x: Int): Int = x%2
Seu corpo pode ainda ser um bloco de execução:
val paridade = fun(x: Int): Int {
return x%2
}
Assim como funções regulares e expressões lambdas, o tipo dos parâmetros pode ser omitido se for possível inferir pelo contexto, sendo que o tipo Unit
pode ser assumido como tipo de retorno no caso funções anônimas com o corpo em formato de bloco de execução.
Exemplos concretos
editarMúltiplas funcionalidades e frameworks da linguagem Kotlin aproveitam a expressividade de lambdas para fornecer interfaces mais simples e intuitivas.
Por exemplo, para medir o tempo de execução de bloco de código, pode-se usar a função measureTimeMillis:
inline fun measureTimeMillis(block: () -> Unit): Long
A função recebe um único argumento: block
, que tem tipo () -> Unit
, ou seja, block
é uma função que não recebe nenhum argumento (não terá ->
) e retorna Unit
. Na prática, block
é nada mais que um bloco de código, como abaixo:
val timeInMillis = measureTimeMillis {
var x = 0
for (i in 1..100000000) {
x++
println(x)
}
}
Note que não colocamos o argumento de measureTimeMillis
entre parênteses. Poderíamos ter feito isso, mas em Kotlin podemos usar apenas as chaves quando o único argumento da função é uma expressão lambda.
Essa sintaxe é ainda mais útil em frameworks mais complexos, como o Ktor. Um servidor muito simples pode ser implementado da seguinte forma usando o framework Ktor:
routing {
get("/") {
call.respondText("Hello World!")
}
}
É possível ver a implementação de get
e outras funções disponibilizadas pelo Ktor:
public fun Route.get(path: String, body: RoutingHandler): Route {
return route(path, HttpMethod.Get) { handle(body) }
}
Sem entrar em mais detalhes de implementação, é claro como o framework simplifica a implementação do servidor: get
recebe o caminho da rota e RoutingHandler
, um tipo que representa uma função que executará os comandos para o comportamento esperado da requisição HTTP.
Manipulação de listas
editarEm Kotlin, a interface List define vários métodos que podem ser usados para manipular "coleções ordenadas de elementos".
As ferramentas funcionais da linguagem Kotlin (em particular usar expressões lambda como argumentos predicados) permitem realizar essa manipulação de maneira bastante expressiva para classes Iterable. Alguns exemplos são os seguintes métodos:
all
: tem como parâmetro um predicado e retorna um valor Boolean indicando se todos os elementos da lista satisfazem esse predicado.
val lista = listOf<Int>(1,3,5)
println(lista.all{ it%2==1 })
// saída: true
any
: tem como parâmetro um predicado e retorna um valor Boolean indicando se algum dos elementos da lista satisfaz esse predicado.
val lista = listOf<Int>(2,4,5)
println(lista.any{ it%2==1 })
// saída: true
count
: tem como parâmetro um predicado e retorna um valor Boolean indicando quantos elementos da lista satisfazem esse predicado.
val lista = listOf<Int>(2,3,5)
println(lista.count{ it%2==1 })
// saída: 2
distinctBy
: seleciona apenas elementos com imagens distintos segundo uma função. Se mais de um elemento tiver a mesma imagem, apenas o primeiro é mantido.
val lista = listOf<String>("abc","ids","cavalo")
println(lista.distinctBy{ it.length })
// saída: [abc, cavalo]
filter
: tem como parâmetro um predicado e retorna uma lista apenas com os elementos da lista que satisfazem esse predicado.
val lista = listOf<Int>(1,2,3,5)
println(lista.filter{ it%2==1 })
// saída: [1, 3, 5]
fold
: acumula os valores da lista a partir da esquerda usando um valor inicial e uma função de acúmulo.
val lista = listOf<Int>(0,2,10)
val soma = lista.
fold(0, {acc, i -> acc + i})
println(soma)
// saída: 12
groupBy
: similar aodistinctBy
mas retorna um Map que leva elementos da imagem da função de escolha nos elementos originais da lista.
val lista = listOf<String>("abc","ids","cavalo")
println(lista.groupBy{ it.size })
// saída: {3=[abc, ids], 6=[cavalo]}
map
: aplica a função a todos os elementos da lista.
val lista = listOf<String>("abc","ids","cavalo")
println(lista.map{ it+"!" })
// saída: [abc!, ids!, cavalo!]
partition
: tem como parâmetro um predicado e retorna um Pair com o primeiro elemento do Pair sendo um lista com os elementos da lista original que tornam o predicado verdadeiro e o segundo elemento do Pair a lista daqueles que tornam o predicado falso.
val lista = listOf<String>("abc","ids","cavalo")
println(lista.partition {it.length % 2 == 0)
// saída: ([cavalo], [abc, ids])
scan
: similar aofold
mas retorna uma lista com todos os valores intermediários da operação.
val lista = listOf<Int>(2,10)
val somasParciais = lista.
scan(0, {acc, i -> acc + i})
println(somasParciais)
// saída: [0, 2, 12]
sortedBy
: ordena a lista de acordo com a ordem natural dos valores de retorno da função passada.
val lista = listOf<Int>(-1,2,-3)
println(lista.sortedBy{ if (it<0) -it else it })
// saída: [-1, 2, -3]
zip
: retorna uma lista de Pair para os valores combinados de duas listas.
val listaString = listOf<String>("abc", "paulo", "ids", "cavalo")
val listaInt = listOf<Int>(2,1,3)
val listaCombinada = listaString.zip(listaInt) {
str, i ->
str + i.toString()
}
println(listaCombinada)
// saída: [abc2, paulo1, ids3]
Iterando com forEach
editar
Um método bastante útil é o forEach
, que aplica uma determinada função aos elementos da lista.
De forma simples, ele pode substituir a iteração "tradicional" em blocos de código por um método de listas, o que torna o código mais expressivo e encapsulado principalmente no caso de laços que fazem operações muito simples.
Em vez de:
val nomes = listOf<String>("paulo", "caio", "outro nome")
for (nome in nomes) {
println("Oi, $nome!")
}
Podemos fazer:
val nomes = listOf<String>("paulo", "caio", "outro nome")
nomes.forEach {
println("Oi, $it!")
}