Las funciones que funcionan o el título de este blog

Como lo indica su nombre la idea es hablar de funciones. Pero en particular de funciones que funcionan. Qué quiere decir esto? Bueno, más allá del juego de palabras, el punto con el nombre es destacar el hecho de que en la computación las funciones no son como en la matemática. En la matemática, se puede definir una función o hablar de funciones sin siquiera tener un método efectivo para poder evaluarla. Poniendo un ejemplo concreto, tenemos la función f (x) = \sqrt{x} que es bien conocida: la raíz cuadrada.

Desde el punto de vista matemática, si especificamos su dominio y codominio correctamente queda con eso bien definida y se puede usar para demostrar propiedades, y demás cosas que hacen los matemáticos. El problema es que esa función no «funciona» sólo por escribirla. Y digo que no funciona en el sentido de que no calcula. O sea, si queremos conocer f(2022), por ejemplo, tenemos que calcularlo de alguna manera y la definición de la función, así como está, no nos dice cómo. En este caso en particular el problema es más grave aún porque la respuesta tendrá tantos decimales que no nos va a quedar otra que conformarnos con una aproximación.

Así, no son las funciones matemáticas las que nos interesan (bueno, sí nos interesan, pero se entiende), sino las que podemos llamar programas. Son programas, digamos, las que una vez que reciben su o sus argumentos se pueden ejecutar con la esperanza de que devuelvan un resultado útil.

Digo con la esperanza porque puede ocurrir que no terminen, es decir, se cuelguen. En general parece que un programa que se cuelga es porque está mal hecho. Pero puede ocurrir que en realidad no se colgaba y faltaba esperar un poco más, y también pude ocurrir que efectivamente se colgaba y que esa era la idea, justamnte que se cuelgue en ese caso. Así que no necesariamente hay que decir que no funcionan las funciones que se cuelgan.

También hay funciones, en el sentido matemático, que no se pueden computar, o sea que no se pueden «programar». El ejemplo más conocido es HALT. Esta función recibe recibe 2 argumentos: un programa y una entrada (para dicho programa) y nos devuelve «si», si es que ese programa con esa entrada termina, y «no» en caso contrario.

Y si bien este tipo de cosas forman parte de las ciencias de la computación, en la industria estamos en general dando vueltas en torno a problemas computables (después de todo es lo único que podemos entregar).

Esta idea de función hoy es bastante central en los lenguajes de programación, pero esto no siempre fue tan así. Hoy se usan lenguajes cono conocidos como «de alto nivel»: C, C++, java, python, Haskell, etc. Si bien algunos dicen que C es bajo nivel, es porque se usa el término en un matiz un poco distinto al que tenía en un principio, cuando el lenguaje era el lenguaje de la maquina, el assembly. Escribir en assembly es básicamente agarrar el manual de instrucciones de la maquina e ir dictando uno por uno los pasos a seguir, eso es bajo nivel. Pero en los años 50 (más o menos) surgieron los primeros compiladores. Con las gramáticas generativas, el desarrollo de un compilador se simplificó drásticamente y empezaron a aparecer distintos lenguajes y compiladores. Los compiladores lo que hacen es tomar un «programa» escrito siguiendo una sintaxis determinada, y lo traduce al lenguaje de una maquina. Con los compiladores, entonces, subimos el nivel de abstracción, ya no pensamos en las operaciones que la máquina tiene que ejecutar si no en conceptos más útiles para la tarea de diseñar software. Por ejemplo, la idea de función. Hoy por hoy se suele decir que C es de bajo nivel pero porque lo es en comparación por ejemplo a Haskell, que cuenta con muchas más abstracciones. Por ejemplo, en Haskell podemos aplicar parcialmente una función, es decir agarrar una función y fijarle «on the fly» uno de sus parámetros para después usarla así. C no permite esta y muchas otras cosas (si bien uno podría hacer cosas para obtener resultados iguales, el código termina siendo mucho más complicado.

Pero tampoco es que la función surgió directamente o de una vez por todas. Lo que sucede es que los programadores pensaron una forma de evitar tener que repetir un mismo bloque de código cada vez que sea necesario repetir que la maquina ejecute una misma tarea. Es decir, en vez de escribir nuevamente las mismas instrucciones cada vez, hacer algo para que el programa las ejecute pero sin tener ni que reescribirlas ni copypatearlas. En interesante que bien al principio ni las maquinas ofrecían esa funcionalidad ni los lenguajes. Lo que hicieron fue llamar subrutinas a esos bloques reutilizables y para poder reutilizarlos tenían que escribir el código que le pasaba el control a la subrutina (con algún argumento quizá) y volvía después al programa principal. Esa parte se llamaba subroutine linkage. (bueno, las primeras subrutinas ni eso, al principio ya el mero copy y paste era mejor que reescribir no sólo porque es menos tiempo, si no porque darle una función a un bloque, es decir que «haga tal cosa» ayuda a que el código sea más ordenado. Aparentemente, la primer subrutina se escribió para la Harvard Mark I por Grace Hooper en 1944 y lo que hacía era calcular la función seno, pero era una subrutina abierta, para ser insertada en el código que la vaya a usar en lugar de dinámicamente linkeada).

Imagen obtenida de este blog

Una ventaja obvia de las subrutinas es que achicaba el tamaño de los programas: si vemos cuánta memoria se podía usar en esa época vemos que era definitivamente una ventaja. Pero mucho más que eso lo que las subrutinas tienen de bueno son dos cosas:

  • permiten entender mejor la complejidad del código
  • permiten escribir «bibliotecas» (libraries en inglés, correctamente es bibliotecas en castellano pero muchas veces se dice «librerías» quizá porque los programadores no somos de letras).

La segunda ventaja es muy importante desde el punto de vista práctico, pero la primera es fundamental para el progreso de la disciplina (bueno, las dos lo son). Imagínense un choclo de código en assembly sin siquiera subrutinas, con bloques repetidos por doquier y ni si quiera identificados. Nadie quiere mantener eso y con razón.

Ahora bien entre las instrucciones de la computadora están los jumps absolutos o goto que lo que hacen es permitirnos saltar de una instrucción a cualquier parte del programa, indicando simplemente la línea a la que queremos ir. Una célebre nota del no menos célebre Edgar Dijkstra ha terminado sepultando el goto en los lenguajes de alto nivel.

Resumiendo el tema es así. Si tenemos gotos podemos saltar de cualquier parte del código a cualquier otra haciendo caótico seguir el curso de la ejecución por ejemplo al debugguear y por ende entender el programa. Entonces, si sacamos los gotos sabemos que uno sigue la ejecución hasta llegar al llamado a una subrutina y al fin de la misma (o a lo sumo a un return) vuelve de donde la había llamado.

En fin, hay muchos temas que venimos mencionando medio al pasar y la idea es quizá dedicarle un post a cada uno más adelante. Acá me quería centrar simplemente en el tema del blog, que es la programación con funciones. Sólo voy a agregar unos comentarios más.

Las funciones en sentido matemático tienen algunas características que las hacen bien distintas a las subrutinas (o incluso a las funciones de lenguajes como C o python, los llamados imperativos) y que sin embargo son muy deseables en la programación. Estas son la transparencia referencial y la inexistencia de efectos secundarios.

El segundo punto es bastante obvio, como las funciones matemáticas no ejecutan, tampoco van a ejecutar algo más de los que definen (por que no ejecutan nada, menos van a ejecutar de más). En cambio, una subrutina puede devolver un valor, pero además hacer cosas como mandárselo para que imprima la impresora o se guarde en una base de datos o cualquier otra cosa. Peor aún, pueden leer algo de una base de datos o esperar a que un usuario les provea algo, y en ese caso esas cosas podrían no pasar dando lugar a excepciones…

La transparencia referencial lo que dice es que siempre podemos reemplazar la función con su(s) parámetros por el valor correspondiente y el programa no se va a ver afectado por eso. Veamos un ejemplo donde esto no pasa. Hay una función de C que se llama rand que nos da un número aleatorio. La función no tiene parámetros, pero no podemos reemplazar rand por una valor porque, básicamente, no es una función (en sentido matemático, ya que las funciones matemáticas siempre asignan un mismo valor a un mismo argumento, y si no tienen argumentos deben ser constantes).

En computación, las funciones que cumplen con estas dos cosas se llaman funciones puras. Las funciones puras son muy buenas, comparándolas con las que no lo son, ya que, nuevamente, hacen más fácil analizar, entender, debuggear un programa. Y mientras más complicados son los programas, más bugs se nos pasan por alto y más tiempo estamos «fixeando» y más difícil es hacerlo …


Deja un comentario

Diseña un sitio como este con WordPress.com
Comenzar