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

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

Posibles valores:

Some - Se genera cuando el valor existe

None - Se genera cuando el valor no existe

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

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")
}

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

Either / Left / Right

Either / Left / Right

Ejemplo

def divideXByY(x: Int, y: Int): Either[String, Int] =
  if (y == 0) Left("No se puede dividir por 0")
  else Right(x / y)

Either / Left / Right

Detalles

Utilizado en lugar de Try (antes de Scala 2.10)

Análogo a Try / Failure / Success

  Try -> Either
  Failure -> Left
  Success -> Right

 

Método devuelve Either

  Right: procesamiento correcto existe un valor
  Left: fallas

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.

 

Ejemplo: Validación donde quieras devolver un mensaje de error detallado.

Recomendación usar:

Try / Succes / Failure

Try

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

¿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")
values.map(toInt)

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. 

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