Funciones recursivas con listas

Volvemos con la recursividad, esta vez para hablar de cómo aprovechar el pattern matching en listas, por ejemplo, para hacer funciones reductoras o transformadoras. Normalmente querremos usar las funciones nativas del módulo Enum o Stream, pero cuando no quede otra, tenemos a nuestra disposición recursividad.

Cuando trabajamos con Elixir, la recursividad es una herramienta fundamental para procesar listas y colecciones. En particular, la combinación de recursividad con pattern matching nos permite crear funciones que operan sobre listas de forma clara y elegante, aunque no siempre la más eficiente. Sin embargo, entender cómo construir estas funciones a mano nos ayuda a comprender mejor el funcionamiento interno de funciones comunes como map, filter o reduce.

Para empezar, pensemos en una función que tome una lista de números y devuelva una lista con el doble de cada uno de esos números. En Elixir, la recursividad suele implementarse directamente en los parámetros de la función mediante pattern matching. Por ejemplo, podemos definir una función llamada dobles que tenga dos cláusulas: una para el caso base, cuando la lista está vacía, y otra para cuando la lista tiene al menos un elemento.

En el caso base, si la lista está vacía, simplemente devolvemos una lista vacía, porque el doble de nada es nada. En la segunda cláusula, descomponemos la lista en cabeza y cola. La cabeza es el primer elemento, y la cola es el resto de la lista. Entonces, construimos una nueva lista donde el primer elemento es el doble de la cabeza original, y el resto es el resultado de llamar recursivamente a dobles con la cola.

Este proceso se repite hasta que la cola se vacía, momento en el que la función devuelve la lista vacía y la recursión termina. Así, cada llamada procesa un elemento y lo transforma, y luego delega el resto del trabajo a la llamada recursiva siguiente.

defmodule RecursionListas do
  def dobles([]), do: []
  def dobles([cabeza | cola]) do
    [cabeza * 2 | dobles(cola)]
  end
end

Si probamos esta función con una lista como [1, 2, 3, 4, 5], obtendremos [2, 4, 6, 8, 10], replicando el comportamiento de un map que dobla cada elemento.

Siguiendo esta misma idea, podemos construir una función pares que filtre los números pares de una lista. Aquí, además del pattern matching, usamos guardas para decidir si la cabeza cumple la condición de ser par. Si la cabeza es par, la incluimos en la lista resultado y seguimos procesando la cola. Si no, simplemente descartamos la cabeza y seguimos con la cola.

defmodule RecursionListas do
  def pares([]), do: []
  def pares([cabeza | cola]) when rem(cabeza, 2) == 0 do
    [cabeza | pares(cola)]
  end
  def pares([_cabeza | cola]) do
    pares(cola)
  end
end

Es importante destacar que en las guardas solo podemos usar ciertas expresiones y funciones, ya que no todo es válido en ese contexto. Por ejemplo, no podemos llamar a funciones arbitrarias dentro de un when, salvo que sean macros o funciones permitidas.

Para hacer estas funciones más flexibles, podemos pasar una función como parámetro que defina el criterio o la transformación a aplicar. Esto nos permite generalizar el comportamiento de filter o map y reutilizar el código para diferentes casos.

Finalmente, para implementar un reduce artesanal, podemos definir una función que reciba una lista y un acumulador inicial. Si la lista está vacía, devolvemos el acumulador. Si no, descomponemos la lista en cabeza y cola, y llamamos recursivamente a reduce con la cola y el acumulador actualizado con la operación que queramos realizar, por ejemplo, sumar la cabeza al acumulador.

defmodule RecursionListas do
  def suma(lista), do: suma(lista, 0)

  defp suma([], acumulador), do: acumulador
  defp suma([cabeza | cola], acumulador) do
    suma(cola, acumulador + cabeza)
  end
end

Aquí, suma/1 es una función fachada que inicia la suma con un acumulador en cero, mientras que suma/2 es la función recursiva privada que realiza el trabajo real.

Este enfoque nos permite entender cómo funcionan internamente muchas de las funciones que usamos a diario en Elixir, y practicar la recursividad con listas de forma clara y didáctica. Además, nos prepara para enfrentar problemas más complejos donde la recursividad y el pattern matching son esenciales.

Lista de reproducción
  1. 1
    ¿Qué es Elixir?
    10 minutos
  2. 2
    Instalación de Elixir
    9 minutos
  3. 3
    ¿Qué es la programación funcional? (Como la de Elixir)
    20 minutos
  4. 4
    ¿Cómo funciona la REPL de Elixir?
    7 minutos
  5. 5
    ¿Cómo hacer asignaciones en Elixir?
    7 minutos
  6. 6
    Operadores aritméticos básicos
    6 minutos
  7. 7
    ¿Qué son los tipos de datos de Elixir?
    5 minutos
  8. 8
    Átomos en Elixir
    4 minutos
  9. 9
    Las palabras clave nil, true y false
    6 minutos
  10. 10
    Operadores lógicos de comparación
    8 minutos
  11. 11
    Comparación estricta con ===
    3 minutos
  12. 12
    Operadores lógicos y proposicionales
    8 minutos
  13. 13
    Invocación de funciones
    10 minutos
  14. 14
    Fundamentos de funciones
    9 minutos
  15. 15
    Cadenas de caracteres
    8 minutos
  16. 16
    Entrada y salida estandar de la mano de gets y puts
    9 minutos
  17. 17
    Concatenar e interpolar strings
    9 minutos
  18. 18
    Código fuente en archivos
    9 minutos
  19. 19
    Condicional IF y bloques DO-END
    11 minutos
  20. 20
    IFs anidados, UNLESS y COND
    12 minutos
  21. 21
    Definición de funciones
    11 minutos
  22. 22
    Fundamentos de compilación de módulos
    6 minutos
  23. 23
    Guardas
    8 minutos
  24. 24
    Funciones anónimas
    7 minutos
  25. 25
    Capturar funciones
    4 minutos
  26. 26
    Invocación de funciones dentro del mismo módulo
    7 minutos
  27. 27
    Tuplas
    8 minutos
  28. 28
    Introducción al pattern matching
    8 minutos
  29. 29
    Pattern matching en funciones
    11 minutos
  30. 30
    Las tuplas :ok, :error
    7 minutos
  31. 31
    case
    10 minutos
  32. 32
    Operador pin
    7 minutos
  33. 33
    Pattern matchings y recursividad
    5 minutos
  34. 34
    Listas
    9 minutos
  35. 35
    Operadores y funciones de lista
    10 minutos
  36. 36
    Keyword lists: listas de palabras clave
    8 minutos
  37. 37
    Mapas
    7 minutos
  38. 38
    Pattern matching de mapas y keyword lists
    6 minutos
  39. 39
    Operadores y funciones para mapas y keyword lists
    5 minutos
  40. 40
    Estructuras con defstruct
    11 minutos
  41. 41
    Bitstrings
    11 minutos
  42. 42
    Charlists
    10 minutos
  43. 43
    Funciones de alto orden en Elixir
    5 minutos
  44. 44
    Uso de la función filter
    10 minutos
  45. 45
    Uso de la función map
    7 minutos
  46. 46
    Uso de la función reduce
    9 minutos
  47. 47
    Pipelines
    11 minutos
  48. 48
    Rangos y Streams
    11 minutos
  49. 49
    Funciones recursivas con listas
    14 minutos