DEV Community

Cover image for Guía para Iniciarse en la Programación Funcional con C#
Manuel Dávila
Manuel Dávila

Posted on • Edited on

Guía para Iniciarse en la Programación Funcional con C#

¿Alguna vez te has visto lidiando con funciones repetitivas y complicadas en tu código? La Programación Funcional puede ayudarte a simplificar esos problemas. Por ejemplo, las expresiones lambda y las funciones anónimas permiten escribir código más limpio y directo, haciendo que tareas que antes eran engorrosas se vuelvan mucho más manejables.

Este paradigma de programación ofrece una forma diferente de pensar y escribir código, centrándose en funciones puras, inmutabilidad, y minimización de efectos secundarios. En este artículo, exploraremos los fundamentos de la programación funcional en C#, sus beneficios, y cómo puedes empezar a aplicarlos en tus proyectos.

A menudo, la programación funcional se asocia con lenguajes como Haskell o Scala, pero C# también ofrece un soporte robusto para este estilo de programación. Incluso si vienes de un fondo de programación orientada a objetos, estos conceptos pueden enriquecer tu enfoque y mejorar la calidad de tu código.

Gif de un cerebro explotando en el cosmo

¿Qué es la Programación Funcional?

La Programación Funcional es un paradigma de programación que evita cambiar el estado y los datos mutables. A diferencia de la programación orientada a objetos, que se centra en objetos y su interacción, la programación funcional se enfoca en la aplicación de funciones, lo que resulta en un código más predecible y fácil de razonar.

En términos simples, se trata de construir software al componer funciones puras, que son funciones sin efectos secundarios y que siempre producen el mismo resultado dado el mismo conjunto de parámetros. Esto permite crear sistemas más modulares y testables.

Algunos beneficios de la Programación Funcional:

  • Al reducir la complejidad del estado mutable y el comportamiento implícito, el código se vuelve más fácil de leer y mantener.
  • Las funciones puras, al no depender de un estado externo, son más fáciles de probar, ya que siempre producirán el mismo resultado con los mismos parámetros.
  • Al evitar el estado mutable, la programación funcional facilita el desarrollo de aplicaciones concurrentes, donde múltiples procesos pueden ejecutarse simultáneamente sin riesgo de colisión de datos.

Cuando utilizas un paradigma de programación, no estas atado a usar sólo este, puedes hacer una combinación de diferentes paradigmas de ser necesario. C# es un lenguaje orientado a objetos, así que al usar el paradigma Funcional, ya estas combinando estos 2 paradigmas.

Características de la Programación Funcional

Estado compartido 🔗

En la programación funcional, la idea es evitar o minimizar el estado compartido. ¿Qué significa esto? Básicamente, que las funciones no dependen de variables globales que puedan ser cambiadas por otras partes del programa. En vez de andar modificando cosas por todos lados, las funciones toman datos y devuelven nuevos datos, sin cambiar los originales.

Al reducir el estado compartido, se bajan los problemas relacionados con la concurrencia, que es cuando varias cosas intentan cambiar datos al mismo tiempo y causan errores.

Inmutabilidad 🔒

La inmutabilidad es como una regla de oro en la programación funcional. Significa que una vez que se crea un dato, no se cambia. En lugar de modificar un objeto o una variable, lo que hacemos es crear uno nuevo con los cambios que queramos. Aunque al principio pueda parecer menos eficiente, esto hace que el código sea más seguro y predecible.

Por ejemplo, imagina que tienes un carrito de compras y querés agregar un producto, en vez de cambiar el carrito que ya tienes, creas uno nuevo que incluye el producto extra. Así, el carrito original no se toca y evitás problemas si otras partes del código lo están usando.

Efectos Secundarios ⚠️

Los efectos secundarios ocurren cuando una función, además de devolver un valor, cambia algo en el estado del programa o interactúa con el exterior (como escribir en un archivo, actualizar una base de datos o llamadas a una API). En la programación funcional, la idea es minimizar estos efectos para que el código sea más fácil de entender y no cause sorpresas.

Delegados en C#

Antes de entrar en más conceptos de la programación Funcional, hablemos un poco de C# y algunas palabras reservadas que te serviran más adelante.

Un delegado es un tipo que representa una referencia a un método. En C#, los delegados se utilizan para definir tipos de métodos que pueden ser asignados a variables, pasados como argumentos a otros métodos, o utilizados como callbacks

public delegate int Operacion(int x, int y);

public class EjemploDelegado
{
    public static int Sumar(int a, int b) => a + b;

    public static void Main()
    {
        Operacion operacion = Sumar;
        Console.WriteLine(operacion(5, 3)); // 8
    }
}
Enter fullscreen mode Exit fullscreen mode

Func

Func es un delegado genérico que encapsula un método que puede devolver un valor. Se puede usar para representar funciones con hasta 16 parámetros. El último tipo genérico en Func siempre representa el tipo de retorno.

Func<int, int, int> suma = (x, y) => x + y;
Enter fullscreen mode Exit fullscreen mode

Aquí, suma es una instancia de Func que representa una función que toma dos enteros como parámetros y devuelve un entero.

Esto es muy similar a crear un método con un tipo de dato de retorno:

public int suma(int x, int y) 
{ 
    return x + y 
} 
Enter fullscreen mode Exit fullscreen mode

Action

Action es otro delegado genérico, similar a Func, pero se usa para métodos que no devuelven un valor (es decir, devuelven void). Action puede tener hasta 16 parámetros de entrada.

Action<string> imprimir = mensaje => Console.WriteLine(mensaje);
Enter fullscreen mode Exit fullscreen mode

En este caso, imprimir es una instancia de Action que representa un método que toma una cadena como argumento y no devuelve ningún valor.

Esto es muy similar a crear un método de tipo void:

public void imprimir(string mensaje) 
{ 
    Console.WriteLine(mensaje); 
} 
Enter fullscreen mode Exit fullscreen mode

Predicate

Un Predicate es un delegado que encapsula un método que toma un argumento de tipo T y devuelve un bool. Este delegado se usa frecuentemente en métodos de colecciones genéricas, como List, para encontrar o filtrar elementos que cumplan con ciertos criterios.

Predicate<int> esPar = num => num % 2 == 0;
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, esPar es un Predicate que representa una función para determinar si un número es par.

Esto es muy similar a crear un método que retorne un tipo bool:

public bool esPar(int num) {
    return num % 2 == 0;
}
Enter fullscreen mode Exit fullscreen mode

¡Ahora sí! Retomemos los conceptos de la Programación Funcional utilizando los delegados aprendidos.

Funciones Puras

Las funciones puras son aquellas que siempre devuelven el mismo resultado cuando se les da el mismo input y no causan efectos secundarios. Esto significa que no cambian el estado de nada fuera de ellas ni dependen de algo que pueda cambiar fuera de su ámbito.

Imagina que tienes una función que calcula el precio final de un producto aplicando un descuento:

decimal CalcularPrecioFinal(decimal precioBase, decimal descuento)
{
    return precioBase - (precioBase * descuento);
}
Enter fullscreen mode Exit fullscreen mode

Esta función es pura porque siempre devolverá el mismo resultado si se le pasa el mismo precioBase y descuento, y no cambia nada más en el sistema.

Como no existe una lógica compleja, podemos refactorizar utilizando un delegado.

Func<decimal, decimal, decimal> CalcularPrecioFinal = (precioBase, descuento) => precioBase - (precioBase * descuento);
Enter fullscreen mode Exit fullscreen mode

Expresiones Lambda y Funciones Anónimas

Las expresiones lambda y las funciones anónimas son formas de definir funciones rápidas y temporales, sin necesidad de nombrarlas explícitamente. Son muy útiles para operaciones sencillas o para pasar funciones como parámetros.

En el ejemplo anterior "(precioBase, descuento) => precioBase - (precioBase * descuento)" era nuestra expresión lambda.

Probemos con otro ejemplo, supongamos que tienes una lista de productos y quieres obtener sólo los nombres de los productos en oferta:

var nombresProductosEnOferta = productos
    .Where(producto => producto.EsEnOferta)
    .Select(producto => producto.Nombre)
    .ToList();
Enter fullscreen mode Exit fullscreen mode

Aquí, producto => producto.EsEnOferta y producto => producto.Nombre son expresiones lambda que se usan para filtrar y proyectar los productos de la lista.

Composicion de Funciones

La composición de funciones es la práctica de combinar funciones pequeñas y simples para construir funciones más complejas. En programación funcional, es común crear funciones reutilizables que se pueden combinar de diversas maneras.

Puedes tener funciones separadas para calcular impuestos y descuentos, y luego componerlas para obtener el precio final:

Func<decimal, decimal> aplicarImpuesto = precio => precio * 1.18m;
Func<decimal, decimal> aplicarDescuento = precio => precio * 0.90m;

Func<decimal, decimal> calcularPrecioFinal = precio => aplicarDescuento(aplicarImpuesto(precio));
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, calcularPrecioFinal es una función compuesta que aplica primero el impuesto y luego el descuento al precio de un producto.

Funciones de Orden Superior

Las funciones de orden superior son aquellas que pueden tomar otras funciones como argumentos o devolverlas como resultados. Esto es útil para crear funciones más generales y reutilizables.

Si quieres filtrar productos según diferentes criterios (por ejemplo, productos en oferta o productos baratos), puedes usar una función de orden superior:

List<Product> FiltrarProductos(List<Product> productos, Predicate<Product> criterio)
{
    return productos.FindAll(criterio);
}

var productosEnOferta = FiltrarProductos(productos, producto => producto.EsEnOferta);
var productosEconomicos = FiltrarProductos(productos, producto => producto.Precio < 50.0m);
Enter fullscreen mode Exit fullscreen mode

En este caso, FiltrarProductos es una función de orden superior que recibe un predicado (criterio) para filtrar productos.

Usar un enfoque funcional puede mejorar la mantenibilidad y escalabilidad de tus proyectos. Esto es especialmente útil en sistemas grandes o de larga vida, donde el código limpio y modular es crucial.

Si eres nuevo en la programación funcional, comienza adoptando pequeñas prácticas en tus proyectos actuales. Utiliza expresiones lambda, funciones puras y trata de minimizar el estado mutable. Experimenta con delegados como Func, Action y Predicate para familiarizarte con su uso.

Además, considera el uso de herramientas y bibliotecas que faciliten la programación funcional en C#, como LINQ, que permite realizar consultas y manipulaciones de datos de manera funcional.

La programación funcional puede parecer un cambio radical si vienes de un fondo orientado a objetos, pero los beneficios en términos de claridad, mantenibilidad y testabilidad del código valen la pena. Espero que este contenido te haya sido de utilidad y te brinde un acercamiento a la programación funcional con C#.

!Happy coding! ✨

Top comments (0)