En la programación funcional, el manejo de errores se aborda de manera diferente a la programación imperativa. En lugar de utilizar excepciones, se prefieren estructuras de datos que representen explícitamente la posibilidad de un error. Esto permite que el flujo de datos y el manejo de errores sean más predecibles y seguros.

Conceptos Clave

  1. Inmutabilidad: Las estructuras de datos son inmutables, lo que significa que no se pueden cambiar una vez creadas. Esto ayuda a evitar errores relacionados con el estado mutable.
  2. Funciones Puras: Las funciones puras no tienen efectos secundarios y siempre producen el mismo resultado para los mismos argumentos.
  3. Monadas: Las monadas son estructuras que representan cálculos secuenciales y pueden manejar errores de manera elegante.

Estructuras Comunes para Manejo de Errores

Option

Option es una estructura que puede contener un valor o estar vacía. Se utiliza para representar la ausencia de un valor sin lanzar una excepción.

val someValue: Option[Int] = Some(5)
val noValue: Option[Int] = None

// Uso de Option
def divide(a: Int, b: Int): Option[Int] = {
  if (b == 0) None else Some(a / b)
}

val result = divide(10, 2) // Some(5)
val error = divide(10, 0)  // None

Either

Either es una estructura que puede contener un valor de dos posibles tipos. Por convención, Left se usa para representar un error y Right para un valor correcto.

def divide(a: Int, b: Int): Either[String, Int] = {
  if (b == 0) Left("Division by zero") else Right(a / b)
}

val result = divide(10, 2) // Right(5)
val error = divide(10, 0)  // Left("Division by zero")

Try

Try es una estructura que puede contener un valor exitoso (Success) o una excepción (Failure).

import scala.util.{Try, Success, Failure}

def divide(a: Int, b: Int): Try[Int] = Try(a / b)

val result = divide(10, 2) match {
  case Success(value) => s"Result: $value"
  case Failure(exception) => s"Error: ${exception.getMessage}"
}

val error = divide(10, 0) match {
  case Success(value) => s"Result: $value"
  case Failure(exception) => s"Error: ${exception.getMessage}"
}

Ejemplos Prácticos

Uso de Option

def findUserById(id: Int): Option[String] = {
  val users = Map(1 -> "Alice", 2 -> "Bob")
  users.get(id)
}

val user1 = findUserById(1) // Some("Alice")
val user2 = findUserById(3) // None

user1 match {
  case Some(name) => println(s"User found: $name")
  case None => println("User not found")
}

Uso de Either

def parseInt(str: String): Either[String, Int] = {
  try {
    Right(str.toInt)
  } catch {
    case _: NumberFormatException => Left("Invalid number format")
  }
}

val number = parseInt("123") // Right(123)
val error = parseInt("abc")  // Left("Invalid number format")

number match {
  case Right(value) => println(s"Parsed number: $value")
  case Left(error) => println(s"Error: $error")
}

Uso de Try

import scala.util.{Try, Success, Failure}

def readFile(filename: String): Try[String] = Try {
  scala.io.Source.fromFile(filename).mkString
}

val content = readFile("example.txt") match {
  case Success(data) => s"File content: $data"
  case Failure(exception) => s"Error: ${exception.getMessage}"
}

Ejercicios Prácticos

Ejercicio 1: Manejo de Errores con Option

Escribe una función safeDivide que tome dos enteros y devuelva un Option[Int] que represente el resultado de la división. Si el divisor es cero, la función debe devolver None.

def safeDivide(a: Int, b: Int): Option[Int] = {
  // Tu código aquí
}

Solución:

def safeDivide(a: Int, b: Int): Option[Int] = {
  if (b == 0) None else Some(a / b)
}

Ejercicio 2: Manejo de Errores con Either

Escribe una función parseDouble que tome una cadena y devuelva un Either[String, Double]. Si la cadena no puede convertirse a un número, la función debe devolver un mensaje de error en Left.

def parseDouble(str: String): Either[String, Double] = {
  // Tu código aquí
}

Solución:

def parseDouble(str: String): Either[String, Double] = {
  try {
    Right(str.toDouble)
  } catch {
    case _: NumberFormatException => Left("Invalid number format")
  }
}

Ejercicio 3: Manejo de Errores con Try

Escribe una función readFileContent que tome un nombre de archivo y devuelva un Try[String] que represente el contenido del archivo. Si el archivo no se puede leer, la función debe devolver una excepción en Failure.

import scala.util.{Try, Success, Failure}

def readFileContent(filename: String): Try[String] = {
  // Tu código aquí
}

Solución:

import scala.util.{Try, Success, Failure}

def readFileContent(filename: String): Try[String] = Try {
  scala.io.Source.fromFile(filename).mkString
}

Conclusión

El manejo de errores en la programación funcional en Scala se basa en el uso de estructuras de datos como Option, Either y Try para representar y manejar errores de manera explícita y segura. Estas estructuras permiten escribir código más robusto y predecible, evitando los problemas comunes asociados con las excepciones. A medida que avances en tu aprendizaje de Scala, estas técnicas te ayudarán a escribir programas más fiables y fáciles de mantener.

© Copyright 2024. Todos los derechos reservados