Treinamento do Kotlin para programadores 6: manipulação funcional

Este codelab faz parte do curso de treinamento do Kotlin para programadores. Você vai aproveitar mais este curso se fizer os codelabs em sequência. Dependendo do seu conhecimento, talvez você possa passar mais rápido por algumas seções. Este curso é destinado a programadores que conhecem uma linguagem orientada a objetos e querem aprender Kotlin.

Introdução

Este é o último codelab do Bootcamp do Kotlin. Neste codelab, você vai aprender sobre anotações e quebras rotuladas. Você vai revisar lambdas e funções de ordem superior, que são partes importantes do Kotlin. Você também vai aprender mais sobre funções inlining e interfaces de método abstrato único (SAM, na sigla em inglês). Por fim, você vai saber mais sobre a biblioteca padrão do Kotlin.

Em vez de criar um único app de exemplo, as lições deste curso foram criadas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções com que já está familiarizado. Para unificar os exemplos, muitos deles usam um tema de aquário. Se quiser conferir a história completa do aquário, acesse o curso da Udacity Treinamento do Kotlin para programadores (link em inglês).

O que você já precisa saber

  • A sintaxe de funções, classes e métodos do Kotlin
  • Como criar uma nova classe no IntelliJ IDEA e executar um programa
  • Noções básicas sobre lambdas e funções de ordem superior.

O que você vai aprender

  • Noções básicas de anotações
  • Como usar intervalos rotulados
  • Mais detalhes sobre funções de ordem superior
  • Sobre interfaces de Método Abstrato Simples (SAM)
  • Sobre a biblioteca padrão do Kotlin

Atividades deste laboratório

  • Crie uma anotação simples.
  • Use uma quebra rotulada.
  • Revise as funções lambda no Kotlin.
  • Usar e criar funções de ordem superior.
  • Chame algumas interfaces de Método Abstrato Único.
  • Usar algumas funções da biblioteca padrão do Kotlin.

As anotações são uma forma de anexar metadados ao código e não são específicas do Kotlin. As anotações são lidas pelo compilador e usadas para gerar código ou lógica. Muitos frameworks, como Ktor e Kotlinx, além do Room, usam anotações para configurar como eles são executados e interagem com seu código. É improvável que você encontre anotações até começar a usar frameworks, mas é útil saber como ler uma anotação.

Também há anotações disponíveis na biblioteca padrão do Kotlin que controlam a forma como o código é compilado. Eles são muito úteis se você estiver exportando Kotlin para código Java, mas, caso contrário, não são necessários com tanta frequência.

As anotações vêm logo antes do que é anotado, e a maioria das coisas pode ser anotada: classes, funções, métodos e até mesmo estruturas de controle. Algumas anotações podem usar argumentos.

Confira um exemplo de algumas anotações.

@file:JvmName("InteropFish")
class InteropFish {
   companion object {
       @JvmStatic fun interop()
   }
}

Isso significa que o nome exportado desse arquivo é InteropFish com a anotação JvmName, que está usando um argumento de "InteropFish".JvmName No objeto complementar, @JvmStatic informa ao Kotlin para transformar interop() em uma função estática em InteropFish.

Também é possível criar suas próprias anotações, mas isso é mais útil se você estiver escrevendo uma biblioteca que precisa de informações específicas sobre classes em tempo de execução, ou seja, reflexão.

Etapa 1: criar um novo pacote e arquivo

  1. Em src, crie um novo pacote, example.
  2. Em example, crie um novo arquivo Kotlin, Annotations.kt.

Etapa 2: criar sua própria anotação

  1. Em Annotations.kt, crie uma classe Plant com dois métodos, trim() e fertilize().
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. Crie uma função que imprima todos os métodos em uma classe. Use ::class para receber informações sobre uma classe durante a execução. Use declaredMemberFunctions para receber uma lista dos métodos de uma classe. Para acessar esse recurso, importe kotlin.reflect.full.*.
import kotlin.reflect.full.*    // required import

class Plant {
    fun trim(){}
    fun fertilize(){}
}

fun testAnnotations() {
    val classObj = Plant::class
    for (m in classObj.declaredMemberFunctions) {
        println(m.name)
    }
}
  1. Crie uma função main() para chamar sua rotina de teste. Execute o programa e observe a saída.
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. Crie uma anotação simples, ImAPlant.
annotation class ImAPlant

Isso não faz nada além de dizer que ele foi anotado.

  1. Adicione a anotação na frente da classe Plant.
@ImAPlant class Plant{
    ...
}
  1. Mude testAnnotations() para imprimir todas as anotações de uma classe. Use annotations para receber todas as anotações de uma classe. Execute o programa e observe o resultado.
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. Mude testAnnotations() para encontrar a anotação ImAPlant. Use findAnnotation() para encontrar uma anotação específica. Execute o programa e observe o resultado.
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

Etapa 3: criar uma anotação segmentada

As anotações podem segmentar getters ou setters. Quando isso acontecer, aplique-os com o prefixo @get: ou @set:. Isso acontece muito ao usar frameworks com anotações.

  1. Declare duas anotações, OnGet, que só pode ser aplicada a getters de propriedades, e OnSet, que só pode ser aplicada a setters de propriedades. Use @Target(AnnotationTarger.PROPERTY_GETTER) ou PROPERTY_SETTER em cada um.
annotation class ImAPlant

@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet

@ImAPlant class Plant {
    @get:OnGet
    val isGrowing: Boolean = true

    @set:OnSet
    var needsFood: Boolean = false
}

As anotações são muito úteis para criar bibliotecas que inspecionam coisas em tempo de execução e, às vezes, em tempo de compilação. No entanto, o código de aplicativo típico usa apenas anotações fornecidas por frameworks.

O Kotlin tem várias maneiras de controlar o fluxo. Você já conhece o return, que retorna de uma função para a função de inclusão. Usar um break é como return, mas para loops.

O Kotlin oferece controle adicional sobre loops com o que é chamado de interrupção rotulada. Um break qualificado com um rótulo pula para o ponto de execução logo após o loop marcado com esse rótulo. Isso é particularmente útil ao lidar com loops aninhados.

Qualquer expressão em Kotlin pode ser marcada com um rótulo. Os rótulos têm a forma de um identificador seguido pelo sinal @.

  1. Em Annotations.kt, teste uma interrupção rotulada saindo de um loop interno.
fun labels() {
    outerLoop@ for (i in 1..100) {
         print("$i ")
         for (j in 1..100) {
             if (i > 10) break@outerLoop  // breaks to outer loop
        }
    }
}

fun main() {
    labels()
}
  1. Execute o programa e observe a saída.
⇒ 1 2 3 4 5 6 7 8 9 10 11 

Da mesma forma, você pode usar um continue rotulado. Em vez de sair do loop rotulado, o comando "continue" rotulado passa para a próxima iteração do loop.

Lambdas são funções anônimas, ou seja, funções sem nome. É possível atribuir esses valores a variáveis e transmiti-los como argumentos para funções e métodos. Elas são extremamente úteis.

Etapa 1: criar uma função lambda simples

  1. Inicie o REPL no IntelliJ IDEA: Tools > Kotlin > Kotlin REPL.
  2. Crie uma lambda com um argumento, dirty: Int, que faz um cálculo, dividindo dirty por 2. Atribua a lambda a uma variável, waterFilter.
val waterFilter = { dirty: Int -> dirty / 2 }
  1. Chame waterFilter, transmitindo o valor 30.
waterFilter(30)
⇒ res0: kotlin.Int = 15

Etapa 2: criar uma função lambda de filtro

  1. Ainda no REPL, crie uma classe de dados, Fish, com uma propriedade, name.
data class Fish(val name: String)
  1. Crie uma lista de três Fish com os nomes Flipper, Moby Dick e Dory.
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. Adicione um filtro para verificar nomes que contenham a letra "i".
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

Na expressão lambda, it se refere ao elemento da lista atual, e o filtro é aplicado a cada elemento da lista por vez.

  1. Aplique joinString() ao resultado, usando ", " como separador.
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

A função joinToString() cria uma string unindo os nomes filtrados, separados pela string especificada. É uma das muitas funções úteis integradas à biblioteca padrão do Kotlin.

Transmitir uma lambda ou outra função como argumento para uma função cria uma função de ordem superior. O filtro acima é um exemplo simples disso. filter() é uma função, e você transmite a ela uma lambda que especifica como processar cada elemento da lista.

Escrever funções de ordem superior com lambdas de extensão é uma das partes mais avançadas da linguagem Kotlin. Leva um tempo para aprender a escrever, mas eles são muito convenientes de usar.

Etapa 1: criar uma turma

  1. No pacote example, crie um arquivo Kotlin chamado Fish.kt.
  2. Em Fish.kt, crie uma classe de dados Fish com uma propriedade, name.
data class Fish (var name: String)
  1. Crie uma função fishExamples(). Em fishExamples(), crie um peixe chamado "splashy", tudo em minúsculas.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. Crie uma função main() que chame fishExamples().
fun main () {
    fishExamples()
}
  1. Compile e execute o programa clicando no triângulo verde à esquerda de main(). Ainda não há saída.

Etapa 2: usar uma função de ordem superior

A função with() permite fazer uma ou mais referências a um objeto ou propriedade de maneira mais compacta. Uso do this. with() é uma função de ordem superior, e na lambda você especifica o que fazer com o objeto fornecido.

  1. Use with() para colocar em maiúscula o nome do peixe em fishExamples(). Dentro das chaves, this se refere ao objeto transmitido para with().
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. Não há saída, então adicione um println() ao redor dela. O this é implícito e não é necessário, então você pode removê-lo.
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

Etapa 3: criar uma função de ordem superior

Por baixo dos panos, with() é uma função de ordem superior. Para ver como isso funciona, crie sua própria versão muito simplificada de with() que funcione apenas para strings.

  1. Em Fish.kt, defina uma função, myWith(), que usa dois argumentos. Os argumentos são o objeto em que a operação será realizada e uma função que define a operação. A convenção para o nome do argumento com a função é block. Nesse caso, a função não retorna nada, o que é especificado com Unit.
fun myWith(name: String, block: String.() -> Unit) {}

Em myWith(), block() agora é uma função de extensão de String. A classe que está sendo estendida é geralmente chamada de objeto receptor. Portanto, name é o objeto receptor neste caso.

  1. No corpo de myWith(), aplique a função transmitida, block(), ao objeto receptor, name.
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. Em fishExamples(), substitua with() por myWith().
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name é o argumento de nome, e println(capitalize()) é a função de bloqueio.

  1. Execute o programa, e ele vai funcionar como antes.
⇒ Splashy

Etapa 4: conhecer mais extensões integradas

A lambda de extensão with() é muito útil e faz parte da biblioteca padrão do Kotlin (link em inglês). Confira alguns outros que podem ser úteis: run(), apply() e let().

A função run() é uma extensão que funciona com todos os tipos. Ela usa uma lambda como argumento e retorna o resultado da execução dela.

  1. Em fishExamples(), chame run() em fish para receber o nome.
fish.run {
   name
}

Isso apenas retorna a propriedade name. Você pode atribuir isso a uma variável ou imprimir. Este não é um exemplo útil, já que você pode acessar a propriedade, mas run() pode ser útil para expressões mais complicadas.

A função apply() é semelhante a run(), mas retorna o objeto alterado a que foi aplicada em vez do resultado da lambda. Isso pode ser útil para chamar métodos em um objeto recém-criado.

  1. Faça uma cópia de fish e chame apply() para definir o nome da nova cópia.
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

A função let() é semelhante a apply(), mas retorna uma cópia do objeto com as mudanças. Isso pode ser útil para encadear manipulações.

  1. Use let() para receber o nome de fish, coloque em maiúsculas, concatene outra string, receba o comprimento desse resultado, adicione 31 ao comprimento e imprima o resultado.
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

Neste exemplo, o tipo de objeto referenciado por it é Fish, depois String, depois String novamente e, por fim, Int.

  1. Imprima fish depois de chamar let() e você verá que ele não mudou.
println(fish.let { it.name.capitalize()}
    .let{it + "fish"}
    .let{it.length}
    .let{it + 31})
println(fish)
⇒ 42
Fish(name=splashy)

Lambdas e funções de ordem superior são muito úteis, mas há algo que você precisa saber: lambdas são objetos. Uma expressão lambda é uma instância de uma interface Function, que é um subtipo de Object. Considere o exemplo anterior de myWith().

myWith(fish.name) {
    capitalize()
}

A interface Function tem um método, invoke(), que é substituído para chamar a expressão lambda. Escrito à mão, ele ficaria parecido com o código abaixo.

// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
    override fun invoke(name: String) {
        name.capitalize()
    }
})

Normalmente, isso não é um problema, porque a criação de objetos e a chamada de funções não geram muito overhead, ou seja, tempo de memória e CPU. Mas se você estiver definindo algo como myWith(), que é usado em todos os lugares, a sobrecarga pode aumentar.

O Kotlin oferece inline como uma maneira de lidar com esse caso para reduzir a sobrecarga durante a execução, adicionando um pouco mais de trabalho para o compilador. Você aprendeu um pouco sobre inline na lição anterior sobre tipos concretos. Marcar uma função como inline significa que, sempre que ela for chamada, o compilador vai transformar o código-fonte para "inline" a função. Ou seja, o compilador vai mudar o código para substituir a lambda pelas instruções dentro dela.

Se myWith() no exemplo acima estiver marcado com inline:

inline myWith(fish.name) {
    capitalize()
}

ele é transformado em uma chamada direta:

// with myWith() inline, this becomes
fish.name.capitalize()

É importante observar que a inclusão inline de funções grandes aumenta o tamanho do código. Portanto, é melhor usar esse recurso em funções simples que são usadas muitas vezes, como myWith(). As funções de extensão das bibliotecas que você aprendeu antes são marcadas como inline. Assim, não é preciso se preocupar com a criação de objetos extras.

Método Abstrato Único significa apenas uma interface com um método. Elas são muito comuns ao usar APIs escritas na linguagem de programação Java. Por isso, existe uma sigla para elas: SAM. Alguns exemplos são Runnable, que tem um único método abstrato, run(), e Callable, que tem um único método abstrato, call().

No Kotlin, você precisa chamar funções que usam SAMs como parâmetros o tempo todo. Teste o exemplo abaixo.

  1. Em example, crie uma classe Java, JavaRun, e cole o seguinte no arquivo.
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

O Kotlin permite instanciar um objeto que implementa uma interface precedendo o tipo com object:. É útil para transmitir parâmetros para SAMs.

  1. De volta ao Fish.kt, crie uma função runExample(), que cria um Runnable usando object:. O objeto precisa implementar run() imprimindo "I'm a Runnable".
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. Chame JavaRun.runNow() com o objeto criado.
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. Chame runExample() de main() e execute o programa.
⇒ I'm a Runnable

É muito trabalho para imprimir algo, mas é um bom exemplo de como um SAM funciona. Claro, o Kotlin oferece uma maneira mais simples de fazer isso: use uma lambda no lugar do objeto para tornar esse código muito mais compacto.

  1. Remova o código em runExample, mude para chamar runNow() com uma lambda e execute o programa.
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. Você pode deixar isso ainda mais conciso usando a sintaxe de chamada do último parâmetro e se livrar dos parênteses.
fun runExample() {
    JavaRun.runNow {
        println("Last parameter is a lambda as a Runnable")
    }
}
⇒ Last parameter is a lambda as a Runnable

Esses são os conceitos básicos de um SAM, um método abstrato único. É possível instanciar, substituir e fazer uma chamada para um SAM com uma linha de código usando o padrão:
Class.singleAbstractMethod { lambda_of_override }

Esta lição revisou lambdas e abordou com mais profundidade as funções de ordem superior, que são partes importantes do Kotlin. Você também aprendeu sobre anotações e quebras rotuladas.

  • Use anotações para especificar coisas ao compilador. Exemplo:
    @file:JvmName("Foo")
  • Use interrupções rotuladas para permitir que seu código saia de loops aninhados. Exemplo:
    if (i > 10) break@outerLoop // breaks to outerLoop label
  • Os lambdas podem ser muito eficientes quando combinados com funções de ordem superior.
  • Lambdas são objetos. Para evitar a criação do objeto, marque a função com inline. O compilador vai colocar o conteúdo da lambda diretamente no código.
  • Use inline com cuidado, mas ele pode ajudar a reduzir o uso de recursos pelo seu programa.
  • SAM, Single Abstract Method, é um padrão comum, simplificado com lambdas. O padrão básico é:
    Class.singleAbstractMethod { lamba_of_override }
  • A biblioteca padrão do Kotlin (link em inglês) oferece várias funções úteis, incluindo várias SAMs. Por isso, conheça o que ela tem.

Há muito mais em Kotlin do que foi abordado no curso, mas agora você tem o básico para começar a desenvolver seus próprios programas em Kotlin. Esperamos que você esteja animado com essa linguagem expressiva e queira criar mais funcionalidades escrevendo menos código, principalmente se você estiver migrando da linguagem de programação Java. Praticar e aprender enquanto você avança é a melhor maneira de se tornar um especialista em Kotlin. Por isso, continue explorando e aprendendo sobre Kotlin por conta própria.

Documentação do Kotlin

Se você quiser mais informações sobre algum tópico deste curso ou se tiver dúvidas, https://kotlinlang.org é o melhor ponto de partida.

Tutoriais do Kotlin

O site https://try.kotlinlang.org inclui tutoriais avançados chamados Kotlin Koans, um interpretador baseado na Web e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para conferir o curso da Udacity sobre esse tema, consulte Treinamento do Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA está disponível no site da JetBrains.

Biblioteca padrão do Kotlin

A biblioteca padrão do Kotlin (link em inglês) oferece várias funções úteis. Antes de escrever sua própria função ou interface, sempre verifique a biblioteca padrão para saber se alguém já fez isso por você. Confira de vez em quando, porque novas funcionalidades são adicionadas com frequência.

Tutoriais do Kotlin

O site https://try.kotlinlang.org inclui tutoriais avançados chamados Kotlin Koans, um interpretador baseado na Web e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para conferir o curso da Udacity sobre esse tema, consulte Treinamento do Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA está disponível no site da JetBrains.

Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:

  • Atribuir o dever de casa, se necessário.
  • Informar aos alunos como enviar deveres de casa.
  • Atribuir nota aos deveres de casa.

Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.

Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.

Responda estas perguntas

Pergunta 1

Em Kotlin, SAM significa:

▢ Correspondência de argumentos segura

▢ Método de acesso simples

▢ Método abstrato único

▢ Metodologia de acesso estratégico

Pergunta 2

Qual das opções a seguir não é uma função de extensão da biblioteca padrão do Kotlin?

elvis()

apply()

run()

with()

Pergunta 3

Qual das seguintes opções não é verdadeira sobre lambdas em Kotlin?

▢ Lambdas são funções anônimas.

▢ Lambdas são objetos, a menos que sejam inlines.

▢ Lambdas consomem muitos recursos e não devem ser usados.

▢ As lambdas podem ser transmitidas para outras funções.

Pergunta 4

Os rótulos em Kotlin são indicados com um identificador seguido de:

:

::

@:

@

Parabéns! Você concluiu o codelab do Treinamento do Kotlin para programadores.

Para ter uma visão geral do curso, incluindo links para outros codelabs, consulte "Treinamento do Kotlin para programadores: seja bem-vindo ao curso".