Meus exemplos favoritos de programação funcional no Kotlin

Um dos grandes recursos do Kotlin é que ele suporta programação funcional. Vamos dar uma olhada e discutir algumas funções simples, mas expressivas, escritas em Kotlin.


Meus exemplos favoritos de programação funcional no Kotlin


Trabalhar com coleções


O Kotlin suporta trabalho conveniente com coleções. Existem muitos recursos diferentes. Suponha que criemos algum sistema para uma universidade. Precisamos encontrar os melhores alunos que merecem uma bolsa de estudos. Temos o seguinte modelo de Student :


 class Student( val name: String, val surname: String, val passing: Boolean, val averageGrade: Double ) 

Agora podemos chamar a seguinte função para obter uma lista dos dez melhores alunos que atendem a todos os critérios:


 students.filter { it.passing && it.averageGrade > 4.0 } // 1 .sortedBy { it.averageGrade } // 2 .take(10) // 3 .sortedWith(compareBy({ it.surname }, { it.name })) // 4 

  • Deixamos apenas os alunos que passaram no exame, cuja pontuação média é superior a 4,0.
  • Classifique-os pela pontuação média.
  • Deixamos os dez primeiros alunos.
  • Classifique-os em ordem alfabética. O comparador primeiro compara os sobrenomes e, se forem iguais, compara os nomes.

E se, em vez de ordem alfabética, precisarmos manter a ordem original dos alunos? Podemos fazer isso usando índices:


 students.filter { it.passing && it.averageGrade > 4.0 } .withIndex() // 1 .sortedBy { (i, s) -> s.averageGrade } // 2 .take(10) .sortedBy { (i, s) -> i } // 3 .map { (i, s) -> s } // 4 

  • Vincule o índice de iteração atual a cada elemento.
  • Classifique pela pontuação média e deixe os dez primeiros alunos.
  • Classifique novamente, mas agora por índice.
  • Excluímos índices e deixamos apenas alunos.

Este exemplo demonstra como é simples e intuitivo o trabalho com coleções no Kotlin.


Trabalhar com coleções no Kotlin


SuperSet (Booleano)


Se você estudou álgebra em uma universidade, pode se lembrar do que é um superconjunto. Para qualquer conjunto, seu superconjunto é o conjunto de todos os seus subconjuntos, incluindo o próprio conjunto original e o conjunto vazio. Por exemplo, se tivermos o seguinte conjunto:


{1,2,3}


Esse é o seu superconjunto:


{{}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}}


Na álgebra, essa função é muito útil. Como a implementamos?


Se você quiser se desafiar, pare de ler agora e tente resolver esse problema sozinho.


Vamos começar nossa análise com uma simples observação. Se pegarmos algum elemento do conjunto (por exemplo, 1), o superconjunto terá um número igual de conjuntos com esse elemento ({1}, {1,2}, {1,3}, {1,2,3}) e sem ele ({}, {2}, {3}, {2,3}).


Observe que o segundo conjunto é um superconjunto ({2,3}) e o primeiro é um superconjunto ({2,3}) com o nosso elemento adicionado (1) a cada conjunto. Assim, podemos calcular o superconjunto pegando o primeiro elemento, calculando o superconjunto para todos os outros e retornando a soma do resultado e do resultado com a adição do primeiro elemento a cada conjunto:


 fun <T> powerset(set: Set<T>): Set<Set<T>> { val first = set.first() val powersetOfRest = powerset(set.drop(1)) return powersetOfRest.map { it + first } + powersetOfRest } 

Mas esse método não funcionará. O problema é um conjunto vazio: first lançará um erro quando o conjunto estiver vazio. Aqui, a definição de um superconjunto vem para o resgate - o superconjunto de um conjunto vazio é um conjunto vazio: powerset ({}) = {{}}. Aqui está a aparência do algoritmo corrigido:


 fun <T> powerset(set: Set<T>): Set<Set<T>> = if (set.isEmpty()) setOf(emptySet()) else { val powersetOfRest = powerset(set.drop(1)) powersetOfRest + powersetOfRest.map { it + set.first() } } 

Meme da recursão


Vamos ver como isso funciona. Suponha que precisamos calcular o conjunto de poderes ({1,2,3}). O algoritmo agirá da seguinte maneira:


powerset ({1,2,3}) = powerset ({2,3}) + powerset ({2,3}). map {it + 1}


conjunto de poderes ({2,3}) = conjunto de poderes ({3}) + conjunto de poderes ({3}). map {it + 2}


powerset ({3}) = conjunto de poderes ({}) + conjunto de poderes ({}). map {it + 3}


powerset ({}) = {{}}


powerset ({3}) = {{}, {3}}


powerset ({2,3}) = {{}, {3}} + {{2}, {2, 3}} = {{}, {2}, {3}, {2, 3}}


powerset ({1,2,3}) = {{}, {2}, {3}, {2, 3}} + {{1}, {1, 2}, {1, 3}, {1, 2, 3}} = {{}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}}


Mas podemos melhorar ainda mais nossa função. Vamos usar a função let para tornar a notação mais curta e mais compacta:


 fun <T> powerset(set: Set<T>): Set<Set<T>> = if (set.isEmpty()) setOf(emptySet()) else powerset(set.drop(1)) .let { it+ it.map { it + set.first() } 

Também podemos definir essa função como uma função de extensão para Collection , para que possamos usá-la como se fosse o setOf(1,2,3).powerset() Set ( setOf(1,2,3).powerset() vez de powerset(setOf(1,2,3)) ):


 fun <T> Collection<T>.powerset(): Set<Set<T>> = if (isEmpty()) setOf(emptySet()) else drop(1) .powerset() .let { it+ it.map { it + first() } 

Também podemos reduzir os efeitos negativos da recursão criada. Na implementação acima, o estado do superconjunto cresce a cada iteração (com cada chamada recursiva), porque o estado da iteração anterior deve ser armazenado na memória.


Em vez disso, poderíamos usar um loop regular ou tailrec função tailrec . Usaremos a segunda opção para manter a legibilidade da função. O modificador tailrec permite apenas uma chamada recursiva na última linha de função executada. Veja como podemos mudar nossa função para usá-la com mais eficiência:


 fun <T> Collection<T>.powerset(): Set<Set<T>> = powerset(this, setOf(emptySet())) private tailrec fun <T> powerset(left: Collection<T>, acc: Set<Set<T>>): Set<Set<T>> = if (left.isEmpty()) acc else powerset(left.drop(1), acc + acc.map { it + left.first() }) 

Essa implementação faz parte da biblioteca KotlinDiscreteMathToolkit , que define muitas outras funções usadas em matemática discreta.


Quicksort


Hora do exemplo mais interessante. Você verá como um problema complexo pode ser simplificado e tornado legível usando o estilo e as ferramentas de programação funcional.


Implementamos um algoritmo de classificação rápida. O algoritmo é simples: selecionamos algum elemento (pivô ( barra russa)) e distribuímos todos os outros elementos em duas listas: uma lista com elementos maiores que a barra e menor. Em seguida, classificamos recursivamente esses subarrays. Finalmente, combinamos uma lista classificada de elementos menores, uma barra e uma lista classificada de elementos maiores. Para simplificar, considere o primeiro elemento como uma barra. Aqui está a implementação completa:


 fun <T : Comparable<T>> List<T>.quickSort(): List<T> = if(size < 2) this else { val pivot = first() val (smaller, greater) = drop(1).partition { it <= pivot} smaller.quickSort() + pivot + greater.quickSort() } // Usage listOf(2,5,1).quickSort() // [1,2,5] 

Parece lindo, certo? Essa é a beleza da programação funcional.


Programação funcional


O primeiro problema com essa função é o tempo de execução. Ela é completamente não otimizada. Mas é curto e fácil de ler.


Se você precisar de uma função otimizada, poderá usá-la na biblioteca Java padrão. É baseado em vários algoritmos, dependendo de certas condições, e é escrito de forma nativa. Isso deve ser muito mais eficiente. Mas quanto exatamente? Vamos comparar essas duas funções. Vamos classificar várias matrizes diferentes com elementos aleatórios e comparar o tempo de execução:


 val r = Random() listOf(100_000, 1_000_000, 10_000_000) .asSequence() .map { (1..it).map { r.nextInt(1000000000) } } .forEach { list: List<Int> -> println("Java stdlib sorting of ${list.size} elements took ${measureTimeMillis { list.sorted() }}") println("quickSort sorting of ${list.size} elements took ${measureTimeMillis { list.quickSort() }}") } 

Aqui estão os resultados que obtivemos:


A classificação Java stdlib de 100000 elementos levou 83
A classificação quickSort de 100000 elementos levou 163
A classificação Java stdlib de 1.000.000 de elementos levou 558
A classificação quickSort de 1.000.000 de elementos levou 859
A classificação stdlib de Java de 10.000.000 de elementos levou 6182
A classificação quickSort de 10.000.000 de elementos levou 12133


Como você pode ver, a função quickSort quase duas vezes mais lenta. Mesmo para grandes listas. Em casos normais, a diferença será tipicamente de 0,1 ms a 0,2 ms. Isso explica por que, em alguns casos, podemos usar uma função um pouco menos otimizada, mas bem legível e simples.

Source: https://habr.com/ru/post/pt420299/


All Articles