Unidad IV · Tema 4.0 Disponible

Pensar en funciones

De las instrucciones a las transformaciones: cómo interpretar un problema de manera funcional y programar su solución paso a paso, en Python y en Haskell.

Importante: pasar de la programación imperativa a la funcional no es aprender una sintaxis nueva: es aprender a hacerse otras preguntas. Es normal sentir que «no se entiende» al principio. Este tema avanza despacio, con muchos ejemplos y con una receta de trabajo que se repite en cada uno.

Objetivo de aprendizaje

Al concluir este tema, el estudiante será capaz de:

  1. Distinguir una solución imperativa (instrucciones que modifican variables) de una funcional (descripción del resultado a partir de otros valores) para un mismo problema.
  2. Aplicar un método de seis pasos para interpretar un problema como una cadena de transformaciones de datos.
  3. Reconocer los patrones filtrar, transformar y combinar, y expresarlos como comprensión de lista, map/filter/fold o recursión.
  4. Escribir la misma solución en Python (estilo funcional) y en Haskell, y explicar las diferencias.
  5. Verificar una solución razonando por sustitución y con ejemplos, incluidos los casos límite.

Ruta de trabajo del tema

Etapa Contenido Producto del estudiante
1 Planteamiento inicial Hipótesis escrita
2 Dos maneras de pensar el mismo cálculo Comparación entre receta y descripción
3 El método de seis pasos Receta de trabajo
4 Caso A: la suma de los cuadrados de los pares Solución en dos lenguajes
5 Caso B: calificaciones Solución en dos lenguajes
6 Caso C: palabras de un texto Solución en dos lenguajes
7 Recursión: pensar en la estructura de los datos Plantilla de tres preguntas
8 Errores frecuentes al cambiar de paradigma Clasificación de fragmentos
9 Verificación y reformulación Cuestionario y respuesta revisada

1. Planteamiento inicial

Considere este problema: dada una lista de números, calcular la suma de los cuadrados de los números pares. Con la lista [1, 2, 3, 4, 5, 6] el resultado es 56, porque 2² + 4² + 6² = 4 + 16 + 36.

Una persona con experiencia en programación imperativa escribiría, casi sin pensarlo:

numeros = [1, 2, 3, 4, 5, 6]
total = 0
for n in numeros:
    if n % 2 == 0:
        total = total + n * n
print(total)

Observe que la solución depende de una variable, total, que cambia a lo largo del programa: vale 0, luego 4, luego 20, luego 56.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Esta hipótesis se retomará al final del tema.


2. Dos maneras de pensar el mismo cálculo

Una analogía conocida: la hoja de cálculo

Quien ha usado una hoja de cálculo ya ha programado de manera funcional. En una celda se escribe =SUMA(B1:B10): no se escribe «inicializa un acumulador, recorre las celdas y suma una por una». La fórmula describe qué valor debe tener la celda en términos de otras celdas. Si cambia un dato, la hoja recalcula; nadie modifica una variable a mano.

Receta (imperativa) Descripción (funcional)
Pregunta que se hace quien programa ¿Qué hago primero? ¿Qué variable actualizo? ¿Qué datos tengo? ¿Qué quiero obtener? ¿Qué transformaciones los conectan?
Variables Se crean y se modifican (total = total + …) Se definen una vez y no cambian
Repetición Ciclos con contador (for, while) Funciones que se aplican a colecciones (map, filter, fold) o recursión
El orden Importa: cambiar dos líneas cambia el resultado Importa poco: cada expresión solo depende de sus partes
Un programa es… Una secuencia de instrucciones Una expresión que se evalúa a un valor
Analogía Una receta de cocina Una fórmula de hoja de cálculo

Primero la receta: observar cómo cambia el estado

Ejecute el programa imperativo y observe la variable total en el marco global: su valor se sobrescribe en cada vuelta.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Ahora la descripción: observar cómo fluyen los datos

La misma solución puede leerse como una tubería: la lista entra, se queda con los pares, eleva al cuadrado cada uno y, al final, los suma. En esta animación, la pestaña funcional muestra cómo cada etapa produce una colección nueva, y la imperativa, cómo una sola variable se va sobrescribiendo.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Nota: en la visión funcional nada se modifica: cada fila nace de la anterior y la anterior permanece intacta. En la visión imperativa, el valor anterior de total se pierde en cada actualización.

La misma idea, escrita como una descripción

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Error común: pensar que el estilo funcional es «el mismo programa con otra sintaxis». La diferencia no está en la notación, sino en que no hay un estado que se vaya modificando: el programa es una cadena de valores que se obtienen unos de otros.


3. El método de seis pasos

Pensar de manera funcional se aprende con una receta que se repite. Estos son los seis pasos; en las secciones siguientes se aplican a tres problemas.

Paso Qué se hace Pregunta guía Producto
1. Entrada y salida Escribir ejemplos concretos de lo que entra y de lo que debe salir, incluidos los casos límite ¿Cómo son los datos de entrada y cómo debe ser el resultado? Ejemplos con su resultado
2. La cadena Describir con verbos, en lenguaje natural, cómo se llega de la entrada a la salida ¿Qué transformaciones sucesivas llevan los datos al resultado? Una frase como «de la lista, quedarme con…; elevar…; sumar…»
3. El patrón Reconocer cada etapa como filtrar, transformar o combinar (o como recursión sobre la estructura) ¿Selecciono, cambio cada elemento o reúno todos en uno? Etapas con su patrón
4. Piezas pequeñas Escribir cada etapa como una función pequeña con un nombre claro y comprobar cada una por separado ¿Puedo probar esta pieza sola con un ejemplo? Funciones puras con ejemplos
5. Composición Unir las piezas: la salida de una es la entrada de la siguiente ¿En qué orden se encadenan? La solución completa
6. Verificación Razonar por sustitución y probar los ejemplos del paso 1 ¿La reescritura paso a paso produce el resultado esperado? ¿Y con la lista vacía? Evidencia de que funciona

Los tres patrones del paso 3 son los ladrillos con los que se construye casi todo:

Patrón Qué hace En Python En Haskell Pregunta
Filtrar Conserva los elementos que cumplen una condición [x for x in xs if cond] o filter filter cond xs ¿Cuáles me interesan?
Transformar Aplica una función a cada elemento [f(x) for x in xs] o map map f xs ¿Cómo cambia cada uno?
Combinar Reduce la colección a un solo valor sum, reduce sum, foldr, foldl ¿Cómo los reúno?

El problema de la suma de cuadrados, como cadena de transformaciones con los tipos de los datos intermedios:

flowchart LR
  A["[1,2,3,4,5,6]<br/>lista de enteros"] -->|"filtrar: los pares"| B["[2,4,6]<br/>lista de enteros"]
  B -->|"transformar: cuadrado"| C["[4,16,36]<br/>lista de enteros"]
  C -->|"combinar: sumar"| D["56<br/>un entero"]

4. Caso A: la suma de los cuadrados de los pares

Se aplican los seis pasos.

Paso 1. Entrada y salida. [1,2,3,4,5,6] → 56. Casos límite: [] → 0 (la suma de nada es cero); [1,3,5] → 0 (no hay pares).

Paso 2. La cadena. «De la lista, quedarme con los pares; elevar al cuadrado cada uno; sumarlos.»

Paso 3. El patrón. Filtrar (los pares) → transformar (cuadrado) → combinar (suma).

Paso 4. Piezas pequeñas. Tres funciones, cada una con una sola responsabilidad y comprobable sola:

# Pieza 1: una condición. Devuelve True o False; no modifica nada.
def es_par(n):
    return n % 2 == 0


# Pieza 2: una transformación de un solo elemento.
def cuadrado(n):
    return n * n


# Composición (pasos 5): la salida de cada etapa es la entrada de la siguiente.
def suma_cuadrados_pares(xs):
    return sum([cuadrado(x) for x in xs if es_par(x)])


print(suma_cuadrados_pares([1, 2, 3, 4, 5, 6]))   # 56
print(suma_cuadrados_pares([]))                   # 0: la suma de nada es cero

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

La misma solución en Haskell. La estructura es idéntica; cambia la notación.

-- Pieza 1: una transformación. La firma dice: recibe un Int y devuelve un Int.
cuadrado :: Int -> Int
cuadrado x = x * x

-- La solución: suma de los cuadrados de los elementos que son pares.
sumaCuadradosPares :: [Int] -> Int
sumaCuadradosPares xs = sum (map cuadrado (filter even xs))

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Ahora se verá cómo Haskell evalúa esa definición: cada paso reescribe la expresión en otra igual. Active «Predecir la regla de cada paso» para anticipar qué se hará con la parte marcada.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Las dos soluciones, una junto a la otra. Ejecútelas y analice las diferencias.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


5. Caso B: calificaciones

Problema. Se tiene una lista de pares (nombre, calificación). Se desea (a) los nombres de quienes aprueban (6 o más) y (b) el promedio de todas las calificaciones.

Paso 1. Entrada y salida. [("Ana", 9.0), ("Beto", 5.5), ("Carla", 7.5), ("Diego", 4.0)] → aprobados ["Ana", "Carla"]; promedio 6.5. Caso límite: ¿qué ocurre con la lista vacía? El promedio de nada no está definido (0 entre 0).

Paso 2. La cadena. (a) «De la lista de pares, quedarme con los que tienen nota ≥ 6 y quedarme con su nombre.» (b) «De la lista de pares, quedarme con las notas; sumarlas; dividir entre cuántas hay.»

Paso 3. El patrón. (a) filtrar + transformar. (b) transformar (extraer la nota) + combinar (suma) + un cálculo final.

Paso 4. Piezas pequeñas. Observe que son dos descripciones independientes del mismo dato: ninguna modifica la lista original y no importa en qué orden se calculen.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Paso 6. El caso límite. El promedio de una lista vacía divide entre cero. Los dos lenguajes lo tratan distinto: Python lanza ZeroDivisionError; Haskell, con números decimales, produce NaN (no es un número). Un diseño cuidadoso decide de antemano qué hacer: devolver un valor especial, o exigir una lista no vacía. Eso es parte de la verificación, no un detalle.


6. Caso C: las palabras largas de un texto

Problema. Dado un texto, obtener sus palabras de más de tres letras, en mayúsculas.

Paso 1. "la forma de pensar" → ["FORMA", "PENSAR"].

Paso 2. «Del texto, separar las palabras; quedarme con las largas; poner cada una en mayúsculas.»

Paso 3. Transformar (separar) → filtrar (longitud) → transformar (mayúsculas).

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

En Haskell, la cadena se escribe con funciones que se componen con el operador .:

largas :: String -> [String]
largas texto = map (map toUpper) (filter ((> 3) . length) (words texto))

Se lee de dentro hacia afuera: words separa; filter ((> 3) . length) conserva las palabras cuya longitud es mayor que 3 (la función (> 3) . length primero mide y después compara); map (map toUpper) aplica «poner en mayúsculas» a cada letra de cada palabra.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Nota: en Haskell, un texto (String) es una lista de caracteres. Por eso map toUpper funciona sobre un texto y map (map toUpper) sobre una lista de textos.


7. Recursión: pensar en la estructura de los datos

Cuando no hay variables que cambien, la repetición se expresa de otra manera: con recursión. La clave es pensar en la estructura del dato. Una lista tiene solo dos formas posibles:

  • vacía: [];
  • una cabeza seguida de una cola: x : xs (el primer elemento y la lista de todos los demás).

Si se sabe resolver el problema para cada una de esas dos formas, se sabe resolverlo para cualquier lista. Para ello se responden tres preguntas:

Pregunta En la suma de los cuadrados de los pares
1. Caso base: ¿qué devuelvo para la lista vacía? 0: la suma de nada es cero
2. La cabeza: ¿qué hago con el primer elemento? Si es par, su cuadrado cuenta; si no, no cuenta
3. La cola: ¿qué le pido a la propia función sobre el resto y cómo combino? Le pido la suma de la cola y la combino con lo de la cabeza
def suma_cuadrados_pares(xs):
    if xs == []:                                  # forma 1: lista vacía
        return 0
    cabeza = xs[0]                                # forma 2: cabeza y cola
    cola = xs[1:]
    if cabeza % 2 == 0:
        return cabeza * cabeza + suma_cuadrados_pares(cola)
    return suma_cuadrados_pares(cola)


print(suma_cuadrados_pares([1, 2, 3, 4]))         # 20

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

En Haskell, las dos formas de la lista son dos ecuaciones; el emparejamiento de patrones elige la que corresponde:

sumaLista :: [Int] -> Int
sumaLista [] = 0
sumaLista (x:xs) = x + sumaLista xs

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Definición — Recursión estructural: forma de escribir una función sobre un dato recursivo (como una lista) con una ecuación por cada forma posible del dato. La terminación está garantizada porque cada llamada recursiva trabaja sobre una parte más pequeña del dato.

Los ciclos son recursión con un acumulador. El programa imperativo del principio tiene una versión recursiva exacta: la variable total se convierte en un parámetro de la función, de modo que en vez de modificarla, cada llamada recibe un valor nuevo. La función ir de la corrida «Invertir una lista con un acumulador» sigue esa idea.


8. Errores frecuentes al cambiar de paradigma

Error común — traducir el ciclo línea por línea: buscar en el estilo funcional «dónde pongo el contador» o «dónde está el acumulador». La pregunta correcta es qué transformación describe el resultado.

Error común — modificar dentro de map: usar una función con efectos laterales (como append a una lista externa) dentro de map o de una comprensión. Se pierde la ventaja de poder razonar y se obtienen resultados que dependen del orden de evaluación.

Error común — olvidar el caso base: una función recursiva sin caso base no termina. Antes de escribir el caso recursivo, escriba el de la lista vacía.

Error común — pensar que la recursión es siempre lenta: en lenguajes funcionales la recursión es la forma normal de repetir y se optimiza; en Python, en cambio, la profundidad de la recursión está limitada (alrededor de mil llamadas), por lo que para listas largas se prefiere una comprensión o un reduce.

Error común — creer que «inmutable» significa «inútil»: si nada puede modificarse, ¿cómo se «actualiza» algo? Se construye un valor nuevo a partir del anterior; el anterior permanece. El tema 4.1 muestra cómo se hace de manera eficiente.

Error común — escribir una función enorme: si no puede describir en una frase qué hace una función, es probable que deba dividirse en piezas pequeñas.

Clasifique los siguientes fragmentos. Los de Java no se ejecutan en esta página.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


9. Verificación y reformulación

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


10. Libreta de trabajo

Este laboratorio interactivo requiere JavaScript habilitado en el navegador.


11. Preguntas de reflexión

  1. Elija un cálculo que haga habitualmente en una hoja de cálculo. Describa cómo se vería escrito como una cadena de transformaciones y como un ciclo con variables. ¿Cuál le parece más fácil de verificar y por qué?
  2. En el caso B, ¿qué decisiones debe tomar un diseño cuidadoso para el promedio de una lista vacía? Proponga dos y explique las consecuencias de cada una.
  3. Escriba, con sus palabras, las tres preguntas de la recursión sobre listas. Aplíquelas para diseñar una función que cuente cuántos elementos de una lista son mayores que 10.
  4. ¿Qué ventaja tiene poder probar cada pieza pequeña por separado? Relaciónelo con la pureza de las funciones.
  5. Describa un problema de su vida cotidiana o de otra materia que se exprese de manera natural como una cadena de filtrar, transformar y combinar.

12. Lecturas recomendadas

  • Hutton, G. (2016). Programming in Haskell (2nd ed.). Cambridge University Press.
  • Bird, R. (2015). Thinking functionally with Haskell. Cambridge University Press. (Disponible en la bóveda: 08-Referencias.)
  • Hughes, J. (1989). Why functional programming matters. The Computer Journal, 32(2), 98–107.
  • Felleisen, M., Findler, R. B., Flatt, M., & Krishnamurthi, S. (2018). How to design programs (2nd ed.). MIT Press. (La «receta de diseño» que inspira el método de seis pasos.)
  • Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulo sobre lenguajes de programación funcional. Pearson.
  • Python Software Foundation. Functional Programming HOWTO. Acceso libre en docs.python.org.