Efectos colaterales y funciones puras

Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Una balanza de baño responde una sola pregunta: cuánto pesa quien se sube a ella. Devuelve un número y nada más cambia: ni la balanza queda distinta, ni la persona pesada queda distinta por haber sido pesada. Una máquina expendedora funciona distinto: al elegir un producto y pagarlo, entrega ese producto, pero además descuenta una unidad de su stock y suma el monto pagado a la recaudación del día. Después de esa operación, la máquina quedó en un estado distinto al que tenía antes de la operación.
Esa misma distinción existe entre algoritmos. Una función pura, dado un mismo conjunto de parámetros, siempre devuelve el mismo resultado, y ese resultado es su único efecto: no cambia nada que exista fuera de ella. Un efecto colateral es cualquier cambio que una función produce y que sigue existiendo después de que la llamada termina, más allá del valor que devuelve: modificar una variable que no recibió como parámetro y que tampoco declaró en su interior, o dejar algo escrito en el mundo.
El post anterior fijó que los parámetros y las variables locales de una función existen solo mientras dura esa llamada puntual, sin memoria entre una y otra. Un efecto colateral aparece exactamente cuando una función rompe ese límite: toca un nombre que ya existía antes de que la llamada empezara y que va a seguir existiendo después de que termine.
Tener un efecto colateral no es un error. Un programa real necesita en algún momento guardar un dato, mostrar un resultado o actualizar un total compartido entre varias partes del sistema. El problema aparece cuando no queda claro cuáles funciones lo tienen y cuáles no: para saber qué hace una función pura alcanza con mirar sus parámetros; para saber qué hace una función con efectos colaterales hace falta conocer, además, en qué estado está el resto del programa en el momento exacto de la llamada.
El primer ejemplo calcula el promedio de dos números. No necesita nada más que esos dos números para funcionar, y no deja ningún rastro más allá del valor que devuelve:
algoritmo promedio(a, b):
asignar el resultado de sumar a y b a suma
devolver dividir suma por 2Llamado dos veces con los mismos valores, promedio(8, 14) devuelve 11 las dos veces. Llamado con otros valores, promedio(20, 30) devuelve 25. El resultado depende únicamente de a y b: nada de lo que haya pasado antes en el programa cambia lo que esta función devuelve.
El segundo ejemplo registra una venta en la caja de un comercio. La caja es una variable que ya existe antes de que este algoritmo se escriba, creada una sola vez al arrancar el día (asignar 0 a caja):
algoritmo sumar_a_caja(monto):
sumar monto a cajaEste algoritmo no tiene ningún devolver: igual que los algoritmos de la vida diaria del primer post de la serie, deja su resultado en el mundo en vez de en un valor. Llamado como sumar_a_caja(100) con caja en 0, la caja pasa a valer 100. Llamado una segunda vez con el mismo monto, la caja no vuelve a valer 100: pasa a valer 200, porque parte del valor que ya tenía. La misma llamada, con el mismo parámetro, deja resultados distintos según cuántas veces se haya llamado antes, y eso es exactamente lo que distingue a una función con efectos colaterales de una pura. sumar_a_caja toca caja, que no es un parámetro ni una variable local suya: existe afuera de la función, así que modificarla es un efecto colateral por la definición de arriba.
Escribir el algoritmo descuento_aplicado, que reciba el precio original de un producto y su precio ya con descuento, y devuelva cuánto se descontó en total, sin modificar nada que exista fuera de él.
Pista: pensar qué datos hacen falta para calcular esa diferencia, y si alcanza con los dos valores que el algoritmo recibe como parámetros o si además hace falta algo que exista afuera de él.
algoritmo descuento_aplicado(precio_original, precio_final):
devolver restar precio_final a precio_originalCon precio_original en 2500 y precio_final en 2000, el algoritmo devuelve 500. No usa ningún nombre que no haya recibido como parámetro, así que es puro: llamarlo de nuevo con los mismos dos precios, en cualquier momento del programa, siempre va a devolver 500.
Un local de barrio necesita dos cosas al final del día: sumar cada venta a una caja compartida por todo el sistema, y armar un mensaje con la cantidad de ventas y el total recaudado, sin que ese mensaje cambie nada.
Pista: antes de escribir cada algoritmo, preguntarse qué necesita para funcionar: si le alcanza con lo que recibe como parámetro, o si además necesita tocar algo que ya existe afuera de él.
algoritmo registrar_venta(monto):
sumar monto a caja
sumar 1 a cantidad_ventas
algoritmo resumen_dia(cantidad, total):
devolver unir "Ventas: ", cantidad, " - Total recaudado: ", totalCon caja y cantidad_ventas en 0 al empezar el día, tres llamadas a registrar_venta -con 1500, 2300 y 800- dejan caja en 4600 y cantidad_ventas en 3. Después, resumen_dia(3, 4600) devuelve "Ventas: 3 - Total recaudado: 4600".
registrar_venta tiene efecto colateral doble: modifica caja y cantidad_ventas, dos nombres que no son ni parámetros ni variables locales suyas. resumen_dia, en cambio, es puro, aunque los dos valores que recibe salgan de esas mismas variables externas: la pureza es una propiedad de la función misma, de si toca o no algo que no le pertenece, no de dónde venga el dato que alguien le pasa al llamarla.
Una función pura alcanza con mirar sus propios parámetros para saber qué va a hacer. Una función con efectos colaterales depende además del estado del resto del programa en el momento exacto en que se la llama.
El próximo post retoma las funciones ya definidas hasta acá para ver cómo se combinan entre sí, usando la salida de una como entrada de la siguiente.