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 emacs-lisp 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:
- Teclea
M-x apropos
y<RET>
. - 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.
Footnotes:
Nunca se publicó la parte II.
Ese texto aparecerá una vez definida y cargada nuestra función
si utilizamos el apropos
de Elisp.
Si elisp evalúa esa lista, lanzará un error diciendo que no es una función válida y no la puede evaluar.
Hay otras estructuras opcionales en la definición de una función, pero ya las veremos más adelante, cuando toque.
Comentarios