Con la transición de DoorDash de un monolito Python a microservicios Kotlin, nuestro equipo de ingeniería se encontró con muchas oportunidades para mejorar la excelencia operativa y continuar con nuestra obsesión por la fiabilidad. Aunque hay muchas maneras de mejorar nuestras métricas de ingeniería a través de hardware optimizado y diseños de sistemas adecuados, una palanca directa de la que podemos tirar como desarrolladores para contribuir a la excelencia general de ingeniería es escribir un código mejor y más limpio a diario.
Y una forma de que los desarrolladores escriban código más limpio es adoptar el paradigma de la programación funcional (PF) utilizando Kotlin. Como lenguaje de programación multiparadigma de propósito general, Kotlin proporciona muchas de las herramientas necesarias para aprovechar la PF en nuestra programación diaria.
En este post, vamos a hablar de lo que es la programación funcional, cuáles son sus beneficios y potenciales desventajas, cómo se compara con el paradigma alternativo de la programación imperativa (PI), lo que Kotlin proporciona a los desarrolladores para aprovechar FP, y ejemplos de cómo en DoorDash escribimos código de estilo FP en Kotlin.
¿Qué es la programación funcional (PF)?
En pocas palabras, FP es un paradigma de programación en el que los programas se construyen aplicando y componiendo funciones. Un programa típico en FP funciona así: Dada una entrada, aplica una serie de funciones (pueden ser grandes o pequeñas) a la entrada para obtener la salida deseada. Esto puede parecer trivial, pero tiene un montón de implicaciones y reglas detrás de cómo se construyen las funciones y cuál es el alcance de los cambios que pueden ser afectados por estas funciones. Juntas, estas implicaciones y reglas son las que hacen de la FP un gran paradigma a considerar.
De todos los conceptos de FP, los tres siguientes son los que más contribuyen a los beneficios de adoptar FP en nuestra programación diaria. (Más adelante veremos con más detalle cómo estos conceptos nos ayudan a escribir un código mejor y más limpio).
- Funciones puras. Por definición, las funciones puras tienen los mismos valores de retorno para la misma entrada, y no tienen efectos secundarios (como la actualización de otras variables locales y la invocación de E/S). Por ejemplo, todas las funciones matemáticas, como suma, máximo y promedio, son funciones puras.
- Estados inmutables. En comparación con los estados mutables con los que estamos familiarizados-como una variable que puede ser reasignada a cualquier valor o un array en el que podemos insertar o eliminar cualquier valor durante el tiempo de ejecución-los estados inmutables no son modificables después de que hayan sido creados o se les haya asignado un valor.
- Composición de funciones. Como sugiere la palabra "composición", la composición de funciones se refiere a la combinación de funciones simples para construir funciones más complicadas. En la práctica, la salida de una función se convierte en la entrada de otra función, que produce una salida que se utiliza para la entrada de otra función, y así sucesivamente.
Es normal o comprensible no haber oído hablar antes de estos conceptos. De hecho, ésta es una de las pocas razones por las que la PF no se utiliza y adopta tan ampliamente como otros paradigmas. Es diferente del otro campo de paradigma de programación, la programación imperativa (PI), que incluye los subparadigmas de programación procedimental y programación orientada a objetos (POO) con los que la mayoría de los desarrolladores están familiarizados. La mayoría de los planes de estudios de informática no cubren la programación imperativa tan extensamente como la programación orientada a objetos (POO), a menudo no se cubre en absoluto. Mientras que muchos cursos de matemáticas cubren los conceptos básicos de la FP, como las funciones puras y la composición, rara vez conectan los puntos entre estos conceptos y cómo se pueden aprovechar en el mundo de la programación.
¿Cómo se compara FP con IP?
Aunque hay muchas áreas de diferencia entre FP e IP, ampliaremos la explicación de Microsoft al comparar entre FP e IP en el contexto de .NET y haremos hincapié en estas tres áreas:
- Enfoque programador. La PI requiere que los programadores piensen en cómo realizar los algoritmos y hacer un seguimiento de los cambios internos de estado para alcanzar el resultado deseado. En FP, sin embargo, los programadores se centran principalmente en tres cosas:
- Cuáles son las entradas
- ¿Cuáles son los resultados deseados?
- Qué transformaciones son necesarias para convertir las entradas en salidas
- Cambios de estado. Básicamente no hay cambios de estado en FP ya que los estados inmutables son el núcleo del paradigma. En IP, sin embargo, los cambios de estado están en todas partes y son cruciales para el flujo de ejecución porque esos cambios de estado son esencialmente la forma en que el programa mantiene un registro de dónde está y qué ejecutar a continuación.
- Control de flujo primario. En FP, las funciones se utilizan para aplicar a colecciones de datos como matrices y mapas para realizar las transformaciones deseadas. Las funciones son ciudadanos de primera clase; por lo tanto, pueden asignarse a valores, pasarse como argumentos y devolverse desde otras funciones. Por otro lado, IP se basa en gran medida en bucles, condicionales y llamadas a funciones (pueden ser puras o no puras) para controlar el flujo del programa y manipular los estados internos para llegar al estado final deseado.
Es evidente que no sólo las metodologías son diferentes entre la FP y la PI, sino que en la práctica la forma de pensar de un programador durante la codificación también es drásticamente diferente. Dicho esto, la FP y la PI no son mutuamente excluyentes. De hecho, muchos lenguajes de programación, como Kotlin, adoptan una mentalidad multiparadigma en la que los programadores son libres de utilizar más de un paradigma en el mismo fragmento de código. Por ejemplo, puesto que Kotlin está diseñado para interoperar plenamente con Java, y Java es principalmente un lenguaje de programación orientada a objetos, cabe esperar que haya muchos ejemplos de programación orientada a objetos en el código de Kotlin. Eso no impide que los programadores apliquen funciones de estilo FP sobre objetos Java, que mostraremos más adelante.
Manténgase informado con las actualizaciones semanales
Suscríbase a nuestro blog de ingeniería para recibir actualizaciones periódicas sobre los proyectos más interesantes en los que trabaja nuestro equipo.
Introduzca una dirección de correo electrónico válida.
Gracias por suscribirse.
Ventajas de escribir código de estilo FP
Ahora que hemos visto las diferencias entre la PF y la PI, más conocida, veamos qué ventajas aporta la PF. En resumen, hay tres ventajas principales:
- Ejecuciones sin efectos secundarios
- Fácil iteración de funciones existentes
- Mayor capacidad de prueba
Ejecuciones sin efectos secundarios
Como se mencionó anteriormente, las funciones puras no garantizan ningún efecto secundario aparte de producir la salida deseada. Las funciones puras no modifican el estado de ninguna de sus entradas, ni modifican el estado de ningún parámetro del sistema. En un sistema altamente complejo como el que DoorDash tiene, esta propiedad es muy valiosa porque, como desarrollador, es beneficioso esperar que una función haga exactamente lo que dice hacer, y no habrá ningún otro efecto secundario al llamar a la función. Cuando varios desarrolladores de equipos de distintos departamentos trabajan en la misma base de código, comprender la lógica del código se convierte en algo sencillo, ya que es fácil leer la serie de funciones que se aplican a la entrada y averiguar qué se está haciendo sin pinchar en todas las funciones individuales.
Fácil iteración de funciones existentes
Como todas las funciones escritas en estilo FP no tendrán efectos secundarios, es mucho más fácil hacer iteraciones sobre funciones y lógica existentes. Por ejemplo, supongamos que existen funciones que realizan una serie de operaciones para calcular el salario base de un Dasher (nuestro término para un conductor) en una entrega. Supongamos que queremos añadir una nueva función tal que el salario base se incremente en un 50% si la entrega se ha realizado en hora punta. Esto será muy fácil de iterar sobre la lógica existente; de hecho, todo lo que necesitamos es añadir una nueva función al final del embudo de cálculo, que multiplique la entrada por 1,5 si la entrega se hizo en hora punta. En este caso, la entrada será el salario base calculado en el paso anterior. Sin embargo, como desarrollador, no necesito preocuparme de dónde viene el input y cómo se calcula. Mientras sepamos que la tarea de esta función pura es calcular un nuevo valor, es una función muy fácil de escribir.
Mayor capacidad de prueba
Cuando una función es una función pura, la salida de la función es determinista dada la misma entrada. Esto hace que probar la función sea mucho más fácil, porque la prueba se puede estructurar como un conjunto de entradas y su salida esperada, y está garantizado que la ejecución de la función a través de estas entradas siempre producirá la misma salida esperada. Por ejemplo, supongamos que tenemos una función que toma una matriz de enteros y devuelve el segundo mayor número de la matriz. Esta operación es pura porque:
- La función no depende de nada más que de la entrada
- No altera la matriz de entrada o cualquier otra cosa en el sistema
- Dada la misma matriz de números, el segundo número más grande siempre será el mismo.
Por lo tanto, la prueba unitaria para esta función será muy sencilla porque no hay necesidad de simular ninguna variable del sistema o llamadas a funciones, y la salida es determinista por lo que no habrá pruebas defectuosas. Por lo tanto, si todos pudiéramos escribir programas de estilo FP, sería mucho más fácil escribir pruebas, especialmente para aplicaciones de misión crítica.
Posibles inconvenientes de la PF
Sería demasiado bueno para ser cierto si la PF sólo aportara ventajas sin ningún inconveniente potencial. Una desventaja, dependiendo del lenguaje de programación y del compilador, es que cada llamada a una función puede crear una nueva pila de llamadas. Sin optimización, estas creaciones y destrucciones de pilas de llamadas podrían convertirse rápidamente en grandes sobrecargas en tiempo de ejecución para la aplicación, incluso cuando estamos realizando operaciones triviales. Afortunadamente, este inconveniente no es tan grave, ya que Kotlin proporciona la posibilidad de hacer una función inline, lo que resuelve muchos de los problemas si se utiliza correctamente. En pocas palabras, en lugar de crear una nueva pila de llamadas y ejecutar el código dentro de una función, una función inline básicamente reemplaza la llamada a la función con el contenido real y los coloca en el cuerpo de la función de llamada.
Otra desventaja potencial de FP es su velocidad y uso de memoria. Dado que cada función crea esencialmente nuevos datos a partir de los ya existentes, estas creaciones de datos pueden requerir tiempo y espacio adicionales para instanciarse en memoria. En IP, en cambio, tratamos sobre todo con estructuras de datos mutables que pueden actualizarse in situ sin asignar nueva memoria. El problema de la velocidad de ejecución puede mitigarse mediante el paralelismo. Naturalmente, la mayoría de las funciones puras en FP son altamente paralelizables, lo que significa que podemos ejecutar un gran conjunto de funciones sin preocuparnos de cómo interactúan entre sí o cómo afectarán a las variables del sistema. Una estrategia eficaz para ejecutar funciones en paralelo puede aportar potencialmente una mejora neta positiva de la velocidad del programa.
Una de las operaciones más comunes en las aplicaciones modernas es la Entrada/Salida (E/S). Cuando hay E/S, significa que la aplicación está tratando con el mundo exterior. Ejemplos de E/S incluyen solicitar al usuario una entrada, invocar una llamada a procedimiento remoto (RPC) a otro servicio, y leer datos de una base de datos. Debido a la naturaleza impredecible de las tareas de E/S, lo más probable es que no sean puras, lo que significa que tanto la entrada como la salida no son deterministas. Cuando estamos tratando con tareas de E/S, escribir funciones puras forzosamente para manejar E/S no es el enfoque correcto. De hecho, dada la naturaleza multiparadigma de muchos lenguajes de programación modernos como Kotlin, los desarrolladores deberían elegir el paradigma basándose en lo que es mejor para la tarea en cuestión en lugar de seguir estrictamente un paradigma para toda la aplicación. En el mundo de Kotlin, los desarrolladores pueden utilizar la biblioteca de E/S estándar de Kotlin, así como la de Java.
¿Qué ofrece Kotlin a los desarrolladores para aprovechar FP?
Antes de entrar en las acciones reales de cómo escribir código FP en Kotlin, es natural preguntarse, ¿es Kotlin siquiera el lenguaje adecuado para FP? La respuesta corta es, ¡definitivamente sí! De hecho, una de las principales preguntas frecuentes del sitio web oficial del lenguaje Kotlin afirma que "Kotlin tiene tanto construcciones orientadas a objetos como funcionales. Kotlin puede utilizar tanto estilos OO como FP, o mezclar elementos de ambos". Entonces, ¿qué características y herramientas tiene Kotlin para que los desarrolladores puedan escribir código de estilo FP?
Funciones de orden superior y lambdas
Hay una sección dedicada en la documentación de Kotlin que habla sobre este tema, así que no vamos a repasar todos los detalles. En resumen, dado que las funciones Kotlin son ciudadanos de primera clase, pueden ser almacenadas en variables, pueden ser pasadas en argumentos de función y valores de retorno, y pueden definir tipos alrededor de las funciones. Con esta capacidad, las funciones comunes de FP como la operación de plegado se pueden escribir fácilmente en Kotlin porque podemos pasar cualquier función acumulativa a la función de plegado para combinar los datos.
Además de ser compatibles con funciones de orden superior, las expresiones lambda son una buena forma de simplificar el código sin tener que escribir todas las declaraciones de funciones que suelen causar mucho desorden en el código. En pocas palabras, las expresiones lambda son funciones que no se declaran, sino que se pasan inmediatamente como una expresión. Esto hace que razonar y entender el código sea mucho más fácil, ya que no tenemos que pasar por el aro para averiguar qué hace realmente la función.
Como ejemplo rápido, considere el siguiente fragmento de código:
deliveries.sumOf { delivery -> delivery.customerTip }
En este fragmento, sumOf
es una función de orden superior porque toma otra función como argumento, y { delivery -> delivery.customerTip }
es una expresión lambda, que toma un objeto de entrega y devuelve el importe de la propina del cliente de la entrega. Mostraremos más ejemplos reales de escritura de código de estilo FP en Kotlin en secciones posteriores.
Operaciones de recogida
Kotlin proporciona un potente conjunto de operaciones basadas en colecciones que pueden utilizarse para facilitar la computación al estilo FP. Según la documentación de Kotlin, dada una lista de elementos, las operaciones comunes se dividen en estos grupos:
- Transformaciones: Transformar todos los elementos de la recopilación de datos
- Filtrado: Devolver un subconjunto de los elementos en función de determinados criterios.
- Agrupación: Agruparlos en grupos de artículos más pequeños en función de determinados criterios.
- Recuperar partes de la colección: Devolver un subconjunto de elementos de alguna manera
- Recuperar elementos individuales: Devolver un elemento en función de determinados criterios
- Ordenar: Ordenar la recopilación de datos en función de determinados criterios de cada elemento.
- Agregado: Devuelve un único valor tras aplicar algunas operaciones sobre todos los elementos
Todas las funciones para colecciones de la librería estándar están en la documentación de la API de Colecciones de Kotlin. En secciones posteriores, veremos cómo los desarrolladores de DoorDash suelen utilizar estas operaciones comunes de forma habitual.
Comparación de Kotlin con lenguajes como Python, JavaScript y C++
Aunque Kotlin proporciona un potente conjunto de herramientas para que los desarrolladores escriban código FP, estas herramientas y funciones no son exclusivas de Kotlin. De hecho, muchos lenguajes modernos soportan el desarrollo al estilo FP y proporcionan conjuntos similares de operaciones basadas en colecciones, especialmente en las versiones más recientes de estos lenguajes. La siguiente tabla resume cómo Kotlin se compara con estos lenguajes de programación populares en términos de la disponibilidad de algunas características que hemos discutido hasta ahora.
Kotlin | Python | Javascript/Tiposcript | C++ | |
Funciones de orden superior | Sí | Sí | Sí | Sí (introducido en C++11) |
Expresiones lambda | Sí | Sí | Sí | Sí (introducido en C++11) |
Tipo de función | Sí | Parcialmente (tipificación dinámica) | No en JS, sí en TS | Sí |
Transformaciones | Sí | Sí | Sí | Sí (no tiene función de mapa, pero sí de transformación ) |
Agrupación | Sí | No (no incorporado, necesita importar otros paquetes) | Sí | No |
Agregado | Sí | sí | Sí | No |
Mientras que Kotlin soporta todas las características de forma nativa, otros lenguajes modernos, como TypeScript (que es el lenguaje principal para los clientes web en DoorDash), también tienen soporte de biblioteca incorporado. Así, el conocimiento de FP y las operaciones comunes en Kotlin pueden transferirse fácilmente a otros lenguajes modernos en la codificación del día a día.
Ejemplos de cómo en DoorDash escribimos código de estilo FP en Kotlin
Ahora que entendemos qué es FP, cuáles son sus pros y sus contras, y qué nos proporciona Kotlin para escribir código estilo FP, es hora de ver FP en acción. En todos los ejemplos a continuación, utilizaremos las siguientes clases de datos como contexto. Ten en cuenta que todos los ejemplos son hipotéticos y sólo con fines ilustrativos.
data class Delivery(
val id: UUID,
val dasherId: UUID,
val basePay: Double,
val customerTip: Double,
val dropOffTime: Calendar
)
data class Dasher(
val id: UUID,
val name: String
)
Empecemos con un ejemplo fácil pero muy común: Dada una lista de entregas, devuelva una lista de importes de pago totales cuando sean superiores a 10 $.
Veamos primero cómo podemos hacerlo al estilo IP.
val totalPayAmounts = mutableListOf<Double>()
for (delivery: Delivery in deliveries) {
val totalPay = delivery.basePay + delivery.customerTip
if (totalPay > 10) {
totalPayAmounts.add(totalPay)
}
}
return totalPayAmounts
A modo de comparación, he aquí el proceso de pensamiento que hay detrás de este fragmento de código:
- Crear un contenedor vacío que contendrá la salida deseada
- Recorrer en bucle cada entrega de la entrada
- Calcular el salario total
- Si el pago total es superior a 10 $, añádelo al contenedor de salida
- Devuelve el contenedor de salida
Ahora veamos cómo podemos escribir la misma lógica en estilo FP.
return deliveries
.map { delivery -> delivery.basePay + delivery.customerTip }
.filter { totalPay -> totalPay > 10 }
Y el proceso de pensamiento detrás de este fragmento de código:
- Transformar todas las entregas en el pago total de cada entrega
- Filtrar y mantener sólo los que tienen un pago total superior a 10 $.
A partir de este ejemplo, no es difícil imaginar lo diferentes que son las mentalidades entre FP e IP. En el estilo iterativo, la lógica fluye de arriba a abajo, y utiliza un estado mutable (totalPayAmounts)
y un bucle for para calcular el resultado final. En cambio, FP se centra en cómo tratamos los datos de entrada transformándolos y filtrándolos. En el fragmento de código de estilo FP, no se introducen estados adicionales ni se utilizan bucles. En su lugar, utiliza funciones basadas en colecciones incorporadas en Kotlin mapa y filtrojunto con dos expresiones lambda para calcular la lista de resultados final. En general, facilita la lectura de la lógica y reduce la creación de estados adicionales en el programa.
Veamos otro ejemplo más elaborado. Supongamos que tenemos una lista de entregas, y queremos mantener sólo las entregas que tienen una propina del cliente mayor a $5, encontrar las últimas 10 entregas por la hora de entrega, y obtener el Dasher ID de estas entregas. Como antes, empezaremos con cómo podemos escribir esto en estilo IP.
val filteredDeliveries = mutableListOf<Delivery>()
for (delivery: Delivery in deliveries) {
if (delivery.customerTip > 5) {
filteredDeliveries.add(delivery)
}
}
// Sort by delivery.dropOffTime descending
val sortedDeliveries = Collections.sort(
filteredDeliveries,
dropOffTimeComparator
)
val result = mutableListOf<UUID>()
for (i in sortedDeliveries.indices) {
result.add(sortedDeliveries[i].dasherId)
if (i == 9) break
}
Para la misma lógica, aquí está el código de estilo FP:
val result = deliveries
.filter { it.customerTip > 5 }
.sortedByDescending { it.dropOffTime }
.map { it.dasherId }
.take(10)
Aquí utilizamos un Identificador especial de Kotlin it
que se utiliza dentro de una expresión lambda para referirse implícitamente a su parámetro. En las lambdas anteriores, todos los parámetros it
s representan el delivery
de la lista.
No hay duda de lo limpio y elegante que parece el código de estilo FP en comparación con el código IP. Leer el fragmento es básicamente leer inglés sencillo:
- Filtra las entregas para quedarte sólo con aquellas cuya propina del cliente sea superior a 5 $.
- Ordenar la lista en orden descendente según la hora de entrega
- Transformar los elementos en el Dasher ID de la entrega
- Toma los 10 primeros elementos de la lista
Aunque este ejemplo parece bastante sencillo a efectos ilustrativos, no es difícil ver lo flexible que es si queremos aplicar una lógica más compleja a la colección. Supongamos que varios equipos quieren filtrar la lista de entregas basándose en su propia lógica, llámelos complexFilterFunc1, complexFilterFunc2
etc. Pueden simplemente aplicar la lógica de filtrado directamente a las entregas llamando a las funciones en una serie. Dado que filter
es una función de orden superior, puede tomar otras funciones como argumento.
val result = deliveries
.filter { complexFilterFunc1(it) }
.filter { complexFilterFunc2(it) }
.filter { ... }
...
Mejor aún, como estas funciones de filtrado son puras, pueden reordenarse e invocarse en cualquier orden sin cambiar la lógica subyacente.
val result = deliveries
.filter { complexFilterFunc3(it) }
.filter { complexFilterFunc1(it) }
.filter { ... }
...
Si pasa it
a todas las funciones de filtrado parece redundante, Kotlin tiene una forma de pasar la función función de referencia a una función de orden superior utilizando dos puntos dobles ::
val result = deliveries
.filter(::complexFilterFunc1)
.filter(::complexFilterFunc2)
.filter(...)
...
A estas alturas ya deberíamos estar familiarizados con cómo escribir código de estilo FP sobre una lista de elementos y transformarla en otra lista. ¿Y si queremos transformar la lista en otras estructuras de datos como map? Esto no sólo es posible, sino también muy común en nuestra codificación diaria. Veamos un ejemplo.
Supongamos que tenemos una lista de entregas. Ahora queremos ver para cada Dasher, cuánto ganó en propinas por cada hora del día. El resultado final se estructurará como un mapa de Dasher ID a otro mapa, donde la clave es la hora del día, y el valor es la propina total del cliente que ganó. Comenzaremos viendo cómo haremos esto en estilo IP.
val dasherIdToDeliveries = mutableMapOf<UUID, MutableList<Delivery>>()
for (delivery: Delivery in deliveries) {
if (dasherIdToDeliveries.containsKey(delivery.dasherId)) {
dasherIdToDeliveries[delivery.dasherId]!!.add(delivery)
} else {
dasherIdToDeliveries[delivery.dasherId] = mutableListOf(delivery)
}
}
val resultMap = mutableMapOf<UUID, MutableMap<Int, Double>>()
for ((dasherId, deliveriesByDasher) in dasherIdToDeliveries) {
val hourToTotalTipMap = mutableMapOf<Int, Double>()
for (delivery in deliveriesByDasher) {
val hour = delivery.dropOffTime.get(Calendar.HOUR_OF_DAY)
if (hourToTotalTipMap.containsKey(hour)) {
hourToTotalTipMap[hour] = hourToTotalTipMap[hour]!! + delivery.customerTip
} else {
hourToTotalTipMap[hour] = delivery.customerTip
}
}
resultMap[dasherId] = hourToTotalTipMap
}
return resultMap
Definitivamente, esto no es un trozo de código limpio. Utiliza un doble bucle for, dos mapas mutables, y dos bloques if-else para obtener el resultado final. Ahora veamos cómo podemos escribir esto en estilo FP.
val result = deliveries
.groupBy { it.dasherId }
.mapValues { it.value
.groupBy { delivery ->
delivery.dropOffTime.get(Calendar.HOUR_OF_DAY)
}
.mapValues { hourToDeliveries ->
hourToDeliveries.value.sumOf { delivery ->
delivery.customerTip
}
}
}
Hay algunas funciones nuevas que se utilizan, por lo que vamos a explicar lo que hacen en primer lugar antes de ir a través del código:
- groupBy: Dada una lista de elementos, devuelve un mapa desde la clave devuelta por el selector de clave (en este caso, la expresión lambda) a la lista de elementos que tiene la clave correspondiente
- mapValues: Dado un mapa, devuelve un nuevo mapa con entradas que tienen las claves del mapa original, y los valores obtenidos por la función de transformación.
- sumOf: Dada una lista de elementos, suma la lista por el selector de clave
Con estas definiciones en mente, el código de estilo FP se lee así:
- Agrupar la lista de entregas por Dasher ID
- Para cada grupo de entregas, agrupar por hora de entrega
- Para cada subgrupo de entregas (a partir de la agrupación por hora de entrega), suma por cliente propina
Este ejemplo demuestra la capacidad de agrupar y agregar una colección de datos con FP en Kotlin. Es muy común poner funciones anidadas basadas en colecciones en las colecciones intermedias creadas en el paso anterior y transformarlas en cualquier nuevo tipo de datos que se necesite. Esta es una capacidad muy poderosa ya que los desarrolladores no están restringidos a transformar los datos al mismo tipo que la entrada.
Conclusión
La programación funcional es un potente paradigma de programación que puede ayudar a los desarrolladores a escribir fácilmente código más limpio y mejor para las necesidades de programación cotidianas. Esto es especialmente cierto cuando los desarrolladores trabajan en operaciones de misión crítica, grandes sistemas distribuidos y transformación intensiva de datos. Aprovecharlo junto con otros paradigmas comunes como la programación orientada a objetos puede ayudar a conseguir lo mejor de ambos mundos, especialmente con el rico ecosistema que proporciona el lenguaje Kotlin. Aunque FP tiene sus desventajas potenciales, con técnicas modernas y diseños bien pensados, podemos aspirar a una mayor simplicidad, comprobabilidad y legibilidad sin sacrificar la eficiencia y la velocidad.