Control de Flujo

Estructuras para tomar decisiones y manejar diferentes casos.

case

case evalúa una expresión contra múltiples patrones:

case File.read("config.txt") do
  {:ok, contenido} ->
    IO.puts("Contenido: #{contenido}")

  {:error, :enoent} ->
    IO.puts("Archivo no encontrado")

  {:error, razon} ->
    IO.puts("Error: #{razon}")
end

case con guards

case numero do
  n when n < 0 -> :negativo
  0 -> :cero
  n when n > 0 -> :positivo
end

El patrón catch-all

case valor do
  1 -> "uno"
  2 -> "dos"
  _ -> "otro"  # Captura cualquier otro valor
end
CaseClauseError

Si ningún patrón coincide y no hay catch-all, Elixir lanza un error. Siempre incluye un caso _ si no estás seguro de cubrir todos los casos.

cond

cond evalúa múltiples condiciones booleanas (como if/else if encadenados):

cond do
  edad < 13 -> "niño"
  edad < 18 -> "adolescente"
  edad < 65 -> "adulto"
  true -> "senior"  # Equivale a "else"
end
¿case o cond?

case: Cuando comparas un valor contra patrones.
cond: Cuando evalúas múltiples condiciones independientes.

if y unless

Para condiciones simples:

# if básico
if edad >= 18 do
  "Mayor de edad"
end

# if con else
if edad >= 18 do
  "Mayor de edad"
else
  "Menor de edad"
end

# Forma de una línea
if activo, do: "Sí", else: "No"

# unless (contrario de if)
unless lista_vacia, do: "Tiene elementos"
if retorna un valor

En Elixir, if es una expresión que retorna un valor:

mensaje = if hora < 12, do: "Buenos días", else: "Buenas tardes"

with

with es perfecto para encadenar operaciones que pueden fallar:

# Sin with (anidación profunda)
case File.read("config.json") do
  {:ok, contenido} ->
    case Jason.decode(contenido) do
      {:ok, datos} ->
        case Map.fetch(datos, "usuario") do
          {:ok, usuario} -> {:ok, usuario}
          :error -> {:error, "usuario no encontrado"}
        end
      error -> error
    end
  error -> error
end

# Con with (limpio y lineal)
with {:ok, contenido} <- File.read("config.json"),
     {:ok, datos} <- Jason.decode(contenido),
     {:ok, usuario} <- Map.fetch(datos, "usuario") do
  {:ok, usuario}
else
  :error -> {:error, "usuario no encontrado"}
  {:error, razon} -> {:error, razon}
end

with sin else

Si omites else, el primer valor que no haga match se retorna directamente:

with {:ok, a} <- obtener_a(),
     {:ok, b} <- obtener_b(a) do
  {:ok, a + b}
end
# Si obtener_a() retorna {:error, :no_encontrado}, with retorna eso

Manejo de errores

try/rescue

try do
  String.to_integer("no soy número")
rescue
  ArgumentError -> "No es un número válido"
  e in RuntimeError -> "Error: #{e.message}"
end

try/catch (para throws)

try do
  throw({:error, "algo salió mal"})
catch
  {:error, mensaje} -> IO.puts("Capturado: #{mensaje}")
end

after

try do
  # operación riesgosa
rescue
  e -> IO.puts("Error: #{Exception.message(e)}")
after
  # Siempre se ejecuta (limpieza)
  IO.puts("Limpieza completa")
end
Evita try/rescue cuando sea posible

En Elixir, es preferible usar pattern matching con tuplas {:ok, valor} / {:error, razon} en lugar de excepciones. Reserva try/rescue para errores verdaderamente excepcionales.

raise y reraise

# Lanzar excepción
raise "Algo salió mal"
raise ArgumentError, message: "argumento inválido"

# Definir excepción personalizada
defmodule MiError do
  defexception message: "error por defecto"
end

raise MiError, message: "error específico"

Funciones con ! (bang)

Por convención, las funciones que terminan en ! lanzan excepciones en lugar de retornar tuplas:

# Retorna {:ok, contenido} o {:error, razon}
File.read("archivo.txt")

# Retorna contenido directamente o lanza excepción
File.read!("archivo.txt")

# Útil cuando sabes que el archivo existe
config = File.read!("config.json") |> Jason.decode!()

Ejemplo completo: Validación de usuario

defmodule Validador do
  def validar_usuario(params) do
    with {:ok, nombre} <- validar_nombre(params),
         {:ok, email} <- validar_email(params),
         {:ok, edad} <- validar_edad(params) do
      {:ok, %{nombre: nombre, email: email, edad: edad}}
    end
  end

  defp validar_nombre(%{"nombre" => nombre}) when is_binary(nombre) do
    if String.length(nombre) >= 2 do
      {:ok, nombre}
    else
      {:error, "nombre muy corto"}
    end
  end
  defp validar_nombre(_), do: {:error, "nombre requerido"}

  defp validar_email(%{"email" => email}) when is_binary(email) do
    if String.contains?(email, "@") do
      {:ok, email}
    else
      {:error, "email inválido"}
    end
  end
  defp validar_email(_), do: {:error, "email requerido"}

  defp validar_edad(%{"edad" => edad}) when is_integer(edad) and edad >= 18 do
    {:ok, edad}
  end
  defp validar_edad(%{"edad" => _}), do: {:error, "debe ser mayor de edad"}
  defp validar_edad(_), do: {:error, "edad requerida"}
end

# Uso
iex> Validador.validar_usuario(%{"nombre" => "Ana", "email" => "[email protected]", "edad" => 25})
{:ok, %{nombre: "Ana", email: "[email protected]", edad: 25}}

iex> Validador.validar_usuario(%{"nombre" => "A"})
{:error, "nombre muy corto"}
Ejercicio 6.1 Clasificador de notas Básico

Crea una función que tome una nota (0-100) y retorne:

  • "Insuficiente" (0-49)
  • "Suficiente" (50-59)
  • "Bien" (60-69)
  • "Notable" (70-89)
  • "Sobresaliente" (90-100)

Usa cond y maneja el caso de notas inválidas.

Ejercicio 6.2 Parser de configuración Intermedio

Crea una función que use with para:

  • Leer un archivo de configuración
  • Parsear su contenido como JSON
  • Extraer una clave específica
  • Retornar {:ok, valor} o un error descriptivo