Saltar al contenido
Volver

Programación Funcional: Conceptos útiles

Publicado:

Programación Funcional: Conceptos útiles

La Programación Funcional (FP por sus siglas en inglés) suele mencionarse como una disciplina reservada para matemáticos o amantes de los lenguajes funcionales. Pero en realidad, la FP tiene conceptos fundamentales que pueden beneficiarte en tu trabajo diario. Adoptar estos conceptos puede mejorar la legibilidad, mantenibilidad y calidad de tu código.

En este post, exploraremos cómo aprovechar algunos conceptos de la Programación Funcional.

¿Qué es la Programación Funcional?

La programación funcional es un paradigma de programación declarativa basado en el uso de verdaderas funciones matemáticas – Wikipedia

La Programación Funcional es una forma de escribir código utilizando funciones que se comportan como en matemáticas: la misma entrada siempre produce la misma salida.

En programación, el estado es la información que un programa conserva y puede cambiar con el tiempo. La FP evita cambiar este estado directamente, lo que ayuda a prevenir errores. Manteniendo los datos inmutables y las funciones predecibles, la FP hace que los programas sean más fáciles de leer, más confiables y más escalables.

Esto puede sonar abstracto, así que pasemos a algunos conceptos clave.

Conceptos Fundamentales de la Programación Funcional

Funciones Puras

Cuando una función es pura significa que cumple con los siguientes aspectos:

Ejemplo de una función pura:

public int square(int number) {
    return number * number;
}

Por a su naturaleza, las funciones puras tienen los siguientes beneficios:

Funciones de Primera Clase y de Orden Superior

Las funciones de primera clase son aquellas que pueden tratarse como variables. Es posible almacenarlas, pasarlas como argumento a otra función y devolverlas desde una función.

Las funciones de orden superior son aquellas que pueden recibir otras funciones como argumento y/o devolver una función como resultado. Esto solo es posible en lenguajes que soportan funciones de primera clase.

La combinación de estos tipos de funciones permite implementar patrones potentes como callbacks, strategy patterns y flujos de lógica personalizados. Además, ayudan a reducir el código repetitivo (boilerplate) y a mejorar la flexibilidad del sistema.

public static List<String> processNumbers(
    List<Integer> input,
    Predicate<Integer> filter, // Function that takes a value and returns a boolean
    Function<Integer, String> transformer // Function that takes an Integer and returns a String
) {
    return input.stream()
        .filter(filter)
        .map(transformer)
        .toList();
}

public static void main(String[] args) {
    List<Integer> numbers = List.of(5, 12, 20, 7, 30);
    List<String> result = processNumbers(numbers,
        n -> n > 10,
        n -> "Value: " + n);
    result.forEach(System.out::println);
}
/*
Result:
Value: 12
Value: 20
Value: 30
*/

NOTA: En Java; Predicate, Function y similares permiten programación de estilo funcional, pero no son funciones como en los lenguajes funcionales: son objetos que implementan interfaces funcionales. Aunque Java soporta lambdas y composición de funciones, estas construcciones siguen basándose en objetos, no en funciones de primera clase en el sentido tradicional.

Inmutabilidad

La inmutabilidad se refiere al principio de que, una vez creado un objeto o estructura de datos, este no puede modificarse.

Si necesitamos modificar un valor o estructura, debemos crear una nueva instancia con los cambios deseados. Esta práctica evita errores relacionados con estados mutables.

Es una gran ventaja en sistemas concurrentes porque ninguna parte del código puede cambiar inesperadamente, lo que también facilita la depuración.

public record User(String name, int age) {
    // Method to create a new User with updated age
    public User withAge(int newAge) {
        return new User(this.name, newAge);
    }

    // Method to create a new User with updated name
    public User withName(String newName) {
        return new User(newName, this.age);
    }
}

// Main.java
List<User> users = Arrays.asList(
    new User("Alice", 30),
    new User("Bob", 25),
    new User("Charlie", 35)
);

// Function to increment age
UnaryOperator<User> incrementAge = user -> user.withAge(user.age() + 1);

// Predicate to filter users over 30
Predicate<User> isOver30 = user -> user.age() > 30;

// Apply transformations without modifying states
List<User> updatedUsers = users.stream()
    .map(incrementAge)   // Create new updated User instances
    .filter(isOver30)    // Filter based on age
    .toList();

updatedUsers.forEach(System.out::println); // Result -> [Alice, Charlie]

Composición de Funciones

Funciones simples pueden combinarse para construir comportamientos más complejos. Esto fomenta la reutilización y el diseño modular. La lógica se convierte en un conjunto de pequeñas y testeables funciones encadenadas unas con otras.

Veamos un ejemplo simple de combinación de funciones:

IntUnaryOperator addOne = x -> x + 1;
IntUnaryOperator triple = x -> x * 3;
IntUnaryOperator addOneThenTriple = addOne.andThen(triple);
addOneThenTriple.applyAsInt(5); // (5 + 1) * 3 = 18

Beneficios de la Programación Funcional

Más Fácil de Testear y Depurar

Gracias a las funciones puras, es más fácil realizar pruebas unitarias: solo entradas y salidas esperadas. Esto también hace más simple la depuración, ya que la lógica está aislada.

Mejor Reutilización y Composición

Dividir la lógica en funciones pequeñas que se pueden componer se alinea con principios como SRP (Principio de Responsabilidad Única) y favorece el DRY (Don’t Repeat Yourself).

Además, la FP facilita la extensibilidad del código al fomentar añadir nuevas funciones en lugar de modificar las existentes. Esto se alinea con el Principio Abierto/Cerrado: abierto a la extensión, cerrado a la modificación.

Concurrencia Segura

Los datos inmutables hacen que el código sea naturalmente seguro para hilos. Esto elimina condiciones de carrera y reduce la necesidad de bloqueos en sistemas concurrentes.

Mejor Mantenibilidad del Código

Hacer que las funciones sean consistentes y las estructuras inmutables facilita rastrear errores. El sistema se vuelve más modular y mantenible con el tiempo.

Código Más Limpio gracias a la Programación Declarativa

La FP fomenta la programación declarativa sobre la imperativa. Esto cambia la mentalidad de “cómo hacerlo” a “qué quiero hacer”.

Veamos un ejemplo de ambos paradigmas. El ejemplo consiste en calcular el cuadrado de los números pares de una lista.

// Imperative Programming
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result = new ArrayList<>();

for (int n : numbers) {
    if (n % 2 == 0) {
        result.add(n * n);
    }
}
System.out.println(result); // [4, 16, 36]

La programación imperativa especifica paso a paso qué hacer: iterar, comprobar si es par, multiplicar y guardar.

// Declarative Programming
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .toList();
System.out.println(result); // [4, 16, 36]

La programación declarativa describe lo que queremos hacer: filtrar pares, elevar al cuadrado y devolver la lista.

Aplicando Conceptos de FP en Java

NOTA: Java no es un lenguaje funcional porque fue diseñado principalmente para la programación orientada a objetos. Sin embargo, desde Java 8, soporta características funcionales como lambdas, streams e interfaces funcionales.

Usar Streams para Transformación de Datos

Los Streams en Java permiten operaciones como map, filter y reduce en estilo funcional. Esto fomenta código declarativo y evita mutar estructuras.

List<String> names = List.of("Bob", "Ben", "Alice");
List<String> uppercased =
    names.stream()
         .map(String::toUpperCase)
         .toList();
System.out.println(uppercased); // [BOB, BEN, ALICE]

Usar Optionals y Evitar Null

La FP promueve evitar null, ya que puede causar comportamientos impredecibles. Java Optional fomenta un manejo seguro de posibles valores nulos.

Optional<String> maybeName = Optional.of("Alice");
maybeName.ifPresent(name -> System.out.println(name.toUpperCase()));

Usar Estructuras de Datos Inmutables

Java no fuerza la inmutabilidad por defecto, pero es posible usar variables final y colecciones inmutables, como List.of(…). También se pueden usar records para definir clases inmutables.

// Use immutable collections in Java
List<String> names = List.of("Alice", "Bob");
List<String> newNames = Stream
    .concat(
        names.stream(),
        Stream.of("Charlie"))
    .toList(); // returns newNames = ["Alice", "Bob", "Charlie"]

Usar Funciones

Cuando sea posible, define funciones puras y pequeñas. Evita modificar estados globales. Usa interfaces funcionales (como Function, Predicate, Supplier…).

Function<Integer, Integer> doubleIt = x -> x * 2;
doubleIt.apply(10); // 20

Aplicar Composición de Funciones

Encadena comportamientos para crear lógica más compleja.

Function<Integer, Integer> incrementByOne = x -> x + 1;
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> incrementThenSquare = incrementByOne.andThen(square);

incrementThenSquare.apply(3); // (3 + 1)^2 = 16

Conclusión

Agregar principios de Programación Funcional como funciones puras, inmutabilidad y composición de funciones puede mejorar la claridad, testabilidad y mantenibilidad del código.

Aunque Java no fue diseñado como un lenguaje funcional, ha incorporado características que permiten aplicar estos conceptos.

Gracias a las expresiones lambda, Streams, interfaces funcionales, etc., es posible escribir aplicaciones más declarativas, robustas y modulares que ayudan a crear mejor software.


Siguiente publicación
GitTricks: How to keep your history clean