Efectos secundarios y idempotencia en programación


Efectos secundarios y idempotencia en programación

Fotografía de Nathan Dias en Unsplash


Para comprender patrones de diseño básicos como Command Query Separation CQS o la base teórica detrás de la programación funcional es necesario saber qué son los efectos secundarios en programación y cuándo se producen. En inglés se conocen como side effects.

En el post anterior definí lo que era el estado de la aplicación, te recomiendo tenerlo presente para comprender mejor este texto. 

1. Qué es un efecto secundario

Un método produce un efecto secundario si modifica el estado de la aplicación.

Se llama efecto secundario porque produce una salida extra observable además de devolver un valor, el efecto principal, al invocador del método.

Vamos a ver tres situaciones donde pueden ocurrir dichos efectos secundarios:

  1. Modificar el estado de una clase, es decir, modificar una variable privada o una propiedad de la misma clase. Las variables internas del método no cuentan.
    Por ejemplo, en el método "Avanzar" modificamos el valor de "_posicion":
     
    public class Vehiculo
    {
      private int _posicion;
       
      public Vehiculo(int posicion)
      {
      _posicion = posicion;
      }
    
     public void Avanzar(int metros)
     {
       _posicion = _posicion + metros; // modifica el estado de una clase
     }
    }
  2. Modificar el estado global, es decir, modificar una variable o propiedad estática (static).
    Por ejemplo, en una clase modificamos la propiedad global "RutaCarpetaInformes":
     
    public static class ClaseConVariablesGlobales
    {
       public static string RutaCarpetaInformes { get; set; }
    }
    
    public class ClaseParaConfigurar
    {
       public void ConfigurarCarpetaInforme(string ruta)
       {
          ClaseConVariablesGlobales.RutaCarpetaInformes = ruta; // modifica el estado global
        }
    }
  3. Modificar el estado externo, por ejemplo, guardar datos en disco.
    Por ejemplo, al escribir un texto en un fichero:
     
    class ClaseParaEscribirEnArchivo
    {
      public void EscribirTextoEnArchivo(string texto)
      {
         File.WriteAllText("Archivo.txt", texto); // modifica el estado externo
       }
    }

2. Lanzar una excepción no produce un efecto secundario

Aunque pueda parecerlo, las excepciones no generan efectos secundarios.

Primero recordamos qué dos tipos de excepciones podemos obtener:

  • Las excepciones deterministas son aquellas que se producen siempre que reciben ciertos valores. Por ejemplo, si a un método que realiza una división le pasamos un 0 cómo parámetro de entrada, siempre va a lanzar la misma excepción (DivideByZeroException).
     
  • Las excepciones no deterministas son aquellas que ocurren de manera excepcional y que no siempre se producen aunque los inputs sean los mismos. Por ejemplo, un método que requiera realizar muchos cálculos se podría quedar sin memoria antes de conseguir devolver el resultado final (OutOfMemoryException), pero ejecutado en una máquina más potente no lanzaría la excepción.

Ambos casos representan el flujo por el cual se ha ido ejecutando el código, pero en ninguno de ellos se modifica un valor. Solo si nosotros modificamos una variable dentro de un bloque "Catch" sí que se produciría un efecto secundario. Pero eso ya no vendría dado por la excepción, sino por nuestra intervención.

Por tanto, las excepciones no producen efectos secundarios porque no modifican el estado de la aplicación. 

3. Capturar una excepción no produce un efecto secundario

Otro caso que genera dudas es capturar la excepción con un try catch y tomar alguna decisión en base a ella.

Imagina el siguiente código:

public decimal RealizarCalculo(int input)
{
  try
  {
   // Código del cálculo
   // ...
   return resultado;
  }
  catch (OutOfMemoryException)
  {
   //Te has quedado sin memoria: vuelve a intentarlo;
   return -1;
  }
  catch (Exception e)
  {
   Console.WriteLine(e);
   throw;
   }
}

Aunque intuitivamente no lo parezca, este ejemplo tampoco produce un efecto secundario. Lo que realmente está pasando es que no podemos predecir el resultado, no siempre va a ser el mismo, pero no modifica ni propiedades ni campos, no cambia una variable global y tampoco escribe en disco. 

Semánticamente hablando es equivalente a una función que devuelve valores aleatorios:

 public int NumeroAleatorio()
 {
   Random random = new Random();
   return random.Next();
 }

Este método no es puro (definición que veremos más adelante) pero no porque no produzca un efecto secundario, sino por inclumplir otra precondición de la definición de función pura: no es predecible.

4. Trabajar con DateTime.Now / DateTime.Today no produce efectos secundarios

Otro caso que causa mucha confusión es si el uso de DateTime.Now / DateTime.Today produce efectos secundarios. La respuesta es no, pero es imposible predecir qué valor va a devolver porque depende de factores externos a dicha función. Está ligado a un valor físico externo como es el reloj del sistema operativo.

Su uso es equivalente a una función que devuelva valores aleatorios.

5. Qué es la idempotencia en programación

El término idempotente se usa para describir una operación que produce los mismos resultados ya se ejecute una o varias veces.

Por ejemplo un método que elimina un elemento de un conjunto siempre produce el mismo resultado. Puedes llamar al mismo método tantas veces como quieras que no va a ocurrir nada nuevo.

EliminarUsuario(int usuarioId);
EliminarUsuario(int usuarioId);
EliminarUsuario(int usuarioId);

Que un método sea idempotente no significa que no genere efectos secundarios.

En este ejemplo, la primera vez que se llama al método se produce el efecto secundario de eliminar al usuario, pero las siguientes veces que se llame, el usuario ya estará eliminado y el sistema ya no se verá afectado.

Como no se puede saber si el usuario existe de antemano debemos considerar que sí se produce un efecto secundario.

6. No confundir efecto secundario con función pura

A menudo se confunde entre la definición de efecto secundario y función pura

Una función pura es aquella que cumple dos características muy concretas:

  1. Es idempotente (mismo resultado si se ejecuta una o varias veces)
  2. No produce efectos secundarios

Por tanto, todas las funciones puras están libres de efectos secundarios, pero no todas las funciones libres de efectos secundarios son funciones puras. 

7. EJEMPLOS

Vamos a ver un mismo ejemplo con pequeñas variaciones que producen comportamientos diferentes:

// Método sin efectos secundarios pero no predecible
public int CalcularEdad(DateTime fechaNacimiento)
{
   var hoy = DateTime.Today;  // el uso del DateTime.Today genera códio no predecible

   var edad = hoy.Year - fechaNacimiento.Year;

   if (fechaNacimiento.DayOfYear > hoy.DayOfYear)
   {
     edad--;
   }

    return edad;
}


// Pasando el día por parámetro convertimos la función anterior en una función pura. 
// Sin efectos secundarios e idempotente. 
public int CalcularEdadEnDia(DateTime fechaNacimiento, DateTime dia) 
{
    var edad = dia.Year - fechaNacimiento.Year;

    if (fechaNacimiento.DayOfYear > dia.DayOfYear)
    {
      edad--;
    }

    return edad;
}


// Método idempotente pero con efectos secundarios
private int _edad;
public int CalcularEdadEnDiaYGuardar(DateTime fechaNacimiento, DateTime dia)
{
   var edad = dia.Year - fechaNacimiento.Year;

    if (fechaNacimiento.DayOfYear > dia.DayOfYear)
    {
     edad--;
     }

    _edad = edad;  // cambio del estado de la aplicación
    return edad;
}

Espero que este artículo te haya provocado un efecto secundario útil. :)




Quizá algun día empiece a enviar una newsletter, si te gustaría recibirla subscríbete aquí

Archivo