Mis ejemplos favoritos de programaci贸n funcional en Kotlin

Una de las grandes caracter铆sticas de Kotlin es que admite programaci贸n funcional. Echemos un vistazo y analicemos algunas funciones simples pero expresivas escritas en Kotlin.


Mis ejemplos favoritos de programaci贸n funcional en Kotlin


Trabaja con colecciones


Kotlin admite trabajos convenientes con colecciones. Hay muchas caracter铆sticas diferentes. Supongamos que creamos un sistema para una universidad. Necesitamos encontrar a los mejores estudiantes que merecen una beca. Tenemos el siguiente modelo de Student :


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

Ahora podemos llamar a la siguiente funci贸n para obtener una lista de los diez mejores estudiantes que cumplen con todos los criterios:


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

  • Dejamos solo a los estudiantes que aprobaron el examen, cuyo puntaje promedio es m谩s de 4.0.
  • Ord茅nelos por el puntaje promedio.
  • Dejamos a los primeros diez estudiantes.
  • Clasif铆calos alfab茅ticamente. El comparador primero compara los apellidos, y si son iguales, entonces compara los nombres.

驴Qu茅 sucede si, en lugar del orden alfab茅tico, necesitamos mantener el orden original de los estudiantes? Podemos hacer esto 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 

  • Vincula el 铆ndice de iteraci贸n actual a cada elemento.
  • Ordenar por el puntaje promedio y dejar a los primeros diez estudiantes.
  • Ordenar de nuevo, pero ahora por 铆ndice.
  • Eliminamos 铆ndices y dejamos solo estudiantes.

Este ejemplo demuestra cu谩n simple e intuitivo es el trabajo con colecciones en Kotlin.


Trabaja con colecciones en Kotlin


SuperSet (booleano)


Si estudiaste 谩lgebra en una universidad, puedes recordar qu茅 es un superconjunto. Para cualquier conjunto, su superconjunto es el conjunto de todos sus subconjuntos, incluido el conjunto original en s铆 y el conjunto vac铆o. Por ejemplo, si tenemos el siguiente conjunto:


{1,2,3}


Ese es su superconjunto:


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


En 谩lgebra, tal funci贸n es muy 煤til. 驴C贸mo lo implementamos?


Si quieres desafiarte a ti mismo, deja de leer ahora mismo y trata de resolver este problema t煤 mismo.


Comencemos nuestro an谩lisis con una simple observaci贸n. Si tomamos alg煤n elemento del conjunto (por ejemplo, 1), el superconjunto tendr谩 un n煤mero igual de conjuntos con este elemento ({1}, {1,2}, {1,3}, {1,2,3}) y sin ella ({}, {2}, {3}, {2,3}).


Tenga en cuenta que el segundo conjunto es un superconjunto ({2,3}), y el primero es un superconjunto ({2,3}) con nuestro elemento agregado (1) a cada conjunto. Por lo tanto, podemos calcular el superconjunto tomando el primer elemento, calculando el superconjunto para todos los dem谩s y devolviendo la suma del resultado y el resultado con la adici贸n del primer 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 } 

Pero este m茅todo no funcionar谩. El problema es un conjunto vac铆o: first arrojar谩 un error cuando el conjunto est茅 vac铆o. Aqu铆 la definici贸n de un superconjunto viene al rescate: el superconjunto de un conjunto vac铆o es un conjunto vac铆o: powerset ({}) = {{}}. As铆 es como se ve el algoritmo corregido:


 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 de recursi贸n


Veamos como funciona. Supongamos que necesitamos calcular powerset ({1,2,3}). El algoritmo actuar谩 de la siguiente manera:


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


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


powerset ({3}) = powerset ({}) + powerset ({}). map {it + 3}


conjunto de potencia ({}) = {{}}


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}}


Pero podemos mejorar nuestra funci贸n a煤n m谩s. Usemos la funci贸n let para hacer la notaci贸n m谩s corta y 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() } 

Tambi茅n podemos definir esta funci贸n como una funci贸n de extensi贸n para Collection , de modo que podamos usar esta funci贸n como si fuera el setOf(1,2,3).powerset() Set ( setOf(1,2,3).powerset() lugar 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() } 

Tambi茅n podemos reducir los efectos negativos de la recursi贸n creada. En la implementaci贸n anterior, el estado del superconjunto crece con cada iteraci贸n (con cada llamada recursiva), porque el estado de la iteraci贸n anterior debe almacenarse en la memoria.


En cambio, podr铆amos usar un bucle regular o tailrec funci贸n tailrec . Utilizaremos la segunda opci贸n para mantener la legibilidad de la funci贸n. El modificador tailrec solo permite una llamada recursiva en la 煤ltima l铆nea de funci贸n ejecutada. As铆 es como podemos cambiar nuestra funci贸n para usarla de manera m谩s eficiente:


 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() }) 

Esta implementaci贸n es parte de la biblioteca KotlinDiscreteMathToolkit , que define muchas otras funciones utilizadas en matem谩ticas discretas.


Quicksort


Tiempo para el ejemplo m谩s interesante. Ver谩 c贸mo un problema complejo puede simplificarse y hacerse legible utilizando las herramientas de estilo y programaci贸n funcional.


Implementamos un algoritmo de ordenaci贸n r谩pida. El algoritmo es simple: seleccionamos alg煤n elemento (pivote ( barra rusa)) y distribuimos todos los dem谩s elementos en dos listas: una lista con elementos m谩s grandes que la barra y m谩s peque帽os. Luego ordenamos recursivamente estas submatrices. Finalmente, combinamos una lista ordenada de elementos m谩s peque帽os, una barra y una lista ordenada de elementos m谩s grandes. Para simplificar, tome el primer elemento como una barra. Aqu铆 est谩 la implementaci贸n 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] 

Se ve hermosa, 驴verdad? Esta es la belleza de la programaci贸n funcional.


Programaci贸n funcional


El primer problema con dicha funci贸n es su tiempo de ejecuci贸n. Ella es completamente inoportuna. Pero es corto y f谩cil de leer.


Si necesita una funci贸n optimizada, puede usar la funci贸n de la biblioteca est谩ndar de Java. Se basa en varios algoritmos dependiendo de ciertas condiciones y est谩 escrito de forma nativa. Esto deber铆a ser mucho m谩s eficiente. 驴Pero cu谩nto exactamente? Comparemos estas dos funciones. Ordenemos varias matrices diferentes con elementos aleatorios y comparemos el tiempo de ejecuci贸n:


 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() }}") } 

Aqu铆 est谩n los resultados que obtuvimos:


La clasificaci贸n de Java stdlib de 100000 elementos tom贸 83
La ordenaci贸n r谩pida de 100000 elementos tom贸 163
La clasificaci贸n de Java stdlib de 1,000,000 de elementos tom贸 558
La clasificaci贸n r谩pida de 1,000,000 de elementos tom贸 859
La clasificaci贸n de Java stdlib de 10,000,000 elementos tom贸 6182
La clasificaci贸n r谩pida de 10,000,000 elementos tom贸 12133


Como puede ver, la funci贸n quickSort casi 2 veces m谩s lenta. Incluso para grandes listas. En casos normales, la diferencia ser谩 t铆picamente de 0.1ms a 0.2ms. Esto explica por qu茅 en algunos casos podemos usar una funci贸n que est谩 ligeramente menos optimizada, pero que es f谩cil de leer y simple.

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


All Articles