Si algo podemos aprender de la programación funcional es que nos falta vocabulario
Publicado el 8 de febrero de 2021
Algo llamativo de la programación funcional, sobre todo al ser observado desde un trasfondo más imperativo u orientado a objetos, es la enorme cantidad de funciones y tipos distintos que un programador funcional usa de manera habitual. Incluso para problemas triviales como invocar una función en cada elemento de una secuencia o sumar todos los elementos, un programador funcional recurrirá a una función que lo haga. A la hora de enseñar el lenguaje, mientras que con C o Java se empezaría por la sintaxis y si eso pequeñas partes de la biblioteca, un tutorial de Haskell enseguida saltará a enseñarte
map
,
fold
,
filter
y compañía. Esto tiene un claro impacto en el código escrito. Mientras que en un programa de Haskell habitualmente nos encontramos con un léxico muy rico, donde para expresar distintos significados y distintas operaciones se usan palabras distintas, es común que código en C++ suene a Homer en el cómico fragmento de Los Simpson de arriba. Hay una llamativa diferencia cultural entre los programadores de los distintos paradigmas ante la que es interesante preguntarse qué podemos aprender de cómo se hacen las cosas en otras partes.
Una de las causas de esto puede encontrarse en C, lenguaje con unos constructos muy poderosos pero poca capacidad para permitir al programador crear sus propias abstracciones. C no permite por ejemplo al programador definir sus propios operadores. Si creamos un tipo de vector R^3 o de número complejo, no podremos usar los símbolos de
+
o
*
para la suma o la multiplicación como podemos hacer con los enteros y reales que trae incorporados el lenguaje. A la hora de escribir algoritmos, la limitadísima capacidad de C para la programación genérica hace que no podamos escribir una función para buscar en cualquier secuencia o para acumular los elementos de una secuencia en uno solo, por ejemplo, mientras que for funciona para todos los arrays muy bien. Para representar algo que puede existir o no, en C es muy cómodo hacerlo mediante un pointer, mientras que escribir una abstracción que lo haga y no sea muy engorrosa de usar es prácticamente imposible. Sin embargo, los pointers en C no son sólo la forma de hacer que algo sea opcional. También son la forma en la que se evita una copia pasando la dirección en memoria de un objeto en lugar de copiarlo, la forma en la que se representa un recurso que hay que liberar, y la forma en la que varias partes del código pueden compartir un objeto. Igualmente, el bucle for es la forma de implementar la mayoría de los algoritmos, no sólo buscar o acumular. Esto hace que cuando en un programa en C nos encontramos un pointer tengamos que pararnos a pensar si lo que esto significa es que el objeto es opcional, que es un recurso, que es compartido… O una combinación de los anteriores. El programa nos está hablando de esa cosa de “taca y a comer” y es el trabajo del lector cada vez entender que se está hablando de una cuchara y no un tenedor o unos palillos chinos. Aunque lenguajes posteriores a C como C++ o Java tienen muchas más facilidades para que el programador desarrolle abstracciones, y en el caso de C++ una colección de brillantes ejemplos de buenas abstracciones en la biblioteca estándar, parece que esta tradición sigue pesando bastante. Total, si algo se puede hacer bastante bien con esto que ya existe, ¿para qué molestarse en escribir algo mejor?
Por otro lado, parece que la tradición en los lenguajes funcionales es completamente la opuesta. Esto posiblemente se deba a que las capacidades del lenguaje de base sean bastante limitadas. Recordemos que ni siquiera hay capacidad para iterar una secuencia, cosa que hay que hacer de forma recursiva, así que la mayoría de las veces resolver un problema de este tipo llamando a uno o varios algoritmos existentes puede ahorrar bastante tecleo. A esto se suma el hecho de que estos lenguajes no tiene ningún constructo de la flexibilidad del pointer en C, por lo que no es posible incurrir en un uso indebido de él. Si quieres hacer algo opcional, tu única opción es usar
Maybe
. ¿Quieres una secuencia? Tienes que usar una lista. Si quieres un recurso estás obligado a usar las abstracciones que ya existen. Por último, el que todas las funciones sean puras también ayuda bastante a hacerlas reusables, de forma que lo que uno escribe hoy puede ser un ladrillo con el que se construya un algoritmo más grande mañana. También es un factor importante la brevedad con la que se puede definir tipos en lenguajes como Haskell, donde uno de los tipos de vocabulario que hemos mencionado hasta ahora requiere como mucho unas decenas de líneas de código, mientras que la implementación de
pair
de libc++ tiene más de 450 líneas y la de
optional
más de 1200.
De todas formas, más que el cómo hemos llegado aquí, lo que es importante mirar es en qué impacta esta diferencia. Vale, tienen un vocabulario más rico. ¿Y qué? La principal ventaja es la legibilidad del código. Todo acto de comunicación tiene dos partes, un emisor que codifica una información y un receptor que la decodifica e interpreta. Al usar un vocabulario más amplio y asignar palabras distintas a su significado, el emisor está haciendo un trabajo mayor para facilitar la comprensión, es decir, para reducir la cantidad de trabajo que tiene que hacer el receptor para entender el mensaje. Por supuesto que podemos entender lo que hace el código en C, porque existe en el contexto de un programa, pero cuanto más ambigua sea una línea más contexto hará falta para entender del todo su significado, y por lo tanto más laborioso será el proceso de leer y entender ese código. Por otro lado, un fragmento donde todo está precisamente nombrado será muchas veces fácilmente comprensible con menos esfuerzo y sin necesidad de contexto. Es decir, es mucho más fácil entender que estamos hablando de una secuencia de números que puede o no existir si hablamos de un
optional<vector<int>>
que si hablamos de un
int *
. Es mucho más fácil saber que estamos buscando un objeto en una secuencia y llamando a una función en él si vemos un find seguido de la llamada a la función sobre el objeto encontrado que si vemos un for que contiene un if que contiene un break. El trabajo que nuestro cerebro tiene que hacer para asignar significado a lo que leemos es mucho menor.
Otra ventaja importante es que usar un vocabulario más específico obliga al programador a concretar más y tener más claro el diseño de lo que está creando. Usar palabras baúl, que pueden significar muchas cosas a la vez, puede ser síntoma de falta de claridad en las ideas de quién está escribiendo. Falta de intencionalidad. Es decir, que el caso no sea que el programador necesitaba expresar que algo podía ser opcional así que usó un pointer, sino que el programador no sabía muy bien lo que quería así que usó un pointer que vale para muchas cosas a la vez y no te pide tener que concretar qué quieres. Esto también permite al código cambiar sutilmente sin que el cambio sea fácilmente visible. Algo que antes no era opcional puede pasar a serlo por ejemplo, o al revés, sin cambiar las interfaces. Esa “flexibilidad” es en realidad peligrosa porque puede resultar sorprendente para el usuario, y suele ser fruto de la falta de concreción y de la falta de comprensión.
La buena noticia es que los lenguajes contemporáneos, incluso los que no son funcionales, tienen una gran capacidad para permitir al programador definir abstracciones. Muchas de ellas ya han sido escritas por otras personas y algunas estandarizadas, y el resto las podemos escribir nosotros mismos. Llamar a las cosas por su nombre no tiene nada que ver con la programación funcional. Que sea más común en su cultura no es más que una consecuencia, un efecto secundario, del distinto diseño de los lenguajes. Lo relevante es que es algo que mejora notablemente la calidad del código y de lo que nos podemos beneficiar todos enormemente. Es cierto que puede suponer un esfuerzo mayor a la hora de escribir, sobre todo al principio cuando hay que quitarse de encima las malas costumbres y cuando nos fuerza a escribir nuestras ideas con un grado de concreción al que no estamos acostumbrados, pero puede ser muy beneficioso a largo plazo, sobre todo si tenemos en cuenta que por lo general un trozo de código es escrito una vez y leído decenas, así que ese esfuerzo extra a la hora de escribir algo va a quedar amortizado con creces en el trabajo que ahorraremos a otros o a nosotros mismos en el futuro.
Logo of RSS.