Conceptos básicos de programación y bases de la programación funcional
Cuando se programa es común la necesidad de almacenar ciertos valores/resultados en la memoria para uso posterior (y así no tener que volver a obtener/expresar estos valores o resultados). Al hacerlo, es común también asociar un identificador (una palabra válida cualquiera) a ese valor que nos facilite recordar su significancia y su acceso a futuro.
En un sentido más abstracto, lo anterior se puede describir cómo la definición de una relación entre un identificador y una variable, dónde por variable entendemos un símbolo que puede representar a un valor cualquiera (tal como se entiende en las matemáticas, no más que un atajo a un valor). (también puede verse como la definición de un par ordenado (identificador, variable)). A cada una de estas relaciones entre un identificador y una variable, se le llama un entorno
Nota: no se debe confundir esta manera de entender el significado de variable con como se utiliza la palabra en muchos lenguajes de programación para referirse a un espacio físico (o una celda) de la memoria.
En los programas también es habitual la presencia de muchos identificadores refiriéndose cada uno a cosas distintas y viviendo cada uno en un espacio específico del programa (lo que también se conoce como su alcance, que puede ser, por ejemplo, el interior de una función determinada, de una clase determinada, o de cualquier otro bloque). Estos «espacios dónde viven los identificadores» también se pueden abstraer matemáticamente. Cada alcance en particular se puede describir cómo una función definiendo a un conjunto de pares ordenados (identificador, variable) específicos. O en otras palabras, definiendo una correspondencia entre identificadores y variables.
Nota: No es díficil visualizar como, a su vez, cada alcance específico dentro del programa (con sus propias variables locales), forma parte de un entorno más grande que representa al programa en general. Y de igual modo que para cada uno de estos alcances internos, este alcance global se puede modelar o representar como una función correspondiendo identificadores con variables. (dónde algunas de las variables representan a otros alcances!)
En definitiva, estos conceptos de identificador, variable, entorno son muy recurrentes y piezas tan fundamentales de la programación, que se podría decir que están presentes en todos los lenguajes modernos. Aunque en el caso de las variables, no siempre con el mismo comportamiento. Cuando declaramos una variable, si nos alineamos al modelo descrito más arriba, lo que hacemos es definir un par ordenado (identificador, variable) adicional para un entorno en específico. A esta altura (con la sola declaración), la variable no representa a ningún valor en particular. Para que lo haga, debemos realizar una acción más: una asignación. Dependiendo de cada lenguaje, la sintaxis podría ser diferente, pero lo que no cambia es que con la asignación vinculamos al fin un valor a la variable(y por consiguiente, al identificador). Perfecto, pero ¿Dónde está el potencial comportamiento distinto? te debes estar preguntando. ¿No es la declaración y la asignación lo mismo en todos lados? La verdad es que sí, los conceptos de declaración y asignación no varían, sin embargo, en lo último (la asignación) puede haber un mundo de diferencia. Esto, porque en su definición no especifica si se puede realizar sólo una vez o múltiples veces. Esto último es más bien una propiedad que especifica un paradigma. Es el paradigma el que define los límites de la asignación.
La asignación en la programación funcional
En la programación funcional las asignaciones son únicas. Esto quiere decir que una vez que se asigna un valor a una variable, no es posible asignar uno nuevo posteriormente.
declare X = 100
//una vez que la variable ya tiene asignado un valor
//no es posible asignarle uno nuevo
//error
X = 500
Dado que es una propiedad que regula el comportamiento de un constructo fundamental de la programación (variables), la «asignación única» es una propiedad fundamental del paradigma funcional.
Nota: es más preciso decir «la variable no se puede asignar a un nuevo valor» que decir «se puede realizar una sóla asignación». El motivo es que es posible realizar múltiples asignaciones sobre la variable, siempre que sea el mismo valor.
Nota2: Aunque en la programación funcional las asignaciones son únicas, nada dice sobre volver a declarar un identificador con el mismo nombre. Por lo tanto, si bien no se puede asignar un nuevo valor, se puede volver a declarar el mismo identificador y vincularlo esta vez con un nuevo valor. Es posible usar un mismo identificador en distintas partes de un programa, y que en cada parte se refiera a un valor distinto. (Siguiendo el modelo anterior, se define un entorno nuevo, o reescribe el anterior)
Ahora, está bien, el paradigma funcional cuenta con esta limitación para la asignación simplemente por una cuestión de definición. Pero, ¿Por qué limitarnos a una asignación única? ¿Qué sentido tiene incluir esta propiedad? ¿Acaso no nos estamos simplemente dificultando las cosas?. En realidad, la inclusión de esta propiedad no se debe a la aparente dificultad de no contar con asignaciones múltiples, sino por las ventajas que existen al hacerlo. Estas ventajas son similares a las dadas por cualquier otra regla restrictiva como los semáforos: reducir la probabilidad de que haya un accidente. En este caso reducir la probabilidad de romper un programa que funciona correctamente. La asignación múltiple, como se verá, es un concepto distinto (de esos conceptos que mencionamos definen a un paradigma). Por lo tanto, si cambiamos la asignación múltiple por la única en la definición del paradigma funcional, pero mantenemos todo lo demás, aún así ya no tenemos paradigma funcional, sino otro completamente diferente.
Funciones
La capacidad de almacenar valores y no tener que escribirlos una y otra vez ya es un gran avance en el camino de facilitarnos la programación, pero no sólo de valores están compuestos los programas. Además de los valores, están las operaciones, lo que hacemos con esos valores para obtener los resultados que buscamos. Y en el caso de las operaciones, también es común el uso de una misma operación en distintas partes del programa, aunque vinculando distintos valores a los identificadores (por ejemplo la operación genérica a + b, podría ser 1 + 2, 3 + 4, etc..). En definitiva, nos vendría bien un medio para agrupar un conjunto de operaciones o instrucciones en torno a un identificador común, y así no tener que escribir una y otra vez lo mismo aunque involucre a valores distintos. Ese medio justamente son las funciones. Para evitar repetir el código una y otra vez lo que podemos hacer es crear una función. Definiendo una función asociamos a un identificador una correspondencia entre un conjunto de valores de entrada (argumentos) y un único valor de salida (retorno). La correspondencia, en tanto, queda definida por la serie de operaciones/instrucciones incluidas en su cuerpo.
Nota: Una función se puede considerar un tipo de valor, así como los enteros, los flotantes, y los caracteres son un tipo de valor. Claramente, distinto a los anteriores, pero que de igual modo se puede vincular a una variable. En otras palabras, lo que hicimos fue asociar una variable a un identificador al igual que antes, sólo que ahora el valor vinculado a la variable es una función
Los conceptos de la programación funcional
No debiera sorprender que el concepto de función sea fundamental en la programación «funcional». Es decir, por algún motivo ha de llamarse así. La programación funcional está basada en modelar las computaciones con el sólo uso de funciones. Aunque para que eso sea posible no basta con los conceptos mencionados hasta ahora, hace falta incluir algunos más, específicamente tres conceptos más. De momento, tenemos un panorama de programación compuesto de variables(de asignación única) que vinculamos a un identificador, de operaciones que podemos realizar sobre los valores vinculados a esas variables, y de funciones (no más que una serie de operaciones/instrucciones agrupadas como si se tratasen de un sólo valor). Con sólo estos tres conceptos podemos hacer bastante poco la verdad. Dejando de lado las estructuras de datos, y centrándonos en la lógica de la programación, con el sólo uso de esos conceptos tendríamos que limitarnos a programas que se ejecuten de manera secuencial, instrucción por instrucción, sin que podamos controlar el flujo de ejecución. Y este hecho es clave «sin que podamos controlar el flujo de ejecución». Es decir, sin la capacidad de definir escenarios en los que nuestro programa se comporte de distinto modo según las circunstancias. Y la verdad, sin esa capacidad, nos limitamos demasiado. El control de flujo es una característica indispensable al momento de programar y, por lo tanto, todo paradigma debiera facilitarlo de algún modo. Justamente ahí es donde entran los últimos tres conceptos. Son los que nos brindan el potencial del control de flujo en el paradigma funcional. Estos conceptos son: la composición de funciones, la recursividad y las condicionales.
Composición de funciones
En el contexto de la programación, componer funciones significa definir funciones que llamen a otras funciones desde su interior. (definir una función en términos de otra función).
fun Id(x1, x2, ..., xN)
Id2()
Id3()
...
end
Esta característica es clave para construir sistemas grandes, ya que podemos construirlos en capas, donde cada función de una capa llama a funciones de capas más abajos, y cada capa está a cargo de un equipo de desarrollo diferente. En la práctica, los sistemas pueden tener cientos de miles de capas.
Recursividad
Una recursión es una función que se llama a sí misma. Con esta capacidad podemos resolver problemas que requieran hacer uso de más de una vez un mismo conjunto de operaciones.
fun Id(x1, x2, ..., xN)
[operaciones]
Id(x'1, x'2, ..., xN)
end
Condicionales
Cuando se aplica la recursividad se hace necesario contar con un medio para definir cuando detener las llamadas recursivas; de otro modo se realizarían de forma «infinita». Ese medio son las condicionales (por lo general una instrucción if). Las condicionales nos permiten definir múltiples vías de ejecución posibles en nuestros programas, y asociar a cada una de los caminos una condición (de ahí el nombre) que se debe cumplir para que el programa tome ese camino u otro. Por lo tanto, podemos definir una condición que controle si en una llamada a una función se realiza una llamada recursiva o no.
if condicion
[operaciones]
else
[operaciones]
end
Nota: Por supuesto, las condicionales no se tienen porque limitar a controlar las llamadas recursivas (y con ello, asegurar no se realicen de forma infinita), pero en conjunto con la recursividad definen una técnica poderosa para resolver problemas complejos diviendolos en subproblemas más pequeños (técnica de dividir y conquistar).
Ejemplo de una solución usando dividir y conquistar, calcular la suma de los digitos de un número
fun sumDigitos: N =>
if (N == 0) then return 0
else
return (N mod 10) + (sumDigitos (N div 10))
end
end
Nota: div denota al operador para la división entera.
Resumen
Ya podemos contar con un lenguaje que reúne todo lo que necesitamos para programar en estilo funcional. En adición, podemos decir que es un lenguaje Turing completo, dado que puede computar las mismas cosas que una máquina de Turing (un modelo para representar teóricamente una computadora definido por Alan Turing en los años 30). Y dado que una máquina de Turing es el modelo de computadora más poderoso que sabemos como construir en la actualidad(en términos de lo que se puede programar), significa que con el lenguaje que definimos usando apenas seis conceptos podemos programar todo lo que sabemos calcular usando programas de computadora.
En conclusión, el lenguaje funcional que definimos es tan poderoso como una máquina de Turing y en lo que sigue, revisaremos como aprovechar en parte este potencial haciendo uso de la recursión para aplicar las técnicas de «programación invariante» y «programación simbólica», y mucho más adelante, las técnicas de «programación de alto orden», de «abstracción de datos» y «de programación concurrente».
identificadores
+ variables asignación única
+ operaciones
+ funciones
+ composición de funciones
+ recursividad
+ condicionales
------------------------------
programación funcional