CSharp

Tutorial CSharp: Fundamentos de la programación funcional

Aprende los conceptos clave de programación funcional en C#, incluyendo inmutabilidad, funciones puras y orden superior para mejorar tu código.

Aprende CSharp y certifícate

Conceptos introductorios de programación funcional

La programación funcional representa un paradigma de programación fundamentalmente diferente al enfoque imperativo tradicional que probablemente ya conoces. Mientras que en la programación imperativa nos centramos en cambiar el estado del programa mediante una secuencia de instrucciones, la programación funcional se basa en la evaluación de expresiones y la aplicación de funciones matemáticas.

En su esencia, la programación funcional trata el cómputo como la evaluación de funciones matemáticas y evita los cambios de estado y datos mutables. Este enfoque tiene sus raíces en el cálculo lambda, un sistema formal desarrollado en los años 1930 por el matemático Alonzo Church.

Principios fundamentales

La programación funcional se sustenta en varios principios clave que la distinguen de otros paradigmas:

  • Funciones como ciudadanos de primera clase: Las funciones son tratadas como cualquier otro tipo de dato. Pueden asignarse a variables, pasarse como argumentos y devolverse como resultados de otras funciones.

  • Declarativo vs imperativo: En lugar de describir paso a paso cómo realizar una tarea (enfoque imperativo), el código funcional describe qué resultado se desea obtener (enfoque declarativo).

  • Transparencia referencial: Una expresión siempre evalúa al mismo resultado en cualquier punto del programa. Esto facilita el razonamiento sobre el código y su comportamiento.

  • Recursión sobre iteración: En lugar de utilizar bucles para repetir operaciones, la programación funcional favorece la recursión como mecanismo principal de iteración.

Programación funcional en C#

Aunque C# nació como un lenguaje orientado a objetos, ha ido incorporando progresivamente características funcionales a lo largo de sus versiones. Hoy en día, C# permite un estilo híbrido que combina programación orientada a objetos con técnicas funcionales.

Algunas características de C# que facilitan la programación funcional incluyen:

  • Expresiones lambda: Permiten definir funciones anónimas de forma concisa.
// Una expresión lambda que calcula el cuadrado de un número
Func<int, int> cuadrado = x => x * x;
  • Tipos de delegados: Como Func<> y Action<>, que permiten tratar las funciones como datos.
// Func<TInput, TOutput> representa una función que toma un tipo y devuelve otro
Func<string, int> obtenerLongitud = s => s.Length;
  • LINQ (Language Integrated Query): Una biblioteca que implementa muchos patrones funcionales para trabajar con colecciones.
// Filtrar una lista de forma declarativa
var numerosPares = numeros.Where(n => n % 2 == 0);

Beneficios de la programación funcional

Adoptar técnicas de programación funcional en C# puede proporcionar varios beneficios:

  • Código más conciso: Las operaciones complejas pueden expresarse de manera más compacta.
  • Menos errores: Al reducir los efectos secundarios y la mutación de estado, se eliminan categorías enteras de bugs.
  • Mejor paralelización: El código sin estado compartido es más fácil de ejecutar en paralelo.
  • Facilidad de prueba: Las funciones puras son más sencillas de probar, ya que su comportamiento depende únicamente de sus entradas.
  • Razonamiento más sencillo: El código se vuelve más predecible y más fácil de entender.

Pensamiento funcional

Para empezar a pensar de manera funcional, es útil considerar estos conceptos:

  • Composición de funciones: Combinar funciones simples para crear funciones más complejas.
  • Transformación de datos: Ver el programa como una serie de transformaciones de datos, donde cada paso toma una entrada y produce una salida.
  • Evitar efectos secundarios: Diseñar funciones que no modifiquen variables externas ni el estado del sistema.

La transición al pensamiento funcional puede requerir tiempo, especialmente si estás acostumbrado a la programación imperativa. Sin embargo, incluso adoptar algunos conceptos funcionales puede mejorar significativamente la calidad de tu código C#.

En las siguientes secciones, exploraremos más a fondo conceptos específicos como la inmutabilidad y las funciones de orden superior, que son pilares fundamentales de la programación funcional y que C# implementa de formas particulares.

Inmutabilidad y efectos

La inmutabilidad constituye uno de los pilares fundamentales de la programación funcional. Este concepto se refiere a la creación de objetos o valores que, una vez creados, no pueden ser modificados. En lugar de alterar datos existentes, las operaciones sobre valores inmutables generan nuevas instancias con los cambios aplicados.

Inmutabilidad en C#

Aunque C# no impone la inmutabilidad por defecto como otros lenguajes funcionales puros, ofrece varias formas de implementarla:

  • Tipos de valor inmutables: Los tipos como int, DateTime o string son inherentemente inmutables en C#.
string nombre = "Ana";
string nombreCompleto = nombre + " García"; // Crea un nuevo string, no modifica el original
Console.WriteLine(nombre); // Sigue mostrando "Ana"
  • Clases inmutables personalizadas: Podemos diseñar nuestras propias clases inmutables siguiendo algunas pautas:
public class Punto
{
    // Propiedades de solo lectura
    public int X { get; }
    public int Y { get; }
    
    // Constructor que inicializa los valores
    public Punto(int x, int y)
    {
        X = x;
        Y = y;
    }
    
    // Método que devuelve un nuevo objeto en lugar de modificar el actual
    public Punto Mover(int deltaX, int deltaY)
    {
        return new Punto(X + deltaX, Y + deltaY);
    }
}
  • Modificador readonly: Para campos que no deben cambiar después de la inicialización.
public class Configuracion
{
    public readonly string RutaArchivo;
    
    public Configuracion(string rutaArchivo)
    {
        RutaArchivo = rutaArchivo;
    }
}
  • Colecciones inmutables: .NET proporciona el espacio de nombres System.Collections.Immutable con versiones inmutables de las colecciones estándar.
using System.Collections.Immutable;

// Crear una lista inmutable
ImmutableList<int> numeros = ImmutableList.Create<int>(1, 2, 3);

// Añadir un elemento (devuelve una nueva lista)
ImmutableList<int> numerosAmpliados = numeros.Add(4);

// La lista original permanece intacta
Console.WriteLine(string.Join(", ", numeros)); // Muestra: 1, 2, 3

Beneficios de la inmutabilidad

La adopción de estructuras de datos inmutables ofrece ventajas significativas:

  • Seguridad en concurrencia: Los objetos inmutables son intrínsecamente seguros para su uso en entornos multihilo, ya que no pueden cambiar una vez creados.
  • Razonamiento simplificado: Es más fácil razonar sobre código que no cambia el estado.
  • Historial de cambios: Cada modificación genera una nueva versión, facilitando la implementación de funcionalidades como "deshacer".
  • Caching y memoización: Los resultados de funciones con parámetros inmutables pueden almacenarse en caché con seguridad.

Efectos secundarios

Un efecto secundario ocurre cuando una función modifica algún estado fuera de su ámbito local o tiene una interacción observable con el mundo exterior más allá de devolver un valor. Algunos ejemplos incluyen:

  • Modificar una variable global
  • Modificar un parámetro recibido por referencia
  • Escribir en un archivo o base de datos
  • Mostrar datos en pantalla
  • Lanzar una excepción

En programación funcional pura, se busca minimizar o aislar los efectos secundarios. Esto no significa eliminarlos por completo (lo cual sería imposible para programas útiles), sino organizarlos de manera controlada.

Funciones puras vs impuras

Una función pura es aquella que:

  1. Siempre produce el mismo resultado para los mismos argumentos
  2. No causa efectos secundarios
// Función pura
public int Sumar(int a, int b)
{
    return a + b;
}

Por otro lado, una función impura tiene efectos secundarios o depende del estado externo:

// Función impura (depende de estado externo)
private int _contador = 0;
public int ObtenerSiguiente()
{
    return _contador++;  // Modifica estado y depende de él
}

Gestión de efectos en C#

En C#, podemos adoptar estrategias para gestionar los efectos secundarios:

  • Separación de responsabilidades: Dividir el código en componentes puros (lógica) e impuros (E/S, UI).
// Componente puro (lógica de negocio)
public decimal CalcularImpuesto(decimal monto, decimal tasa)
{
    return monto * (tasa / 100);
}

// Componente impuro (interacción con usuario)
public void MostrarImpuesto(decimal monto, decimal tasa)
{
    decimal impuesto = CalcularImpuesto(monto, tasa);
    Console.WriteLine($"El impuesto es: {impuesto:C}");
}
  • Inyección de dependencias: Pasar servicios que encapsulan efectos secundarios en lugar de realizarlos directamente.
public class Procesador
{
    private readonly ILogger _logger;
    
    public Procesador(ILogger logger)
    {
        _logger = logger;
    }
    
    public void Procesar(string datos)
    {
        // Lógica pura
        string resultado = datos.ToUpper();
        
        // Efecto secundario aislado y explícito
        _logger.Log($"Datos procesados: {resultado}");
    }
}

Inmutabilidad y rendimiento

Una preocupación común sobre la inmutabilidad es su impacto en el rendimiento, ya que crear nuevos objetos constantemente podría parecer ineficiente. Sin embargo:

  • Los objetos inmutables pequeños son eficientes en el recolector de basura.
  • Las implementaciones modernas de colecciones inmutables utilizan técnicas como estructuras de datos persistentes que comparten partes no modificadas.
  • El compilador y el runtime pueden realizar optimizaciones específicas para código inmutable.
// Las colecciones inmutables en .NET usan estructuras de datos eficientes
ImmutableList<int> lista = ImmutableList<int>.Empty;
for (int i = 0; i < 1000; i++)
{
    // Cada operación crea una nueva lista, pero internamente
    // se reutilizan muchas estructuras
    lista = lista.Add(i);
}

Aplicación práctica

La combinación de inmutabilidad y control de efectos secundarios nos permite escribir código más predecible y mantenible. Un enfoque práctico es:

  • Usar tipos inmutables para representar el estado del dominio
  • Implementar operaciones como transformaciones que devuelven nuevos estados
  • Aislar los efectos secundarios en capas específicas de la aplicación

Este enfoque es especialmente valioso en escenarios como:

  • Aplicaciones concurrentes o distribuidas
  • Sistemas con requisitos de alta confiabilidad
  • Implementaciones de patrones como Event Sourcing o CQRS

Funciones de orden superior

Las funciones de orden superior representan uno de los conceptos más potentes de la programación funcional. Se trata de funciones que pueden recibir otras funciones como parámetros o devolver funciones como resultado. Este concepto eleva las funciones al estatus de "ciudadanos de primera clase", permitiéndoles ser tratadas como cualquier otro valor en el lenguaje.

En C#, las funciones de orden superior se implementan principalmente mediante delegados, expresiones lambda y los tipos genéricos Func<> y Action<>. Estas herramientas nos permiten escribir código más flexible, modular y expresivo.

Delegados como base para funciones de orden superior

Los delegados en C# son tipos que representan referencias a métodos con una firma particular. Constituyen el fundamento sobre el que se construyen las funciones de orden superior:

// Definición de un delegado
public delegate int Transformador(int x);

// Método que recibe un delegado
public static List<int> TransformarLista(List<int> numeros, Transformador funcion)
{
    var resultado = new List<int>();
    foreach (var numero in numeros)
    {
        resultado.Add(funcion(numero));
    }
    return resultado;
}

// Uso
public static int Duplicar(int x) => x * 2;
var listaOriginal = new List<int> { 1, 2, 3, 4 };
var listaDuplicada = TransformarLista(listaOriginal, Duplicar);
// listaDuplicada contiene { 2, 4, 6, 8 }

Tipos Func y Action

C# proporciona tipos genéricos predefinidos que simplifican el trabajo con funciones como parámetros:

  • Func<T1, T2, ..., TResult>: Representa una función que acepta hasta 16 parámetros y devuelve un valor.
  • Action<T1, T2, ...>: Representa una función que acepta hasta 16 parámetros y no devuelve ningún valor.
// Usando Func en lugar de un delegado personalizado
public static List<T> TransformarLista<T, U>(List<U> items, Func<U, T> transformador)
{
    var resultado = new List<T>();
    foreach (var item in items)
    {
        resultado.Add(transformador(item));
    }
    return resultado;
}

// Uso con lambda
var nombres = new List<string> { "ana", "juan", "elena" };
var longitudes = TransformarLista<int, string>(nombres, s => s.Length);
// longitudes contiene { 3, 4, 5 }

Funciones que devuelven funciones

Otra característica poderosa es la capacidad de crear funciones que devuelven otras funciones, lo que permite técnicas avanzadas como la aplicación parcial y la currificación:

// Función que devuelve otra función
public static Func<int, int> CrearMultiplicador(int factor)
{
    return x => x * factor;
}

// Uso
var duplicador = CrearMultiplicador(2);
var triplicador = CrearMultiplicador(3);

Console.WriteLine(duplicador(5));  // Muestra: 10
Console.WriteLine(triplicador(5)); // Muestra: 15

Patrones comunes con funciones de orden superior

Las funciones de orden superior facilitan la implementación de varios patrones útiles:

  • Transformación (Map): Aplicar una función a cada elemento de una colección.
// Implementación manual de Map
public static IEnumerable<TResult> Map<T, TResult>(
    this IEnumerable<T> source, 
    Func<T, TResult> selector)
{
    foreach (var item in source)
    {
        yield return selector(item);
    }
}

// En C# moderno usaríamos LINQ
var numeros = new[] { 1, 2, 3, 4 };
var cuadrados = numeros.Select(x => x * x);
  • Filtrado (Filter): Seleccionar elementos que cumplen un predicado.
// En C# moderno con LINQ
var numeros = new[] { 1, 2, 3, 4, 5, 6 };
var pares = numeros.Where(x => x % 2 == 0);
  • Reducción (Reduce/Fold): Combinar elementos de una colección.
// En C# moderno con LINQ
var numeros = new[] { 1, 2, 3, 4 };
var suma = numeros.Aggregate(0, (acumulador, numero) => acumulador + numero);
  • Composición de funciones: Combinar funciones para crear nuevas funciones.
public static Func<T, V> Componer<T, U, V>(Func<U, V> f, Func<T, U> g)
{
    return x => f(g(x));
}

// Uso
Func<int, int> duplicar = x => x * 2;
Func<int, string> convertir = x => x.ToString();
var duplicarYConvertir = Componer(convertir, duplicar);

Console.WriteLine(duplicarYConvertir(5)); // Muestra: "10"

Ventajas prácticas

El uso de funciones de orden superior en C# ofrece beneficios tangibles:

  • Abstracción de patrones: Permite extraer patrones comunes de código y reutilizarlos.
  • Separación de preocupaciones: Facilita separar qué hacer (lógica de negocio) de cómo hacerlo (implementación).
  • Código más declarativo: El código expresa qué debe lograrse en lugar de los pasos detallados.
  • Flexibilidad: Permite cambiar comportamientos en tiempo de ejecución.

Funciones de orden superior en la biblioteca estándar

C# y .NET incluyen numerosas funciones de orden superior en su biblioteca estándar:

  • LINQ: Prácticamente todos los métodos de LINQ son funciones de orden superior.
var personas = new List<Persona>();
// Where, OrderBy y Select son funciones de orden superior
var resultado = personas
    .Where(p => p.Edad > 18)
    .OrderBy(p => p.Apellido)
    .Select(p => new { p.NombreCompleto, p.Edad });
  • Parallel LINQ (PLINQ): Permite paralelizar operaciones manteniendo la misma interfaz.
// AsParallel convierte la secuencia para procesamiento paralelo
var resultado = coleccionGrande
    .AsParallel()
    .Where(predicado)
    .Select(transformacion);
  • Task API: Para programación asíncrona.
// ContinueWith recibe una función que se ejecutará cuando la tarea termine
Task<int> tarea = ObtenerDatoAsync();
Task<string> continuacion = tarea.ContinueWith(t => t.Result.ToString());

Las funciones de orden superior constituyen una herramienta fundamental para escribir código más expresivo y modular en C#. Aunque el lenguaje no fue diseñado originalmente con la programación funcional en mente, la incorporación de estas características ha enriquecido significativamente su capacidad para expresar soluciones elegantes a problemas complejos.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

Ejercicios de esta lección Fundamentos de la programación funcional

Evalúa tus conocimientos de esta lección Fundamentos de la programación funcional con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender los principios básicos de la programación funcional y su diferencia con el paradigma imperativo.
  • Identificar y aplicar el concepto de inmutabilidad en C# para evitar efectos secundarios.
  • Distinguir entre funciones puras e impuras y gestionar los efectos secundarios en el código.
  • Utilizar funciones de orden superior mediante delegados, expresiones lambda y tipos Func<> y Action<>.
  • Reconocer los beneficios prácticos de la programación funcional para escribir código más conciso, seguro y mantenible.