Colecciones

Domina el módulo Enum y las comprehensions para manipular datos.

El módulo Enum

El módulo Enum es tu caja de herramientas para trabajar con colecciones (listas, mapas, rangos, etc.):

Funciones básicas

lista = [1, 2, 3, 4, 5]

# Contar elementos
iex> Enum.count(lista)
5

# Verificar si está vacía
iex> Enum.empty?(lista)
false

# Primer y último elemento
iex> Enum.at(lista, 0)
1
iex> Enum.at(lista, -1)  # Índice negativo desde el final
5

# Verificar membresía
iex> Enum.member?(lista, 3)
true
iex> 3 in lista  # Alternativa más legible
true

map/2 - Transformar elementos

Aplica una función a cada elemento y retorna una nueva lista:

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

# Con sintaxis corta
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

# Con captura de función
iex> Enum.map(["hola", "mundo"], &String.upcase/1)
["HOLA", "MUNDO"]

# Con mapas
iex> usuarios = [%{nombre: "Ana"}, %{nombre: "Pedro"}]
iex> Enum.map(usuarios, & &1.nombre)
["Ana", "Pedro"]

filter/2 - Filtrar elementos

Retorna solo los elementos que cumplen una condición:

iex> Enum.filter([1, 2, 3, 4, 5], fn x -> x > 2 end)
[3, 4, 5]

# Filtrar pares
iex> Enum.filter(1..10, &(rem(&1, 2) == 0))
[2, 4, 6, 8, 10]

# reject es lo opuesto
iex> Enum.reject(1..10, &(rem(&1, 2) == 0))
[1, 3, 5, 7, 9]

reduce/3 - Acumular valores

Reduce una colección a un solo valor:

# Sumar todos los elementos
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6

# Sintaxis corta
iex> Enum.reduce([1, 2, 3], 0, &(&1 + &2))
6

# También existe Enum.sum/1
iex> Enum.sum([1, 2, 3])
6

# Construir un mapa a partir de una lista
iex> palabras = ["hola", "mundo", "elixir"]
iex> Enum.reduce(palabras, %{}, fn palabra, acc ->
...>   Map.put(acc, palabra, String.length(palabra))
...> end)
%{"elixir" => 6, "hola" => 4, "mundo" => 5}

Más funciones útiles de Enum

Ordenamiento

iex> Enum.sort([3, 1, 2])
[1, 2, 3]

# Orden descendente
iex> Enum.sort([3, 1, 2], :desc)
[3, 2, 1]

# Por criterio personalizado
iex> usuarios = [%{nombre: "Zara"}, %{nombre: "Ana"}]
iex> Enum.sort_by(usuarios, & &1.nombre)
[%{nombre: "Ana"}, %{nombre: "Zara"}]

Búsqueda

# Encontrar el primer elemento que cumple condición
iex> Enum.find([1, 2, 3, 4], &(&1 > 2))
3

# Verificar si alguno cumple
iex> Enum.any?([1, 2, 3], &(&1 > 2))
true

# Verificar si todos cumplen
iex> Enum.all?([2, 4, 6], &(rem(&1, 2) == 0))
true

Agrupación y partición

# Agrupar por criterio
iex> Enum.group_by(["ana", "bob", "amy"], &String.first/1)
%{"a" => ["ana", "amy"], "b" => ["bob"]}

# Partir en dos grupos
iex> Enum.split_with(1..10, &(rem(&1, 2) == 0))
{[2, 4, 6, 8, 10], [1, 3, 5, 7, 9]}

Tomar y soltar

iex> Enum.take([1, 2, 3, 4, 5], 3)
[1, 2, 3]

iex> Enum.drop([1, 2, 3, 4, 5], 2)
[3, 4, 5]

iex> Enum.take_while([1, 2, 3, 4], &(&1 < 3))
[1, 2]

iex> Enum.chunk_every([1, 2, 3, 4, 5], 2)
[[1, 2], [3, 4], [5]]

Rangos

Los rangos son una forma eficiente de representar secuencias:

iex> 1..5
1..5

iex> Enum.to_list(1..5)
[1, 2, 3, 4, 5]

# Con paso
iex> Enum.to_list(1..10//2)
[1, 3, 5, 7, 9]

# Descendente
iex> Enum.to_list(5..1//-1)
[5, 4, 3, 2, 1]

Comprehensions (for)

Las comprehensions son una forma elegante de transformar y filtrar colecciones:

# Básico: equivale a Enum.map
iex> for x <- [1, 2, 3], do: x * 2
[2, 4, 6]

# Con filtro
iex> for x <- 1..10, rem(x, 2) == 0, do: x
[2, 4, 6, 8, 10]

# Múltiples generadores (producto cartesiano)
iex> for x <- [1, 2], y <- [:a, :b], do: {x, y}
[{1, :a}, {1, :b}, {2, :a}, {2, :b}]

# Con pattern matching
iex> usuarios = [%{nombre: "Ana", activo: true}, %{nombre: "Pedro", activo: false}]
iex> for %{nombre: nombre, activo: true} <- usuarios, do: nombre
["Ana"]

into: Cambiar el tipo de resultado

# Generar un mapa
iex> for x <- ["a", "b"], into: %{}, do: {x, String.upcase(x)}
%{"a" => "A", "b" => "B"}

# Generar un string
iex> for c <- [72, 111, 108, 97], into: "", do: <<c>>
"Hola"

El módulo Stream

Stream es como Enum pero perezoso (lazy). Las operaciones no se ejecutan hasta que son necesarias:

# Enum ejecuta cada paso completamente
1..1000000
|> Enum.map(&(&1 * 2))     # Crea lista de 1M elementos
|> Enum.filter(&(&1 < 10))  # Filtra toda la lista
|> Enum.take(3)              # Toma 3

# Stream procesa elemento por elemento
1..1000000
|> Stream.map(&(&1 * 2))     # No ejecuta aún
|> Stream.filter(&(&1 < 10))  # No ejecuta aún
|> Enum.take(3)              # Aquí se ejecuta todo (solo 3 elementos)
¿Cuándo usar Stream?

Usa Stream cuando trabajes con colecciones grandes o infinitas, o cuando encadenes muchas operaciones. Para colecciones pequeñas, Enum es más simple y a menudo más rápido.

Streams infinitos

# Secuencia infinita de números
iex> Stream.iterate(0, &(&1 + 1)) |> Enum.take(5)
[0, 1, 2, 3, 4]

# Repetir un valor
iex> Stream.repeatedly(fn -> :rand.uniform(10) end) |> Enum.take(5)
[7, 2, 9, 1, 4]

# Ciclar una lista
iex> Stream.cycle([:a, :b, :c]) |> Enum.take(7)
[:a, :b, :c, :a, :b, :c, :a]

Ejemplo práctico: Procesamiento de datos

defmodule Ventas do
  def procesar(transacciones) do
    transacciones
    |> Enum.filter(& &1.completada)           # Solo completadas
    |> Enum.map(& &1.monto)                   # Extraer montos
    |> Enum.filter(&(&1 > 0))                 # Solo positivos
    |> estadisticas()
  end

  defp estadisticas(montos) do
    %{
      total: Enum.sum(montos),
      promedio: Enum.sum(montos) / max(Enum.count(montos), 1),
      maximo: Enum.max(montos, fn -> 0 end),
      cantidad: Enum.count(montos)
    }
  end
end
Ejercicio 5.1 Análisis de palabras Básico

Dada una lista de palabras, usa Enum para:

  • Filtrar las que tienen más de 5 letras
  • Convertirlas a mayúsculas
  • Ordenarlas por longitud (de menor a mayor)
Ejercicio 5.2 Contador de frecuencias Intermedio

Implementa una función que tome una lista de elementos y retorne un mapa con la frecuencia de cada uno:

frecuencias([:a, :b, :a, :c, :a])
# => %{a: 3, b: 1, c: 1}
Ejercicio 5.3 Tabla de multiplicar Intermedio

Usa una comprehension para generar una tabla de multiplicar del 1 al 10 como lista de tuplas {a, b, resultado}.