Funciones puras y efectos secundarios
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/
En esta lección, abordaremos el concepto de funciones puras y su relación con los efectos secundarios en la programación. Las funciones puras, al no tener efectos secundarios, ofrecen numerosas ventajas, como una mayor facilidad para probarlas, una mejor previsibilidad de su comportamiento, y su uso en entornos como la programación funcional. Por otro lado, los efectos secundarios, aunque a menudo necesarios en aplicaciones del mundo real, pueden hacer que el código sea más difícil de mantener y depurar.
A lo largo de este tema, aprenderás a identificar funciones puras e impuras, a refactorizar código para eliminar efectos secundarios y a comprender cómo utilizar las herramientas de Kotlin para manejar valores de forma segura, evitando modificaciones indeseadas en el estado del programa.
Funciones Puras
Transparencia Referencial
Una expresión es referencialmente transparente si, en cualquier programa , todas las ocurrencias de en pueden ser reemplazadas por el resultado de evaluar sin cambiar el significado de .
Función Pura
Una función es pura si, para todos los que son referencialmente transparentes, la expresión también lo es.
Corolario
Efectos Secundarios
También conocidos como side-effects, los efectos secundarios son cualquier operación que una función realice además de retornar un valor. Un efecto secundario puede ser modificar una variable global, escribir en un archivo, o interactuar con la interfaz de usuario. Cuando una función tiene efectos secundarios, se la considera impura.
Ejercicio: Identificar Funciones Puras e Impuras
Identifica si las siguientes funciones son puras o impuras:
-
fun add(a: Int, b: Int): Int = a + b
-
fun addCurried(): (Int) -> (Int) -> Int = { a -> { b -> a + b } }
-
fun greet(name: String): Unit = println("Hello, $name!")
-
fun incrementArray(array: Array<Int>): Unit {
for (i in array.indices) {
array[i]++
}
}
Solución
- Pura: No tiene efectos secundarios y solo realiza una operación matemática.
- Pura: Es una función currificada que no tiene efectos secundarios.
- Impura: Tiene un efecto secundario al imprimir en la consola.
- Impura: Modifica el array original, lo que implica un efecto secundario.
Corolario
Mitigando efectos secundarios
Cuando una función es impura debido a que modifica el estado del programa, como en el siguiente ejemplo, se puede refactorizar para evitar esos efectos.
Ejemplo de función impura: Incrementar un arreglo
fun incrementArray(array: Array<Int>): Unit {
for (i in array.indices) {
array[i]++
}
}
El código anterior es impuro porque modifica directamente el arreglo pasado como argumento. Esto genera efectos secundarios, lo que hace que el comportamiento de la función dependa del estado externo.
Para hacer que la función sea pura, debemos evitar modificar el arreglo original. En su lugar, vamos a devolver un nuevo arreglo con los valores incrementados. A continuación, utilizamos recursión para lograr esto:
fun incrementArray(array: Array<Int>, index: Int = 0): Array<Int> =
if (index >= array.size) {
arrayOf()
} else {
arrayOf(array[index] + 1) + incrementArray(array, index + 1)
}
- [2] Caso Base: El caso base ocurre cuando el índice ha recorrido todos los elementos del arreglo (
index >= array.size
). En este caso, la función retorna un arreglo vacío, lo cual sirve como punto de finalización de la recursión. - [5] Caso Recursivo:
- En cada paso de la recursión, se crea un nuevo arreglo que contiene el valor incrementado del elemento actual, y luego se concatena con el resultado de la llamada recursiva para los elementos restantes.
- Esto genera un nuevo arreglo sin modificar el original.
Ejemplo de Función Impura: Cálculo de Intereses Compuestos
En el código original, el uso de println
para mostrar los balances en cada iteración es un efecto secundario que lo hace impuro:
fun calculateCompoundInterest(
principal: Double,
rate: Double,
years: Int
): Double {
var balance = principal
for (year in 1..years) {
balance += balance * rate
println("Año $year: Balance = $balance")
}
return balance
}
Para hacer la función pura, eliminamos los efectos secundarios (la impresión) y almacenamos el historial de balances en una lista que será parte del resultado. Utilizaremos recursión con una función auxiliar para no tener una 💣explosión💥 en el número de parámetros:
fun calculateCompoundInterest(
principal: Double,
rate: Double,
years: Int
): Pair<Double, List<String>> {
fun calculateRecursive(
currentYear: Int,
currentBalance: Double,
history: List<String>
): Pair<Double, List<String>> = if (currentYear > years) {
currentBalance to history
} else {
val newBalance = currentBalance + currentBalance * rate
val newHistory = history + "Año $currentYear: Balance = $newBalance"
calculateRecursive(currentYear + 1, newBalance, newHistory)
}
return calculateRecursive(1, principal, emptyList())
}
Ejercicio: Eliminar Efectos Secundarios
En el siguiente ejemplo, la función transferirFondos
tiene efectos secundarios, ya que modifica el estado de las cuentas y la base de datos:
object Database {
fun update(cuenta: Cuenta): Unit = TODO()
}
fun transferirFondos(cuentaOrigen: Cuenta, cuentaDestino: Cuenta, monto: Int) =
if (cuentaOrigen.saldo >= monto) {
cuentaOrigen.saldo -= monto
cuentaDestino.saldo += monto
database.update(cuentaOrigen)
database.update(cuentaDestino)
true
} else {
false
}
Modifica la función transferirFondos
para eliminar los efectos secundarios. En lugar de modificar directamente las cuentas, devuelve un objeto que describa el resultado de la operación.
Ver hint
- Utiliza un registro para representar el resultado de la transferencia.
Solución
data class TransferenciaResultado(
val cuentaOrigenActualizada: Cuenta,
val cuentaDestinoActualizada: Cuenta,
val exito: Boolean
)
fun transferirFondos(cuentaOrigen: Cuenta, cuentaDestino: Cuenta, monto: Int) =
if (cuentaOrigen.saldo >= monto) {
val nuevaCuentaOrigen = cuentaOrigen.copy(saldo = cuentaOrigen.saldo - monto)
val nuevaCuentaDestino = cuentaDestino.copy(saldo = cuentaDestino.saldo + monto)
TransferenciaResultado(nuevaCuentaOrigen, nuevaCuentaDestino, true)
} else {
TransferenciaResultado(cuentaOrigen, cuentaDestino, false)
}
Beneficios y limitaciones de las funciones puras
Beneficios
- Previsibilidad y simplicidad: Las funciones puras son más fáciles de razonar porque siempre producen los mismos resultados dados los mismos parámetros de entrada, sin depender de factores externos o del estado global.
- Facilidad para probar: Debido a que no tienen efectos secundarios, las funciones puras son mucho más sencillas de probar de forma unitaria. No requieren mocks ni configuración especial, ya que no interactúan con el estado externo.
- Reutilización y composición: Las funciones puras se pueden combinar fácilmente para construir soluciones más complejas. Son modulares y reutilizables sin preocuparse por su impacto en el estado externo.
- Facilidad de paralelización: Al no depender de variables globales o del estado externo, las funciones puras pueden ejecutarse en paralelo sin riesgo de condiciones de carrera, mejorando el rendimiento en sistemas concurrentes o distribuidos.
- Transparencia referencial: Dado que el resultado de una función pura es predecible y no depende del contexto, se puede optimizar el código mediante técnicas como la memoización o el cacheado.
Limitaciones
- Dificultad para interactuar con el mundo real: Muchos programas del mundo real requieren efectos secundarios, como leer archivos, hacer consultas a bases de datos o interactuar con una API. En estos casos, las funciones puras por sí solas no son suficientes.
- Overhead por evitar mutaciones: En algunos casos, evitar efectos secundarios y crear nuevas estructuras de datos en lugar de modificar las existentes puede resultar en un sobrecosto de rendimiento o memoria, especialmente en algoritmos que requieren operaciones intensivas.
- Mayor complejidad para ciertas tareas: Algunas operaciones que dependen de efectos secundarios, como la escritura de logs o la gestión de estados en aplicaciones interactivas, pueden volverse más complicadas al tratar de forzarlas a ser puras.
- Curva de aprendizaje: Para aquellos acostumbrados a programar de manera imperativa, trabajar con funciones puras y evitar efectos secundarios puede ser un desafío conceptual, requiriendo un cambio de mentalidad.
¿Qué aprendimos?
En esta lección, hemos explorado el concepto de funciones puras y efectos secundarios. Aprendimos que las funciones puras son aquellas que no tienen efectos secundarios y siempre devuelven el mismo resultado con las mismas entradas. Además, comprendimos que las funciones con efectos secundarios, si bien son necesarias en muchos casos, pueden dificultar el mantenimiento y la depuración del código.
Puntos clave
- Funciones puras: Son previsibles, fáciles de probar, reutilizables y adecuadas para la paralelización. No modifican el estado del programa ni interactúan con el mundo externo.
- Efectos secundarios: Involucran operaciones como modificar variables globales o escribir en un archivo, lo que hace que las funciones que los contienen sean impuras y dependan del estado externo.
- Refactorización para evitar efectos secundarios: Es posible transformar funciones impuras en funciones puras devolviendo nuevas estructuras de datos en lugar de modificar las existentes, o almacenando resultados en lugar de imprimirlos directamente.
- Balance en la programación: Aunque las funciones puras tienen muchos beneficios, en la programación real es necesario manejar efectos secundarios para interactuar con el mundo externo.
El uso de funciones puras y la minimización de los efectos secundarios conducen a un código más limpio, predecible y fácil de mantener. Sin embargo, es esencial encontrar un equilibrio entre pureza y funcionalidad práctica para lograr aplicaciones eficientes y robustas.
Bibliografías Recomendadas
- 📚 "What is functional programming?". (2021). M. Vermeulen, R. Bjarnason, & P. Chiusano, en Functional Programming in Kotlin, (1st, pp. 3–16.) Shelter Island, NY.