Objetivo de aprendizaje
Al concluir este tema, el estudiante será capaz de:
- Distinguir una función pura de una impura, e identificar el efecto lateral o el estado externo que la hace impura.
- Explicar la inmutabilidad y cómo se «modifica» una estructura construyendo una versión nueva que comparte lo que no cambia.
- Aplicar la transparencia referencial para razonar sobre un programa sustituyendo expresiones por sus valores.
- Usar funciones de orden superior (
map,filter,fold) y predecir cómo plegar hacia la derecha o hacia la izquierda cambia el resultado. - Componer funciones y predecir el efecto del orden de la composición.
Ruta de trabajo del tema
| Etapa | Contenido | Producto del estudiante |
|---|---|---|
| 1 | Planteamiento inicial | Hipótesis sobre una llamada que cambia de valor |
| 2 | Funciones puras y efectos laterales | Clasificación de funciones |
| 3 | Inmutabilidad y estructuras persistentes | Explicación de la compartición |
| 4 | Transparencia referencial y razonamiento por sustitución | Derivación paso a paso |
| 5 | Funciones de orden superior | Predicción de plegados |
| 6 | Composición de funciones | Predicción del efecto del orden |
| 7 | Python y Haskell frente a frente | Comparación de las garantías de cada lenguaje |
| 8 | Verificación y reformulación | Cuestionario y respuesta revisada |
1. Planteamiento inicial
Un programa en Python lleva la cuenta de lo que se ha pedido en una cafetería:
contador = 0
def siguiente(n):
global contador
contador = contador + n
return contador
print(siguiente(3))
print(siguiente(3))
print(siguiente(3))
Las tres líneas finales son idénticas. Sin ejecutar nada, considere la pregunta.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Esta hipótesis se retomará en la sección 4.
2. Funciones puras y efectos laterales
Definición — Función pura: una función es pura si cumple dos condiciones: (1) mismo argumento, mismo resultado: su resultado depende únicamente de sus argumentos; y (2) sin efectos laterales: no modifica nada fuera de ella ni produce ningún efecto observable (no modifica un argumento, no cambia una variable global, no muestra algo en pantalla, no lee el reloj ni genera números al azar).
Una función que viola la primera condición depende de un estado externo (una variable global, la hora, un número aleatorio). Una que viola la segunda produce un efecto lateral (modifica un argumento, escribe en pantalla, cambia un archivo). Ambas son impuras.
Dos máquinas
Dos máquinas reciben un número y devuelven otro. Llame varias veces a las dos con el mismo argumento. Una de ellas esconde un estado.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Ahora observe la diferencia en el código real, con la corrida de escritorio. En la función impura, el marco de la función cambia una variable que vive en el marco global.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
¿Pura o impura?
Clasifique las funciones siguientes. Si son impuras, indique por qué.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Nota — ¿por qué importa? Una función pura es fácil de probar (basta un ejemplo de entrada y la salida esperada, sin preparar un estado previo), fácil de reutilizar (no depende del contexto), fácil de ejecutar en paralelo (no hay estado compartido que dos hilos puedan modificar a la vez) y fácil de entender (se lee de manera aislada). Los efectos laterales no desaparecen del software —un programa que no escribe en pantalla ni lee datos no sirve—, pero se concentran en los bordes y se mantienen fuera del núcleo del cálculo.
En Haskell, la pureza la garantiza el lenguaje. El tipo de una función dice si puede tener efectos:
-- Pura: el tipo no menciona ningún efecto. NO puede modificar estado ni imprimir.
doble :: Int -> Int
doble x = x * 2
-- Con efecto: el tipo lo declara con «IO».
saludar :: String -> IO ()
En Python, en cambio, la pureza es una disciplina: nada impide que una función con apariencia inocente modifique una lista global. Es responsabilidad de quien programa.
3. Inmutabilidad y estructuras persistentes
Definición — Inmutabilidad: un valor inmutable no puede modificarse después de crearse. «Cambiarlo» significa construir otro valor a partir de él; el original permanece. En Haskell todos los valores son inmutables. En Python hay tipos inmutables (números, textos, tuplas) y mutables (listas, diccionarios).
Esta idea ya apareció en la Unidad II, al estudiar los alias: cuando dos nombres comparten una lista mutable, una modificación hecha por un nombre la observa el otro. Esa fuente de errores desaparece si las listas no pueden modificarse.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
¿No es carísimo copiar todo cada vez?
Si construir una versión nueva exigiera copiar la estructura completa, programar de este modo sería prohibitivo. La solución es la compartición estructural: la versión nueva reutiliza todo lo que no cambió. Como los valores no se modifican, compartir es seguro. Recorra las cuatro escenas.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Definición — Estructura persistente: estructura de datos inmutable que, al «modificarse», conserva la versión anterior y comparte con ella lo que no cambió. Las listas de Haskell son estructuras persistentes:
0 : xscrea una celda nueva y reutilizaxscompleta.
En Haskell, esto puede observarse en la corrida de reducción: ys se construye a partir de xs y xs sigue valiendo lo mismo.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Error común: creer que, si nada se puede modificar, programar se vuelve imposible. Lo que cambia es la forma de expresar las actualizaciones: una función recibe el valor anterior y devuelve el siguiente. Un contador, por ejemplo, ya no es una variable que se incrementa, sino una función de un valor a otro valor.
4. Transparencia referencial y razonamiento por sustitución
Definición — Transparencia referencial: una expresión es referencialmente transparente si puede reemplazarse por su valor sin cambiar el comportamiento del programa. Si todas las funciones son puras, todas las expresiones son referencialmente transparentes.
Es la propiedad que permite razonar sobre un programa como se razona en álgebra: «si doble (cuadrado 3) vale 18, puedo escribir 18 en su lugar». Retome el planteamiento inicial: ¿qué valor corresponde a siguiente(3)? Ninguno: es 3, luego 6, luego 9. La expresión no es transparente. Compruébelo ejecutando el programa original y el que se obtiene al sustituir las llamadas por su valor.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Razonar por sustitución en Haskell
Cuando todo es transparente, evaluar un programa es reescribirlo. Observe cómo se calcula doble (cuadrado 3) + doble (cuadrado 3). En cada paso se sustituye una expresión por otra igual, exactamente como al simplificar una expresión algebraica. Active «Predecir la regla de cada paso».
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Nota — consecuencias prácticas: como una expresión pura siempre vale lo mismo, un compilador puede calcularla una sola vez y reutilizar el resultado, reordenar cálculos sin cambiar el significado y ejecutar en paralelo expresiones independientes. Y quien programa puede probar una función de manera aislada: si funciona con sus ejemplos, funcionará dentro de cualquier programa.
5. Funciones de orden superior
Definición — Función de orden superior: función que recibe otra función como argumento, o que devuelve una función como resultado. Las funciones son valores como los números: se pueden guardar en una variable, pasar y devolver.
# Una función que RECIBE otra función como argumento.
def aplicar_dos_veces(f, x):
return f(f(x))
# Una función que DEVUELVE otra función. La función devuelta «recuerda» el valor de n
# (se le llama clausura).
def crear_sumador(n):
return lambda x: x + n
suma5 = crear_sumador(5) # suma5 es una función: sumar 5
print(aplicar_dos_veces(suma5, 1)) # suma5(suma5(1)) = 11
print(aplicar_dos_veces(lambda x: x * 3, 2)) # triplicar dos veces: 18
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
En Haskell, la misma idea es más corta: aplicar una función a otra es la operación básica, y las secciones como (+ 3) crean funciones sobre la marcha.
-- «f» es una función de tipo «a -> a»: recibe un valor y devuelve otro del mismo tipo.
aplicarDosVeces :: (a -> a) -> a -> a
aplicarDosVeces f x = f (f x)
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Las tres funciones fundamentales: map, filter y fold
| Función | Recibe | Devuelve | Ejemplo |
|---|---|---|---|
map f xs |
Una función y una lista | La lista de los f x |
map (* 2) [1,2,3] → [2,4,6] |
filter p xs |
Un predicado y una lista | Los elementos que cumplen p |
filter even [1,2,3,4] → [2,4] |
fold f z xs |
Una función de dos argumentos, un valor inicial y una lista | Un solo valor | foldr (+) 0 [1,2,3] → 6 |
map y filter ya aparecieron en el tema 4.0. fold (plegado) es la más general: casi cualquier recorrido de una lista puede escribirse con ella. Existen dos versiones, que se diferencian en hacia qué lado agrupan:
foldr f z [a, b, c]equivale af a (f b (f c z))— los paréntesis se agrupan a la derecha.foldl f z [a, b, c]equivale af (f (f z a) b) c— los paréntesis se agrupan a la izquierda.
Con una operación asociativa como la suma, los dos dan el mismo resultado. Con una que no lo es, como la resta, el resultado cambia. Elija la operación y la lista y observe los dos árboles de evaluación.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Las dos corridas siguientes reproducen, en Haskell, el caso de la resta. Antes de ejecutarlas, anticipe los dos resultados.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
En Python, functools.reduce es un plegado a la izquierda. Para obtener un plegado a la derecha se pliega la lista invertida intercambiando los argumentos de la función.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Error común: pensar que
foldsolo sirve para sumar. Con la función adecuada se obtiene el máximo, la longitud, la inversión de una lista, la concatenación… Casi todo recorrido que combina elementos es un plegado.
6. Composición de funciones
Definición — Composición: dadas dos funciones
fyg, su composición es la función que aplica primerogy despuésf:(f . g) x = f (g x). Es la manera de construir funciones nuevas conectando la salida de una con la entrada de otra, como una tubería.
La composición se lee de derecha a izquierda: en (f . g), la primera función aplicada es g. Construya una cadena de funciones y vea cómo el orden cambia el resultado.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
La misma idea, en Python (con una función que compone) y en Haskell (con el operador .):
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
7. Python y Haskell frente a frente
| Idea | Python | Haskell |
|---|---|---|
| Pureza | Una disciplina: nada impide los efectos | Garantizada: los efectos aparecen en el tipo (IO) |
| Inmutabilidad | Algunos tipos sí (tuplas, textos), otros no (listas, diccionarios) | Todos los valores son inmutables |
| Estructuras persistentes | No por omisión; se copian las listas | Las listas y los árboles comparten estructura |
| Orden superior | Sí: lambda, map, filter, functools.reduce |
Sí, y es la forma natural de programar |
| Composición | Se escribe con funciones auxiliares o llamadas anidadas | Operador . incorporado |
| Fold | reduce (solo a la izquierda) |
foldr y foldl |
| Transparencia referencial | Solo si quien programa mantiene la pureza | En todo el código puro |
Nota: Python permite programar de manera funcional cuando se quiere, pero no lo exige ni lo garantiza. Haskell lo exige y lo garantiza, a cambio de una forma de trabajar distinta para los efectos (entrada y salida).
8. Errores conceptuales frecuentes
Error común: creer que una función es pura porque «no imprime nada». Modificar un argumento o leer una variable global también la hace impura.
Error común: creer que
lista + [x]ylista.append(x)son lo mismo. El primero construye una lista nueva; el segundo modifica la existente.
Error común: creer que
foldlyfoldrsiempre dan lo mismo. Solo coinciden con operaciones asociativas.
Error común: escribir
(f . g)pensando que se aplica primerof. Se aplica primerog.
Error común: pensar que las funciones de orden superior son «avanzadas». Son herramientas cotidianas:
sorted(xs, key=len)ya usa una.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
9. Libreta de trabajo
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
10. Preguntas de reflexión
- Escriba una función impura de su experiencia (por ejemplo, un contador o un generador de identificadores) y reescríbala como una función pura que reciba el estado anterior y devuelva el nuevo. ¿Qué ganó y qué perdió?
- Si todos los valores son inmutables, ¿cómo representaría el estado de un juego que cambia en cada jugada? Describa la idea con funciones de un estado a otro estado.
- Explique, con la transparencia referencial, por qué una función pura puede probarse de manera aislada.
- Dé un ejemplo de operación para la que
foldlyfoldrden el mismo resultado y uno para el que den resultados distintos. - Escriba una composición de tres funciones para un problema que conozca y describa el orden en que se aplican.
11. Lecturas recomendadas
- Hughes, J. (1989). Why functional programming matters. The Computer Journal, 32(2), 98–107.
- Hutton, G. (2016). Programming in Haskell (2nd ed.), capítulos sobre funciones de orden superior y sobre plegados. Cambridge University Press.
- Okasaki, C. (1998). Purely functional data structures. Cambridge University Press. (Estructuras persistentes.)
- Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulo sobre lenguajes de programación funcional. Pearson.
- Python Software Foundation. Functional Programming HOWTO y documentación de
functools. Acceso libre en docs.python.org.