La programación funcional representa un cambio radical en la forma en que concebimos la escritura de código, especialmente si venimos de un paradigma imperativo. En la programación imperativa, estamos acostumbrados a pensar en términos de algoritmos que manipulan un estado mutable. Por ejemplo, en lenguajes como C, Java o C++, nuestro programa se compone de variables que almacenan datos y de instrucciones que modifican esos datos a lo largo del tiempo. Así, el programa es una serie de transformaciones sobre un estado inicial que, tras varias modificaciones, nos lleva a un estado final del que extraemos el resultado deseado.
Este enfoque implica que constantemente estamos actualizando variables, usando asignaciones y bucles para controlar el flujo y el estado del programa. Por ejemplo, para calcular el factorial de un número en un lenguaje imperativo, normalmente usaríamos un bucle que actualiza un acumulador en cada iteración, multiplicándolo por el siguiente número en la secuencia. Este método es muy explícito en cuanto al cómo se realiza el cálculo, detallando paso a paso las operaciones necesarias.
Sin embargo, la programación funcional nos invita a cambiar la perspectiva. En lugar de centrarnos en el cómo hacer las cosas, nos enfocamos en el qué queremos obtener. No manipulamos estados ni variables mutables, sino que definimos funciones y expresiones que describen las reglas del sistema. El programa se convierte en un conjunto de definiciones matemáticas o computacionales que, dadas unas entradas, producen un resultado sin alterar un estado interno.
Un ejemplo clásico para ilustrar esta diferencia es la definición del factorial. Matemáticamente, el factorial de un número n se define recursivamente: es 1 si n es 0, y para valores mayores, es n multiplicado por el factorial de n-1. Esta definición recursiva es natural en programación funcional y refleja directamente la fórmula matemática, sin necesidad de bucles ni variables mutables.
def factorial(0), do: 1
def factorial(n), do: n * factorial(n - 1)
Aquí, la función factorial está definida en términos de sí misma, y el lenguaje se encarga de gestionar la recursión y el cálculo. No hay asignaciones ni estados que modificar, solo definiciones claras y concisas.
Otro ejemplo que muestra la diferencia entre paradigmas es la función de filtrado. En programación imperativa, para filtrar una lista y obtener solo los elementos pares, normalmente escribimos un bucle que recorre la lista, verifica cada elemento y, si cumple la condición, lo añade a una lista auxiliar. Este proceso implica modificar el estado de la lista auxiliar en cada iteración.
En cambio, en programación funcional, pensamos en la lista como una estructura recursiva compuesta por una cabeza (el primer elemento) y una cola (el resto de la lista). Filtrar una lista se define recursivamente: si la lista está vacía, el resultado es una lista vacía; si la cabeza cumple la condición, la incluimos en el resultado y filtramos recursivamente la cola; si no, simplemente filtramos la cola sin incluir la cabeza.
def filter([], _func), do: []
def filter([head | tail], func) do
if func.(head) do
[head | filter(tail, func)]
else
filter(tail, func)
end
end
En este código, filter es una función que recibe una lista y una función func que determina si un elemento debe incluirse. La función se aplica recursivamente sin modificar ningún estado, construyendo una nueva lista resultado.
Además, la programación funcional destaca por tratar las funciones como elementos de primera clase. Esto significa que podemos pasar funciones como argumentos a otras funciones, devolverlas como resultados o asignarlas a variables, igual que haríamos con cualquier otro dato. Este enfoque facilita la creación de programas más modulares y expresivos. Por ejemplo, en Elixir, podemos usar funciones estándar como Enum.filter para filtrar listas sin tener que implementar el algoritmo nosotros mismos, simplemente pasando la función que define el criterio de filtrado.
Enum.filter([1, 2, 3, 4], fn x -> rem(x, 2) == 0 end)
# Resultado: [2, 4]
Aquí, Enum.filter recibe la lista y una función anónima que devuelve true para los números pares, filtrando la lista de forma declarativa y sin mutaciones.
En resumen, la programación funcional nos invita a pensar en términos de definiciones y expresiones, usando recursión para manejar estructuras de datos y aprovechando la capacidad de tratar funciones como ciudadanos de primera clase. Aunque al principio puede resultar un cambio de mentalidad desafiante para quienes vienen de la programación imperativa, con práctica y paciencia se convierte en una forma elegante y poderosa de resolver problemas, especialmente en lenguajes como Elixir que están diseñados para aprovechar estas características.