1. Introducción
Función pura — es una función que con los mismos datos de entrada siempre devuelve el mismo resultado y no provoca ningún efecto secundario. Más fácil: una función pura no "estropea" nada a su alrededor y no se ve afectada desde fuera.
Una función se considera pura si:
- Solo calcula un valor en base a los argumentos de entrada.
- No modifica variables externas ni el estado del programa.
- No depende de variables externas o estados que puedan cambiar.
Dos reglas de oro de la pureza
- Determinismo: la misma entrada — la misma salida.
- Sin efectos secundarios: la función no cambia nada fuera de sí misma: ni archivos, ni variables globales, ni la interfaz de usuario (hola, Console.WriteLine).
No es religión, es sentido común
Parecerá un truco académico, pero la práctica demuestra lo contrario:
- Una función pura es predecible. Es fácil de testear: la llamas con argumentos concretos y obtendrás un resultado concreto.
- Una función pura se puede reordenar en el código sin miedo a que algo se rompa en otro sitio.
- En un mundo multinúcleo, una función pura es garantía de seguridad: se puede ejecutar en paralelo sin miedo a race conditions.
2. Ejemplos de funciones puras e impuras
Función pura: todo predecible
// Función pura: no toca nada fuera de sí misma
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Llama a Add(2, 3) cien veces — siempre devolverá 5. Aburrido, pero fiable.
Función impura: rompe las reglas
// Rompe la pureza: depende de estado externo (variable estática)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
Aquí la llamada Increase() devolverá un valor distinto cada vez — o sea, ya no hay determinismo.
// Rompe la pureza: provoca un efecto externo (imprime en pantalla)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // ¡Efecto secundario!
return sum;
}
¿Y la aleatoriedad y el tiempo?
Cualquier función que use DateTime.Now o Random ya no es pura:
// ¡No pura!
int GetRandomNumber()
{
return new Random().Next();
}
Tabla de diferencias
| Característica | Función pura | Función impura |
|---|---|---|
| Siempre el mismo resultado para los mismos argumentos | Sí | No |
| Efectos secundarios | No | Sí |
| Dependencia del estado externo | No | Sí |
3. Inmutabilidad de datos: teoría y práctica
Inmutabilidad (immutability) — es un enfoque donde un objeto no se puede cambiar después de crearse. Si necesitas un nuevo valor — creas un objeto nuevo.
¿Por qué es importante?
- La aplicación se vuelve resistente a cambios accidentales de datos.
- No hay "fugas" secretas de cambios: si tienes un objeto, nadie lo modificará a escondidas.
- La inmutabilidad es la base para muchas optimizaciones automáticas y cálculos en paralelo.
Ejemplos sencillos en C#
Tipos inmutables en .NET
Las cadenas (string) en C# son inmutables. Cada vez que haces string.Concat(s, "world"), se crea una cadena nueva.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
Arrays y colecciones: mutable por defecto
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // ¡El array cambió!
Inmutabilidad "a grosso modo": compilar código
En lugar de modificar un objeto/valor existente, devuelve uno nuevo:
// En lugar de esto:
void AddToList(List<int> list, int value)
{
list.Add(value); // ¡Mutacion!
}
// Mejor así:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // Nueva lista
return newList;
}
Ilustración: cambiar vs no cambiar
flowchart LR
A[Objeto original] --"mutación"--> B[Mismo objeto, pero diferente por dentro]
A --"inmutabilidad"--> C[Objeto nuevo]
4. ¿Para qué sirve esto en código C# real?
- En el C# moderno, librerías como LINQ, Entity Framework y ASP.NET Core apuestan por funciones puras e inmutabilidad.
- La inmutabilidad reduce la cantidad de bugs "mágicos" cuando alguien sobrescribe un valor importante en algún lado.
- Las funciones puras facilitan el testing automático (unit tests), porque para probar solo necesitas comprobar entrada y salida, sin preocuparte por el mundo exterior.
Ejemplo: trabajar con cadenas
string s = "Hello";
string newS = s.Replace("H", "J"); // s sigue siendo "Hello"; newS — "Jello"
Ejemplo: LINQ y colecciones
Where, Select y otros métodos devuelven colecciones nuevas, sin tocar las antiguas.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers permanece igual!
Ejemplo real: configuración con objetos inmutables
Muchas APIs .NET modernas usan objetos inmutables para configuración, por ejemplo JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// Este objeto no cambia "en caliente", lo que aumenta la fiabilidad.
5. Errores y trampas típicas
El terreno resbaladizo aparece cuando "accidentalmente" mutas datos en código que deberías considerar "puro".
Suele pasar con colecciones: quieres filtrar una lista y de paso modificas la original.
O te olvidas de que un método como List<T>.Add modifica el objeto en sitio.
Ejemplo traicionero:
List<int> DoubleTheNumbers(List<int> xs)
{
// ¡Error! Mutamos la lista original, devolvemos el mismo objeto.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
Este código incluso lanzará una excepción en tiempo de ejecución (InvalidOperationException), porque estamos cambiando la colección mientras la iteramos — problema clásico de la mutación.
Correcto:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION