Composición de funciones

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.
En una panadería, la masa que sale de la amasadora no se guarda en ningún recipiente aparte antes de hornearse: pasa directo de una máquina a la otra, y el horno recibe exactamente lo que la amasadora entregó. El pan terminado es en realidad la combinación de dos procesos más chicos, cada uno resuelto por su cuenta, encadenados uno después del otro.
Varias de las funciones ya definidas en la serie devuelven un valor pensado para poder usarse en cualquier lugar donde haga falta un valor de ese tipo: sumarlo, guardarlo en una variable, o pasarlo como parámetro de otra función. Componer funciones es justamente eso último llevado a la práctica: usar el resultado de una función ya definida como parámetro directo de otra, encadenando dos algoritmos que cumplen su contrato por separado sin escribir de nuevo ningún paso de ninguno de los dos.
Esto no es una operación nueva de la notación. Desde promedio, en el post anterior, ya se venía escribiendo devolver dividir suma por 2, sin necesidad de guardar esa división en un nombre aparte antes de devolverla: el resultado de una operación puede usarse directo donde haga falta un valor. Este post extiende esa misma idea a las llamadas a función: el resultado de llamar a un algoritmo puede usarse directo como parámetro de otro, sin necesidad de asignarlo primero a un nombre propio, siempre que ese nombre no se necesite para nada más.
Esa forma anónima es una comodidad, no una obligación: nombrar el resultado intermedio -como ya hacía costo_pintura, en el post de funciones, al guardar el área antes de multiplicarla por el precio del metro- sigue siendo perfectamente válido, y es la opción más clara cuando ese valor se va a usar en más de un lugar, o cuando la función siguiente necesita además otros parámetros que no salen de la primera llamada.
El orden en que se componen dos funciones importa, de la misma forma en que la secuencia (post 4 de la serie) ya mostró que el orden de los pasos cambia el resultado. Encadenar la función A y después la función B no es lo mismo que encadenar B y después A: cada una recibe un valor de entrada distinto según en qué posición de la cadena esté, y el resultado final puede no coincidir.
Un detalle que no cambia por componer: si alguna de las funciones combinadas tiene un efecto colateral (post anterior), ese efecto sigue ocurriendo igual, esté la función sola o encadenada con otra. Componer combina lo que las funciones devuelven, no oculta ni cancela lo que además modifican por fuera de sí mismas.
Dos funciones simples, cada una resolviendo una única operación:
algoritmo al_cuadrado(numero):
multiplicar numero por numero
devolver el resultadoalgoritmo mas_uno(numero):
sumar 1 a numero
devolver el resultadoEncadenarlas nombrando el resultado intermedio, igual que ya hacía costo_pintura:
algoritmo cuadrado_mas_uno_v1(numero):
llamar a al_cuadrado con numero para obtener cuadrado
llamar a mas_uno con cuadrado para obtener resultado
devolver resultadoLa misma cadena, sin nombrar cuadrado porque no hace falta en ningún otro lado:
algoritmo cuadrado_mas_uno_v2(numero):
devolver llamar a mas_uno con el resultado de llamar a al_cuadrado con numerocuadrado_mas_uno_v2(3) da 10: al_cuadrado convierte el 3 en 9, y ese 9 -sin haber quedado guardado en ningún nombre- entra directo como parámetro de mas_uno, que lo convierte en 10. Las dos versiones, con nombre y sin nombre, hacen exactamente lo mismo y devuelven el mismo valor; la diferencia es solo de escritura.
El orden importa: encadenar mas_uno antes que al_cuadrado, sobre el mismo 3, da un resultado distinto. mas_uno(3) da 4, y al_cuadrado(4) da 16, no 10. Sumar uno y después elevar al cuadrado no es la misma operación que elevar al cuadrado y después sumar uno, aunque las dos combinen exactamente las mismas dos funciones.
Definir un algoritmo precio_con_envio que reciba el precio de un producto, un porcentaje de descuento y un costo de envío fijo, y devuelva el precio final: primero se le aplica el descuento, y sobre ese resultado -sin volver a tocar el precio original- se suma el envío.
Pensar qué función resuelve cada una de las dos partes del problema por separado, y en qué orden tiene que pasar el resultado de una a la otra.
algoritmo aplicar_descuento(precio, porcentaje):
multiplicar precio por porcentaje
asignar el resultado a descuento
restar descuento a precio
devolver el resultadoalgoritmo agregar_envio(precio, costo_envio):
sumar costo_envio a precio
devolver el resultadoalgoritmo precio_con_envio(precio, porcentaje, costo_envio):
llamar a aplicar_descuento con precio y porcentaje para obtener precio_con_descuento
llamar a agregar_envio con precio_con_descuento y costo_envio para obtener resultado
devolver resultadollamar a precio_con_envio con 2000, 0.1 y 350 para obtener precio_final da 2150: aplicar_descuento deja el precio en 1800 (2000 menos el 10%), y agregar_envio le suma los 350 del envío. precio_con_envio nombra precio_con_descuento en vez de anidar la llamada porque acá sí conviene poder leerlo de un vistazo: es el valor que conecta las dos funciones, y ponerle nombre deja más clara la cadena que resolverla en una sola línea anidada.
Una fábrica de repuestos calcula el importe final que le cobra a un cliente frecuente: al importe base se le aplica un descuento fijo por ser cliente frecuente, y sobre lo que queda se suma el costo de flete, que depende de la distancia en kilómetros hasta el domicilio de entrega.
Pensar si estas dos cuentas ya se resolvieron antes en algún otro algoritmo de este mismo post, o si hace falta escribir la lógica de nuevo.
algoritmo descuento_frecuente(importe, porcentaje):
multiplicar importe por porcentaje
asignar el resultado a descuento
restar descuento a importe
devolver el resultadoalgoritmo costo_flete(importe, distancia_km):
multiplicar distancia_km por 100
asignar el resultado a flete
sumar flete a importe
devolver el resultadoalgoritmo importe_final_pedido(importe, porcentaje, distancia_km):
llamar a descuento_frecuente con importe y porcentaje para obtener importe_con_descuento
llamar a costo_flete con importe_con_descuento y distancia_km para obtener resultado
devolver resultadodescuento_frecuente y costo_flete son estructuralmente la misma idea que aplicar_descuento y agregar_envio del ejercicio anterior -una resta proporcional, seguida de una suma que depende de otro dato-, así que importe_final_pedido no inventa ninguna lógica nueva: encadena las dos funciones ya definidas en este post, en el orden que el problema pide. llamar a importe_final_pedido con 5000, 0.05 y 12 para obtener total da 5950: descuento_frecuente deja el importe en 4750 (5000 menos el 5%), y costo_flete le suma 1200 (12 kilómetros por 100 cada uno).
Componer funciones es usar el resultado de una llamada directo como entrada de otra, con o sin nombrarlo en el medio. No es una herramienta nueva: es una forma de aprovechar el contrato de cada función -qué entra, qué sale- para construir un algoritmo más grande sin escribir de nuevo ningún paso ya resuelto.
El próximo post de la serie retoma la idea de que una función puede llamar a otra función, con un caso particular: qué pasa cuando esa otra función es ella misma.