Inicio / Elixir / Elixir: Programación Funcional y Concurrente / Recursión y Enumerables

Recursión y Enumerables

Tail recursion, Enum, Stream y lazy evaluation.

Intermedio Funciones
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Recursión y Enumerables en Elixir

La recursión es la forma natural de iterar en lenguajes funcionales. Elixir, al no tener bucles imperativos como for o while tradicionales, depende de la recursión y de módulos como Enum y Stream para procesar colecciones. La optimización de tail recursion en la BEAM garantiza que las funciones recursivas bien escritas no agoten la pila.

Recursión Básica

La recursión en Elixir se expresa mediante múltiples cláusulas de función, con un caso base y un caso recursivo:

defmodule Recursion do
  # Sumar elementos de una lista
  def sumar([]), do: 0
  def sumar([cabeza | cola]) do
    cabeza + sumar(cola)
  end

  # Longitud de una lista
  def longitud([]), do: 0
  def longitud([_ | cola]), do: 1 + longitud(cola)

  # Invertir una lista
  def invertir(lista), do: invertir(lista, [])
  defp invertir([], acumulador), do: acumulador
  defp invertir([cabeza | cola], acumulador) do
    invertir(cola, [cabeza | acumulador])
  end
end

Recursion.sumar([1, 2, 3, 4, 5])  # => 15
Recursion.longitud([1, 2, 3])      # => 3
Recursion.invertir([1, 2, 3])      # => [3, 2, 1]

Tail Recursion

La tail recursion ocurre cuando la llamada recursiva es la última operación de la función. La BEAM optimiza estas llamadas para no consumir espacio adicional en la pila:

defmodule TailRecursion do
  # NO es tail recursive (la suma ocurre después de la recursión)
  def factorial_no_tail(0), do: 1
  def factorial_no_tail(n), do: n * factorial_no_tail(n - 1)

  # SÍ es tail recursive (usa acumulador)
  def factorial(n), do: factorial(n, 1)
  defp factorial(0, acc), do: acc
  defp factorial(n, acc), do: factorial(n - 1, n * acc)

  # Fibonacci tail recursive
  def fibonacci(n), do: fibonacci(n, 0, 1)
  defp fibonacci(0, a, _b), do: a
  defp fibonacci(n, a, b), do: fibonacci(n - 1, b, a + b)
end

TailRecursion.factorial(20)   # => 2432902008176640000
TailRecursion.fibonacci(10)   # => 55

El Módulo Enum

Enum proporciona funciones para trabajar con colecciones de forma eagerly (evalúa todo inmediatamente):

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map: transformar cada elemento
Enum.map(numeros, &(&1 * 2))
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# filter: seleccionar elementos
Enum.filter(numeros, &(rem(&1, 2) == 0))
# => [2, 4, 6, 8, 10]

# reduce: acumular un resultado
Enum.reduce(numeros, 0, &+/2)
# => 55

# Otras funciones útiles
Enum.any?(numeros, &(&1 > 5))       # => true
Enum.all?(numeros, &(&1 > 0))       # => true
Enum.find(numeros, &(&1 > 7))       # => 8
Enum.chunk_every(numeros, 3)         # => [[1,2,3],[4,5,6],[7,8,9],[10]]
Enum.zip([1, 2, 3], [:a, :b, :c])   # => [{1, :a}, {2, :b}, {3, :c}]
Enum.flat_map([[1,2],[3,4]], &(&1))  # => [1, 2, 3, 4]
Enum.group_by(numeros, &(rem(&1, 2) == 0))
# => %{false: [1,3,5,7,9], true: [2,4,6,8,10]}

El Módulo Stream

Stream crea enumerables lazy que solo se evalúan cuando se consumen. Ideal para datos grandes o infinitos:

# Stream lazy: no se evalúa hasta que se consume
resultado =
  1..1_000_000
  |> Stream.filter(&(rem(&1, 3) == 0))
  |> Stream.map(&(&1 * 2))
  |> Enum.take(5)
# => [6, 12, 18, 24, 30]

# Streams infinitos
Stream.iterate(1, &(&1 + 1))
|> Stream.filter(&(rem(&1, 7) == 0))
|> Enum.take(5)
# => [7, 14, 21, 28, 35]

# Stream.cycle: repite infinitamente
Stream.cycle([:rojo, :verde, :azul])
|> Enum.take(7)
# => [:rojo, :verde, :azul, :rojo, :verde, :azul, :rojo]

# Stream.unfold: generar secuencias
Stream.unfold(1, fn n -> {n, n * 2} end)
|> Enum.take(8)
# => [1, 2, 4, 8, 16, 32, 64, 128]

Lazy Evaluation vs Eager Evaluation

La diferencia clave entre Enum y Stream se observa en cadenas de transformaciones:

# Eager: crea listas intermedias en cada paso
1..100
|> Enum.map(&(&1 * 3))       # Lista intermedia 1
|> Enum.filter(&(&1 > 50))   # Lista intermedia 2
|> Enum.take(5)
# Procesa los 100 elementos en cada paso

# Lazy: procesa elemento por elemento
1..100
|> Stream.map(&(&1 * 3))     # No evalúa aún
|> Stream.filter(&(&1 > 50)) # No evalúa aún
|> Enum.take(5)               # Ahora evalúa solo lo necesario
# Solo procesa ~18 elementos hasta obtener 5 resultados

Reduce: La Base de Todo

reduce es la función más fundamental — muchas otras funciones se pueden implementar con ella:

defmodule MiEnum do
  # Implementar map con reduce
  def map(lista, funcion) do
    lista
    |> Enum.reduce([], fn elem, acc -> [funcion.(elem) | acc] end)
    |> Enum.reverse()
  end

  # Implementar filter con reduce
  def filter(lista, predicado) do
    lista
    |> Enum.reduce([], fn elem, acc ->
      if predicado.(elem), do: [elem | acc], else: acc
    end)
    |> Enum.reverse()
  end

  # Frecuencia de elementos
  def frecuencias(lista) do
    Enum.reduce(lista, %{}, fn elem, acc ->
      Map.update(acc, elem, 1, &(&1 + 1))
    end)
  end
end

MiEnum.frecuencias(["a", "b", "a", "c", "b", "a"])
# => %{"a" => 3, "b" => 2, "c" => 1}

Resumen

La recursión con tail call optimization permite iterar de forma segura sin agotar la pila. El módulo Enum proporciona más de 70 funciones para transformar colecciones de forma eager, mientras que Stream ofrece evaluación lazy para datos grandes o infinitos. reduce es la función más poderosa del arsenal funcional, capaz de implementar prácticamente cualquier transformación. Elegir entre Enum y Stream depende del tamaño de los datos y las necesidades de rendimiento de cada situación.

🔒

Ejercicio práctico disponible

Recursión y Enum/Stream

Desbloquear ejercicios
// Recursión y Enum/Stream
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes