Notxor tiene un blog

Defenestrando la vida


Un poco más sobre funciones.

Un poco de historia para comenzar

Una de las bellezas de Lisp, o de Smalltalk, es que se basan en un concepto claro y lo llevan hasta sus últimas consecuencias. En Smalltalk todo el sistema se basa en objetos enviándose mensajes entre ellos. Así, por ejemplo, en Smalltalk la forma de ejecutar código de manera condicional no depende de una palabra clave del lenguaje, sino que es un mensaje que se manda a un objeto lógico. Otros lenguajes recurren en su sintaxis a expresiones definidas por el intérprete o el compilador con lo que se llaman palabras reservadas. No voy a darle muchas vueltas más al tema de las palabras reservadas.

Sólo comentar que Alan Kay, desarrollador principal de Smalltalk afirmó verse muy influenciado por Lisp, y también remarcar que Smalltalk influyó en Lisp haciendo que se pudiera utilizar POO (Programación Orientada a Objetos). La clave fundamental de ambos sistemas es que están pensados para que un programador ─que sepa lo que hace─ pueda redefinir todo el sistema y también en ambos, se puede hacer en caliente, mientras se está ejecutando. Por lo tanto, todo el código del sistema es accesible para el programador. Lo digo por propia experiencia, me he cargado unas cuantas imágenes de trabajo de Smalltalk, intentando hacer lo que no debía. Lo bueno, es que a pesar de eso nunca pasa nada: reinicias el trabajo, quitas lo que has puesto o metes lo que has quitado y las cosas se suelen arreglar, no tengáis miedo. Al contrario, equivocándote aprendes más que no haciendo y si no eres capaz de arreglarlo, siempre puedes reinstalarlo (que tampoco es tan grave).

El Lisp fue presentado por John McCarthy en 1960 en el MIT en un artículo con el título «Funciones recursivas de expresiones simbólicas y su cómputo a máquina, Parte I»1, demostrando en él que con algunos operadores simples y una notación para las funciones, se puede construir un lenguaje de programación completo para procesar algoritmos.

Buenas maneras

También comparten, ambos sistemas, algunas buenas costumbres de programación. Por ejemplo, el documentar bien su código. Ambos parten del hecho de que el código se escribe una vez y se lee muchas para modificarlo algunas, así pues el código suele estar documentado. Además, aunque sea un lenguaje interpretado los comentarios no disminuyen la efectividad del código, porque el sistema lo compila a un byte code que interpreta la máquina del sistema. No seáis rácanos con los comentarios, lo que ahora sentado con tranquilidad haciendo nuestras cosas es lógico, deja de serlo unos meses, o semanas, o días después, cuando el problema que nos llevó a escribir ese código dejó de estar en nuestra cabeza, porque lo dejamos solucionado.

Otra de las cosas que aprendí con Smalltalk y que mantengo desde hace tiempo son las pruebas unitarias o Units tests. Algo muy lógico cuando lo entiendes pero a lo que no se llega con el sentido común. Las pruebas unitarias se basan en que hay que escribir primero código suplementario que pruebe que nuestro código productivo funciona. Lo iremos viendo cuando nos metamos en faena.

Listas, funciones y cálculo lambda

Lisp se desarrolló basándose en el cálculo lambda, que es un sistema formal presentado en 1930 por Alonzo Church y Stephen Kleene para investigar la definición de función, la noción de aplicaciones de funciones y la recursión.

Como estos son conceptos matemáticos que van mas allá de lo que puede abarcar éste artículo, lo dejaremos aquí. Sólo avanzar que el cálculo lambda tiene una gran influencia en los lenguajes llamados funcionales como Lisp o Haskell.

El elemento fundamental de Lisp son las listas. Dado que en Lisp las listas se delimitan con paréntesis, hay quien dice que LISP es un acrónimo de Lost In Stupid Parentheses. Y la verdad es que a los que se acercan por primera vez al lenguaje les puede llamar la atención la profusión de los mismos.

Definir funciones

Una función se define llamando a defun de la siguiente manera:

(defun incremento (num)
  "Realiza el incremento de un número."
  (+ 1 num))

Podemos ver una serie de elementos que se van a repetir siempre en la estructura de defun. El primero tras la llamada es el nombre de la función que estamos creando, en nuestro caso incremento. A continuación viene una lista que son los argumentos de la función, en nuestro caso sólo hay uno y lo llamamos num, lo podríamos haber llamado número, pero es más largo. El tercer elemento de la definición es una cadena de texto que describe qué es lo que hace nuestra función2. La última parte es el cuerpo de la función, en nuestro caso el cálculo de sumar uno al número que le hemos pasado. En elisp ya existe una función que hace lo mismo: 1+, pero esto sólo es para nuestro ejemplo. Un ejercicio sencillo: teclea el texto de la definición anterior y cuando lo tengas evalúalo, con C-j o con C-x C-e. Cuando lo tengas haz la siguiente prueba:

  1. Teclea M-x apropos y <RET>.
  2. Teclea incremento... ¿Qué sucede?

Efectivamente, la cadena que hemos escrito en nuestra función pasa a estar disponible en uno de los sistemas de ayuda de Emacs: apropos. Recuérdalo, porque muchas veces la forma de enterarse de para qué sirve una variable o cómo funciona una función (valga la «rebuznancia») es llamar a apropos. Y es muy frustrante acudir a la ayuda y no encontrar nada, así que recuerdo: Documenta todo el código que escribas, por tu propio bien.

Usar funciones

Vamos con un ejemplo y sobre él explicaré algunas cosas más delicadas o detalles. Vamos a aprovechar que tenemos definida ya la función incremento y vamos a teclear el siguiente código. De momento iremos evaluando cada expresión al finalizarla en el buffer *scratch*.

(set 'lista1 '(1 5 9))
(setq lista2 (list 1 5 9))

Estoy definiendo dos listas diferentes con el mismo contenido. Lo hago de dos maneras para apreciar el detalle del apóstrofe en el código. Ya he comentado que el apóstrofe se coloca sólo en los casos en que queramos que Lisp no interprete el elemento como ejecutable. En ambos casos definimos una variable global que contendrá una lista con los elementos 1, 5 y 9. Ambas definiciones tienen tres elementos: la función de asignación: set y setq; el nombre de la variable: lista1 y lista2 y el contenido que se asigna a ellas. Si nos fijamos, vemos que en algunos lugares hemos añadido un carácter «'». ¿Cuándo se pone y cuándo no? Pues como he dicho antes, se pone cuando queremos que Lisp no evalúe la expresión que le pasamos. Por ejemplo, no queremos que evalúe la expresión lista1, así que le ponemos el «'»; en el segundo caso lista2 no lleva porque setq hace lo mismo que set pero no evalúa la siguiente expresión, o dicho de otro modo (no exacto): pone por nosotros ese apóstrofe (quote en inglés). Si vemos cómo formamos la expresión que asignamos, en el primer caso se lo damos hecho, es la lista (1 5 9), mientras que en el segundo caso llamamos a la función list para que forme la lista con los elementos que le pasamos. En el primer caso, no queremos que Lisp evalúe la lista3, sin embargo, en el segundo, queremos que evalúe list y nos devuelva su valor. ¿Lioso? Al principio puede serlo, pero con la práctica se verá que es sencillo de entender.

Un apunte sobre macros

No quiero liarnos con las macros. Los menciono sólo porque Emacs se llama editor de macros y algo hay que saber sobre ellas. Pero ¿qué es una macro?

Vamos a ver un ejemplo de macro: defun. Sí, defun no es una función es una macro que define el símbolo que le pasamos como función, crea una expresión lambda y la asigna al símbolo creado. Su estructura es la siguiente:

(defun nombre (argumentos)
  "documentación"
  cuerpo)

La documentación es opcional, claro, pero muy recomendable utilizarla.4

Tampoco hay que preocuparse demasiado por las macros si ahora no se entienden. Una macro es una estructura que se parece mucho a una función: tiene un nombre al que llamar y una lista de elementos que son los argumentos de la macro. La diferencia fundamental es que los argumentos que se pasan a la macro no se evalúan, se pasan tal cual con la expresión que se suministre; sin embargo, los argumentos de una función son el resultado de evaluar los elementos que se le pasen como argumentos. Un lío a estas alturas ¿no?

Digo que no hay que preocuparse demasiado por ellas, porque normalmente se utilizan para extender el lenguaje Lisp y no estamos en ese nivel aún.

Vemos un ejemplo sencillo con nuestro incrementador para aplicarlo a variables:

(defmacro inc (var)
  (list 'setq var (list 'incremento var)))

Si lo probamos en el buffer *scratch* veremos que no funciona llamándolo con (inc 4), porque trabaja con variables (por eso, el argumento lo hemos llamado var). ¿Cómo trabaja la macro? Supongamos que tenemos una variable x con valor 4. Lo que hace la macro es convertir su cuerpo en la siguiente expresión:

(setq x (incremento x))

Esta expresión es la que evalúa Lisp para devolvernos la variable x incrementada.

Un pequeño apunte sobre tipos de datos

Vamos a hacer un experimento con nuestra función incremento. Como vemos el parámetro que trabaja lo hemos llamado num, lo podríamos haber llamado pepe como mi vecino o juan como mi cuñado, pero no es la función de los nombres. Es aconsejable que utilicemos los nombres también para dar una pista de lo que es: en nuestro caso un número. Si a nuestra función le enviamos un valor numérico no hay problema. Evaluad las siguientes expresiones una a una:

(incremento 2)
(incremento 0.5)

No hay problema. La primer nos devolverá 3 y la segunda 1.5. Funciona como esperamos, pero ¿qué ocurre si le pasamos una cadena?

(incremento "hola")

Evalúa la expresión anterior y verás qué sucede... Aparecerá un buffer de error que nos dirá algo así:

Debugger entered--Lisp error: (wrong-type-argument number-or-marker-p "hola")
  +(1 "hola")
  incremento("hola")
  eval((incremento "hola") nil)
  elisp--eval-last-sexp(t)
  eval-last-sexp(t)
  eval-print-last-sexp(nil)
  funcall-interactively(eval-print-last-sexp nil)
  call-interactively(eval-print-last-sexp nil nil)
  command-execute(eval-print-last-sexp)

A destacar el wrong-type-argument... es decir, que el tipo del argumento no concuerda con algo que espera utilizar en una función +.

¿Podéis suponer qué ocurrirá si evaluamos la expresión (incremento ?A)? Probadlo... 66 ¿qué...? ¡Esto es un sindiós!

Bueno, no lo es: la expresión ?A es la forma de pasar a la máquina el carácter A, lo que ha hecho nuestra función es coger el carácter A, cuyo valor numérico es 65 y sumarle 1. En la próxima entrega, veremos más despacio esto de los tipos.

Nota al pie de página:

1

Nunca se publicó la parte II.

2

Ese texto aparecerá una vez definida y cargada nuestra función si utilizamos el apropos de Elisp.

3

Si elisp evalúa esa lista, lanzará un error diciendo que no es una función válida y no la puede evaluar.

4

Hay otras estructuras opcionales en la definición de una función, pero ya las veremos más adelante, cuando toque.


Comentarios