Efectos colaterales en programación funcional

Programación funcional y reactiva - Computación

Profesor: Ing. Santiago Quiñones

Docente Investigador

Departamento de Ingeniería Civil

Contenidos

El problema de los efectos colaterales

Entendiendo a los efectos colaterales

Programación funcional se basa en que las funciones no deberían tener efectos colaterales.

Un ejemplo:

Entendiendo a los efectos colaterales

En muchos lenguajes de programación imperativos, la llamada a la función print("hola") imprimirá algo y no devolverá nada.

Otro ejemplo:

En lenguajes funcionales puros, una función de impresión toma un objeto que representa el estado del mundo exterior y devuelve un nuevo objeto que representa al estado después de haber realizado la salida.

Efectos colaterales

Se considera que una función tiene efectos colaterales si

  • Modifica el valor de una variable o de una estructura de datos mutable.
  • Utiliza mecanismos de IO
  • Lanza una excepción
  • Se detiene por un error

Solución a los efectos colaterales

La solución es dejar de usar efectos colaterales y codificarlos en el valor de retorno.

Efectos colaterales - solución - Ejemplo

Representa al error como una estructura de datos.

Fenómenos representados como datos.

def division(n1: Double, n2: Double) =
  if (n2 == 0) throw new RuntimeException("División por 0")
  else n1 / n2
import scala.util.Try

def pureDivision(n1: Double, n2: Double): Try[Double] =
  Try { division(n1, n2) }

Efectos colaterales - solución - Ejemplo

Option / Some / None

Option / Some / None

Presencia vs ausencia

Representa un valor opcional. No hay información de error, solo "existe" (Some) o "no existe" (None). Se utiliza para representar la presencia o ausencia de un valor, reemplazando al infame null.

  • Some(valor): Contiene el dato.
  • None: Representa la ausencia.

Option

// La forma canónica y recomendada
def dividir(a: Int, b: Int): Option[Int] =
  if b == 0 then None
  else Some(a / b)

Ejemplo

Option

def getMiddleName(personId: Int): Option[String] = {
  if (personId == 1) Some("Luis") // Tiene segundo nombre
  else None                      // No tiene segundo nombre
}

// Uso
val middleName = getMiddleName(1)
middleName match {
  case Some(name) => println(s"El segundo nombre es $name")
  case None       => println("No tiene segundo nombre")
}

Ejemplo

Option

val inventory = List(
  ("Laptop", 1200.50),
  ("Mouse", 25.75),
  ("Keyboard", 45.00)
)

def findPrice(productName: String): Option[Double] = {
  inventory.find(_._1 == productName).map(_._2)
}

println(findPrice("Laptop"))    // Some(1200.5)
println(findPrice("Mouse"))     // Some(25.75)
println(findPrice("Monitor"))   // None

Ejemplo

Option

def getText(msg: String): Option[String] = {
  if (msg != null) Some(msg.toUpperCase) else None
}

// Uso
val length = getText(null).map(_.length).getOrElse(0)
// o
val length = getText(null).map(_.length)  // Option[Int]

Ejemplo

Option / Some / None

Manejo de nulos en tú código

Analice el siguiente código

  • ¿Qué hace el método toInt?
  • ¿Qué devuelve el método toInt?
  • ¿Qué devuelve toInt("1")?
  • ¿Qué devuelve toInt("Uno")?
  • ¿Cuál es el tipo de dato de txtNumbers?
  • ¿Qué tipo de dato devuelve txtNumbers.map(toInt)?
  • ¿Qué hace la función flatten?
def toInt(s: String) : Option[Int] = {
  try {
    Some(Integer.parseInt(s))
  } catch {
    case e: Exception => None
  }
}


Scala Option

Manejo de nulos

Alternativa Option

Si tiene la tentación de usar un valor nulo piense en Option

Option: representación de valores opcionales

Scala Option

Manejo de nulos

def toInt(s: String) : Option[Int] = {
  try {
    Some(Integer.parseInt(s))
  } catch {
    case e: Exception => None
  }
}



import scala.util.control.Exception._
def toInt(s: String): Option[Int] = allCatch.opt(s.toInt)

Option

Obtener valores

val x = toInt("1").getOrElse(0)
toInt("1").foreach { i => printf("Obtener un Int:%d", i) }
toInt("1") match {
  case Some(i) => println(i)
  case None => println("That didn't work.")
}

Usar:

getOrElse

match

foreach

Option en Listas

Ejemplo

def toInt(s: String): Option[Int] = {
  try {
    Some(Integer.parseInt(s))
  } catch {
    case e: Exception => None
  }
}

val txtNumbers = List("1", "2", "foo", "3", "bar")
txtNumbers.map(toInt)

Option en Listas

Ejemplo

def toInt(s: String): Option[Int] = {
  try {
    Some(Integer.parseInt(s))
  } catch {
    case e: Exception => None
  }
}

val txtNumbers = List("1", "2", "foo", "3", "bar")
txtNumbers.map(toInt)
txtNumbers.map(toInt).flatten

Ejemplo de Option

Un cine tiene asientos numerados del 1 al 100. Escribe una función selectSeat que reciba un número de asiento (Int) y devuelva un Option[String]:

  • Si el número de asiento está entre 1 y 100 (inclusive), devuelve Some("Asiento seleccionado: [número]").
  • Si el número está fuera del rango, devuelve None.

Ejemplo de Option

// Definición de la función selectSeat
def selectSeat(seatNumber: Int): Option[String] = {
  if (seatNumber >= 1 && seatNumber <= 100) {
    Some(s"Asiento seleccionado: $seatNumber")
  } else {
    None
  }
}

// Función para mostrar el resultado al usuario
def displaySeatSelection(seatOption: Option[String]): String =
  seatOption.getOrElse("El asiento seleccionado no es válido.")

// Ejemplo de uso
val seats = List(50, 150, 1, -10, 100) // Lista de asientos a probar

// Uso funcional para procesar y mostrar todos los asientos
seats.map(selectSeat).map(displaySeatSelection).foreach(println)

Option

Utiliza option si necesitas representar la presencia o ausencia de un valor. Cuando no estás seguro de que exista un valor

 

Ejemplo: Buscar datos dentro de una base de datos, donde no siempre existe un resultado. Para evitar null. Cuando no importa por qué no hay dato (ej. buscar en un Mapa).

Either / Left / Right

Either / Left / Right

Disyunción lógica

Un resultado correcto o un error informativo. Se utiliza cuando una operación puede fallar y necesitamos saber por qué.

  • Left(Error): Convencionalmente contiene el fallo o error.
  • Right(Success): Contiene el valor exitoso.

Either / Left / Right

Ejemplo

def dividir(a: Int, b: Int): Either[String, Int] = {
  if (b == 0) Left("Error: División por cero") 
  else Right(a / b) 
} 

val calculo = dividir(10, 0) 
calculo match { 
  case Right(v) => println(s"Total: $v") 
  case Left(e) => println(s"Falló: $e") 
}

Either / Left / Right

Ejemplo

def validarEdad(edad: Int): Either[String, Int] =
  if edad < 18 then Left("Error: Menor de edad") // Fallo explicativo
  else Right(edad)                               // Éxito

Either / Left / Right

Ejemplo

Either / Left / Right

Invocaciones

divideXByY(1, 0) match {
  case Left(s) => println("Answer: " + s)
  case Right(i) => println("Answer: " + i)
}

Either / Left / Right

Invocaciones

val x = divideXByY(1, 0)
// x: Either[String, Int] = Left(No se puede dividir por 0)

x.isLeft
// res0: Boolean = true

x.left
// res1: LeftProjection[String, Int] = LeftProjection(Left(No se puede dividir por 0))

Either

Utiliza either si necesitas manejar errores explícitos con información adicional.

 

Cuando usarlo:

  • Validación donde quieras devolver un mensaje de error detallado. Para reglas de negocio. 
  • Para reglas de negocio. Cuando necesitas decir al usuario por qué falló (ej. "Saldo insuficiente").
  • Si tu función tiene multiples razones para fallar y necesitas distinguirlas. Ej.: Si estuvieras haciendo una "Calculadora Financiera" donde hay reglas de negocio complejas: "No se puede dividir para cero", "No se permiten resultados negativos", "El numerador no puede ser mayor a 1 millón".

Try

Try / Failure / Success

Protección

Un cálculo que tuvo éxito o "explotó" (crasheó). Diseñado específicamente para envolver código inseguro que podría lanzar excepciones (como convertir Strings a números o leer archivos).

  • Success(v): La operación se completó sin errores.
  • Failure(e): Captura la excepción lanzada.

Actúa como una red de seguridad funcional alrededor de código imperativo.

Try ejemplos - ingreso de datos

Una forma de ingreso de datos en Scala es a través del método readLine que pertenece al objeto

El método toInt permite transformar un String a Int.

¿Qué sucedería en el código anterior si en lugar de un número ingresa un texto o un valor real?

Try ejemplos - Parseo de datos

import scala.util.{Try, Success, Failure} 
def parsearNumero(s: String): Try[Int] = { 
  Try(s.toInt) // Código inseguro envuelto 
} 

val input = "123a" // Esto fallaría normalmente 
parsearNumero(input) match { 
  case Success(num) => println(s"Número: $num") 
  case Failure(ex) => println(s"Error: ${ex.getMessage}") }

Try ejemplos

¿Qué sucedería en el código anterior si en lugar de un número ingresa un texto o un valor real?

Try ejemplos

¿Cómo solucionaría esos problemas?

Try ejemplos

Un pequeño programa de ingreso de datos

import scala.io.StdIn

object Exa0 {
  def main(args: Array[String]) = {
    val name = StdIn.readLine("Nombre: ")
    val age = StdIn.readLine("Edad: ")
    val weight = StdIn.readLine("Peso: ")
    printf("Hola %s, tienes %s años y pesas %skg\n", name, age, weight)
  }
}

Pero y ¿si el usuario se equivoca en el ingreso de los datos?

Try ejemplos

La solución final

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

object Ex4 {
  def main(args: Array[String]): Unit = {
    print(inData())
  }

  def inData(): String = {
    val name = StdIn.readLine("Nombre: ")
    val age = Try(StdIn.readLine("Edad: ").toInt)
    val weight = Try(StdIn.readLine("Peso: ").toDouble)

    "Hola %s, tienes %d años y pesas %.2fkg\n".format(
      name,
      age match {
        case Success(v) => v
        case Failure(e) => {
          println("Error en la edad")
          inData()
        }
      },
      weight match {
        case Success(v) => v
        case Failure(e) => {
          println("Error en el peso")
          inData()
        }
      }
    )
  }
}

Lectura de un archivo

La lectura de un archivo puede lanzar diferentes excepciones

import scala.io.Source
Source.fromFile("myData.txt")


import scala.util.Try
import scala.io.Source

def readDataFromFile(filepath: String): Try[List[String]] =
  Try{
    var source = Source.fromFile(filepath)
    var data = source.getLines.toList
    source.close()
    data
  }

var data = readDataFromFile("C:/scala/files/numeros.txt").get

Try ejemplos - Procesamiento de valores

Suponga que tiene una lista de valores que le fueron entregados y que supuestamente representan números que necesita para trabajar.

val values = List("1", "3", "5", "9", "2", "2 0")


import scala.util.Try
val lista_valores = values.map(v => Try { v.toInt })

val lista_valores: List[scala.util.Try[Int]] = List(Success(1), Success(3), Success(5), Success(9), Success(2), Failure(java.lang.NumberFormatException: For input string: "2 0"))

Try ejemplos - Procesamiento de valores

¿Cómo procesarlos?

Try ejemplos - Procesamiento de valores leídos desde un archivo

Suponga que tiene un lista de valores que fueron obtenidos desde un archivo de texto y que supuestamente representan números que necesita para trabajar

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


def readNumbersFromFile(filename: String): Try[List[Int]] = {
  Try {
    val source = Source.fromFile(filename)
    val numbers = source.getLines().map(_.toInt).toList
    source.close()
    numbers
  }
}

// Pruebas
println(readNumbersFromFile("numbers.txt")) 
// Si el archivo contiene: 
// 1
// 2
// 3
// Resultado: Success(List(1, 2, 3))

println(readNumbersFromFile("missing.txt")) 
// Resultado: Failure(java.io.FileNotFoundException)

println(readNumbersFromFile("invalid.txt")) 
// Si el archivo contiene: 
// 1
// a
// Resultado: Failure(java.lang.NumberFormatException: For input string: "a")

Try ejemplos - Procesamiento de valores leídos desde un archivo

Obtener una lista de números enteros

Try ejemplos - Procesamiento de valores leídos desde un archivo

Verificar si existen errores

Try

Utiliza Try si estás trabajando con operaciones que pueden lanzar excepciones.

 

Ejemplo:

  • Parseo de datos de texto a números.
  • Para código inseguro. I/O (archivos, BD), librerías Java o parseo de datos. 

Resumen 

Ejemplos con Option

Ejemplos con Either

Ejemplos con Try

Prácticas y Experimentación

Ejercicio 1

Trate de escribir la función toInt para que trabaje con Try/Succes/Failure

 

Use su nueva función para determinar si es posible utilizar:

getOrElse

foreach

match

flatten

allCatch

Ejercicio 2

Utilice las siguientes expresiones para leer cada una de las líneas de un archivo de texto y representarlo como una lista String

El archivo que debe leer está disponible en el EVA

Considerando que el archivo contiene un número entero en cada línea, intente sumarlos y devolver un resultado.

En el caso de existir excepciones use Try / Succes / Failure

B1S6 Efectos colaterales en programación funcional

By Santiago Quiñones Cuenca

B1S6 Efectos colaterales en programación funcional

Control de efectos colaterales

  • 442