Un ensayo sobre Lisp y la enseñanza de las matemáticas

Niveles de abstracci´on. Lisp y la pedagog´ıa
F. Javier Gil Chica
versi´
on de enero de 2015
Resumen
Estudiar implica alcanzar sucesivos niveles de abstracci´
on. En este
art´ıculo se muestra c´omo estos niveles pueden hacerse expl´ıcitos mediante el uso de un lenguaje de programaci´
on y hacemos una excursi´
on
desde los niveles inferiores del aprendizaje de las matem´
aticas hasta
el nivel de los estudios superiores, mostrando c´omo estas abstracciones
se pueden formalizar al tiempo que obtenemos herramientas u
´tiles y
adquirimos formas de pensar y enfrentar problemas.
1.
Motivaci´
on
El curso de los estudios supone la adquisici´on de niveles de abstracci´on
cada vez m´as elevados, construidos a partir de abstracciones anteriores. Esto
es v´alido al menos para las disciplinas cient´ıficas. Por otro lado, la computaci´on personal es ya corriente. Por otro, finalmente, en muchos textos se
exponen procedimientos algor´ıtmicos, escritos en pseudoc´odigo, reconociendo
impl´ıcitamente que ´esta es una forma conveniente y u
´til de formalizar ciertos
conocimientos.
Nosotros nos proponemos mostrar c´omo, mediante un lenguaje de programaci´on de sintaxis trivial, el alumno puede formalizar estas sucesivas abstracciones, lo que tiene a nuestro modo de ver varias importantes ventajas.
En primer lugar, el dominio o al menos la familiaridad con un lenguaje de
programaci´on resulta m´as que conveniente para las disciplinas cient´ıficas,
desde el punto de vista de la mera utilidad. En segundo lugar, un nivel mediano en programaci´on es suficiente para que el estudiante se de cuenta y
aproveche algunos patrones generales, como pueden ser: la descomposici´on
de un problema en una serie de problemas m´as peque˜
nos, el uso de la recursividad, estrategias generales para abordar problemas (fuerza bruta, dividir
para vencer, b´
usqueda aleatoria, exhaustiva, heur´ısitca,...), el reconocimiento de estructuras de datos generales para organizar la informaci´on, etc. En
1
tercer lugar, la programaci´on de una abstracci´on reci´en adquirida obliga a
plantear esta abstracci´on de una forma clara, libre de ambig¨
uedades. En
cuarto lugar, con el paso del tiempo, el alumno puede adquirir una biblioteca personal de valor permanente, que le ayude en sus estudios posteriores.
Esto es mucho m´as interesante que instruirlo en el uso de programas comerciales, que nunca ser´an tan flexibles. No sugerimos que estos programas
deban ser descartados, s´olo que, con frecuencia, existen soluciones sencillas
para problemas sencillos (o no tanto) que est´an al alcance de un usuario
corriente y que pueden alcanzarse con poco esfuerzo y un alto nivel de satisfacci´on intelectual. Adem´as, la habilidad de los lenguajes para combinar
herramientas sencillas formando otras m´as complejas hace que la destreza
que se adquiera hoy se revalorizar´a con el tiempo: a medida que adquirimos
nuevas herramientas, crecen las posibilidades para combinarlas con otras que
ya poseemos.
Para mostrar nuestro punto de vista, vamos a guiar al lector por una
serie de niveles de abstracci´on creciente, y veremos c´omo estos niveles se
pueden expresar en un lenguaje de programaci´on. En definitiva, vamos a
crear peque˜
nos programas, casi todos triviales, para formalizar los niveles
crecientes que se alcanzan en el estudio de las matem´aticas.
Elegimos el lenguaje Lisp por varias razones. En primer lugar, es el u
´nico
lenguaje del que puede decirse que es bello. Su uso recompensa al intelecto.
En segundo lugar, porque su sintaxis es trivial, lo que nos ahorra tener que
mostrar una sintaxis particular. De hecho, al lector le bastar´a leer los ejemplos
para darse cuenta de la sintaxis. En tercer lugar, porque este lenguaje va
mucho m´as all´a de estos primeros niveles que nosotros vamos a desbrozar.
En cuarto lugar, porque es uno de los m´as antiguos lenguajes de programaci´on
(junto con Cobol y Fortran), y su estabilidad en el pasado dice mucho sobre
su posible estabilidad futura, de modo que el esfuerzo que se hace hoy en
aprenderlo se podr´a rentabilizar durante d´ecadas. En quinto lugar, porque es
interactivo: basta escribir una funci´on y ´esta funci´on puede ejecutarse, con
independencia de que pertenezca a un programa m´as complejo. Adem´as, su
interactividad nos libra del molesto ciclo de edici´on-compilaci´on-ejecuci´on.
En sexto lugar, porque dispone de una aritm´etica completa y potente, que
puede trabajar con enteros, reales, racionales y complejos, sin estar limitado
por los bits del procesador. Con seis es suficiente, pero se pueden presentar
otras seis, al menos.
¿Qu´e haremos a partir de aqu´ı? Presentaremos nivel a nivel, y c´omo puede
usarse Lips en cada uno de ellos.
2
2.
Nivel 1
Cuando un estudiante se encuentra en este nivel, es seguramente demasiado joven para introducirle un lenguaje de programaci´on. No obstante, para
hacer un recorrido completo, partimos de aqu´ı.
En este nivel, el alumno, probablemente en primaria, reconoce algunos
n´
umeros a primera vista. Cuando se le presentan conjuntos de objetos, reconoce inmediatamente cu´antos de estos objetos hay en el conjunto, sin
necesidad de contar. Esto sucede para conjuntos de 1, 2, 3 y probablemente
4 objetos. La suma es entonces algo natural, pues consiste s´olo en asignar
nombres al cardinal de los conjuntos mostrados. Por ejemplo, si a un infante
se le muestran dos conjuntos, uno con 1 elemento y otro con 2 elementos,
la suma de ambos cardinales es el cardinal de un conjunto de 3 elementos,
reconocible inmediatamente. En Lisp, esta operaci´on con n´
umeros sencillos
se escribe as´ı:
> (+ 1 2)
> 3
y el resultado es 3. Suponemos que tras escribir la primera l´ınea pulsamos
la tecla INTRO, y como resultado se imprime la segunda. Se observa ya que
en este lenguaje las operaciones se encierran entre par´entesis, y que tras
el par´entesis de entrada se escribe la operaci´on que se desea realizar, y a
continuaci´on sus argumentos. Pr´acticamente, esta es toda la sintaxis que hay
que saber.
3.
Nivel 2
En el siguiente nivel de abstracci´on, el alumno aprende a sumar n´
umeros
cualesquiera. Esto requiere de un algoritmo, y el algoritmo es independiente
de qu´e n´
umeros concretos se desean sumar. Esto expande las posibilidades.
En Lisp:
> (+ 234 907)
> 1141
4.
Nivel 3
A partir del nivel anterior, se alcanzan enseguida operaciones adicionales,
como la resta, la multiplicaci´on (que no es m´as que una suma repetida) y la
divisi´on (que no es m´as que una resta repetida). En Lisp:
3
>
>
>
>
>
>
>
>
(+ 101 202)
303
(- 34 16)
18
(* 20 20)
400
(/ 100 25)
4
En este punto, el estudiante puede usar Lisp como una calculadora potente, si bien limitada a n´
umeros enteros. Sin duda, es una calculadora mejor
que una de sobremesa, pues no est´a limitada por el tama˜
no de los n´
umeros
y la introducci´on de las operaciones es m´as c´omoda. ¿Por qu´e entonces no
usar Lisp como calculadora?
>
>
>
>
>
>
(+ 34535345289 8976568464)
43511913753
(- 986796796969698769875 858758765876443)
986795938210932893432
(* 987654321012345 123456789012345)
121932631126351935653102399025
5.
Nivel 4
En este nivel, el alumno conoce distintas clases de n´
umeros. Los enteros
no son suficientes, y se le presentan los naturales, y despu´es los racionales,
los reales y, finalmente, los complejos. Nuestro lenguaje es capaz de operar
con distintos tipos de n´
umeros. Por ejemplo con racionales:
> (+ 12/67 34/101)
> 3490/6767
Con reales:
> (* 1.0098 12.713)
> 12.837587
y con complejos:
> (/ #C(12 -9) #C(-9 7))
> #C(-171/130 -3/130)
4
6.
Nivel 5
El siguiente nivel borra las diferencias entre los distintos tipos de n´
umeros:
un n´
umero es s´olo un n´
umero. Lisp permite usar tipos distintos en la misma
expresi´on, y da el resultado en el tipo adecuado. Por ejemplo, la suma de dos
racionales es un racional, pero el producto de un racional por un real es un
real:
> (* 7/11 2.0)
> 1.2727273
Adem´as, se puede operar con complejos cuyas partes real e imaginaria
sean de cualquier tipo:
> (/ #C(5.0 45/118) #C(-9 14))
> #C(-0.14318056 -0.26509818)
7.
Nivel 6
En el nivel 6, generalizamos las operaciones b´asicas haciendo que puedan
tomar un n´
umero cualquiera de argumentos:
>
>
>
>
(+ 1 2/90 4.45 -6)
-0.5277777
(/ 1.0 2.0 4.0)
0.125
8.
Nivel 7
En el nivel 6, tenemos ya los operadores elementales, que pueden tomar un
n´
umero arbitario de operandos. Ahora ampliamos a´
un m´as las capacidades
de c´alculo, admitiendo que un operando puede ser no s´olo un n´
umero, sino
otra expresi´on, es decir, el resultado de otro u otros operandos. Adem´as, el
proceso puede anidarse tanto como se desee o necesite:
>
>
>
>
>
>
(+ 2 (* 3 3))
18
(* 4 (+ (/ 10 5) 1))
12
(- 10 (* 2 (+ 9 1 (- 3 5))))
-6
5
9.
Nivel 8
El nivel 8 es crucial, pues es el paso desde el concepto de operando al
concepto de funci´on. Sabemos que existen funciones como las exponenciales,
ra´ıces, logar´ıtimas y trigonom´etricas que, en u
´ltima instancia, se reducen a
una secuencia, quiz´as larga, de operaciones elementales de suma, resta, divisi´on y multiplicaci´on. Podemos ahora ver las operaciones elementales como
funciones sencillas e incluirlas en la gran familia de las ”funciones”. Estas
otras funciones tambi´en se pueden calcular en Lisp:
>
>
>
>
>
>
>
>
>
>
(sqrt 25.3)
5.0299106
(log 12)
2.4849067
(sin 0.34)
0.3334871
(cos 2.71)
-0.9083007
(atan 3.0)
1.2490458
10.
Nivel 9
El n´
umero de funciones que pueden definirse es infinito. Lisp permite
definir nuevas funciones, combinando funciones ya existentes. Existe una
funci´on especial, defun, para definir nuevas funciones. Para hacerlo adem´as,
necesitamos una abstracci´on adicional: la variable. En efecto, podemos escribir (sqrt 8), (sqrt 34) o de cualquier otro valor. El hecho de que la
funci´on pueda aplicarse a cualquier argumento de su dominio implica la separaci´on entre el procedimiento de c´alculo y los valores a los que se aplica este
procedimiento. Mediante el uso de variables podemos designar no un valor en
concreto, sino cualquier valor. Como ejemplo, definimos una funci´on que calcula la media de dos n´
umeros. Para representar a dos n´
umeros cualesquiera,
usamos dos variables, a y b:
>
>
>
>
>
(defun media (a b) (/ (+ a b) 2))
(media 4 9)
13/2
(media 3.0 7.6)
5.3
6
Inmediatamente despu´es, es posible usar esta funci´on como si fuese parte
del lenguaje, con el mismo rango que las funciones elementales o trigonom´etricas:
> (* 2.0 (media 3 4))
> 7.0
11.
Nivel 10
A veces una funci´on puede calcularse recursivamente. Recursivas son
aquellas funciones que entran dentro de su propia definici´on. Por ejemplo,
el factorial de un n´
umero n se puede definir en funci´on del factorial de n − 1:
n! = n × (n − 1)!. Hay muchas funciones que pueden calcularse as´ı. Para el
factorial:
(defun factorial (n)
(if (= n 0) 1 (* n (factorial (- n 1)))))
Recordemos que Lisp no est´a limitado a la representaci´on de los n´
umeros
por hardware, es decir, puede trabajar con cantidades que superan en mucho
las que se pueden representar con los 32 o 64 bits de los procesadores:
> (factorial 543)
> 87289155679026045684358636693446257021740446758498686215
79516602180334762072835529399733505370692175375494781142
29971312262991181125700413163606116971183695586311579361
17391585219324184468358556411453708009915701308257614957
35233353697384423555998167108040688653556845999420474539
68407440725166640261015140054933542126214585902983116193
90712911174766519604185503266095609588369497418699892424
77495293359310940924297256406583100421996682142026379246
31140057800119930627800378541111012271243379033822559451
08531541695567291279139523793259525765347547902120897915
43895916185954498557799257513083033148700674640758302108
93226767964127916083986194604372529850059441599156750579
67006711041599794976786096343900548525251434242518056433
00563926595522814735023531592849186104934114678005580760
01750687023376509841526414457026571388047691226042613278
04707607839582633502846804740587194154655813419655463834
04799081864291782417945589548785060786465318535054281875
07441610683354213870320835243780540993795316813622383436
7
15133564225574181769668968212741580998469316749061133683
03540213516072472617031543849136200880060209239477452800
00000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000
0000000000000000000
Entretanto, el mayor entero que se puede representar con 32 bits es
4294967296; con 64 bits: 18446744073709551616 y una hipot´etica futura generaci´on de procesadores de 128 bits podr´a representar enteros hasta
340282366920938463463374607431768211456. Para calcular estos n´
umeros,
hemos escrito en Lisp la funci´on ”elevado a” que toma dos enteros y calcula
el primero de ellos elevado al segundo:
> (defun elv (x y) (if (= y 0) 1 (* x (elv x (- y 1)))))
> (elv 2 32)
> 4294967296
12.
Intermedio: listas
La lista es la estructura de datos m´as general, si admitimos que un elemento de una lista puede a su vez ser una lista. Entonces, las listas permiten
representar vectores, matrices, pilas, colas, ´arboles y, por supuesto, listas. Estas estructuras de datos son abstractas, en el sentido de que pueden contener
elementos de muchos tipos distintos, como n´
umeros, palabras o combinaciones de ambos. Por ejemplo, un diccionario es una lista de listas de dos
elementos, cada una de las cuales contiene un t´ermino y su definici´on. Todos los datos que se organizan jer´arquicamente encajan en el tipo de datos
abstracto ”´arbol”. Por ejemplo, la taxonom´ıa de los reinos vegetal y animal se pueden representar como un ´arbol. En Lisp, una lista se representa
encerrando a sus elementos entre par´entesis:
(+ 1 2 3)
(hola adios luna)
Como se ve, en Lisp el c´odigo y los datos se representan de la misma
forma. La u
´nica diferencia es que en una lista de c´odigo se supone que el
primer elemento es un operador y el resto de elementos sus argumentos.
Pero si Lisp encontrase la segunda lista buscar´ıa en vano el operador hola
y se emitir´ıa un mensaje de error. Para evitar esto, se indica a Lisp con un
ap´ostrofo que la lista que viene a continuaci´on contiene s´olo datos:
8
’(hola adios luna)
Dos funciones son u
´tiles: (list ) y (cons ). La primera crea una lista
a partir de los argumentos que se le ofrezcan:
> (list 1 2 3 4)
> (1 2 3 4)
La segunda toma como argumentos un objeto y una lista, y a˜
nade el
objeto al principio de la lista. El objeto, a su vez, puede ser una lista:
>
>
>
>
(cons 3 ’(2 1 0))
(3 2 1 0)
(cons ’(1 2) ’(3 4 5))
( (1 2) 3 4 5)
Como ejemplo de uso, escribimos una funci´on que toma una lista de
n´
umeros como argumento y devuelve otra cuyos elementos son los elementos
de la lista original multiplicados por 2:
(defun duplica (lista)
(if (null lista)
nil
(cons (* 2 (firs lista)) (duplica (rest lista)))))
13.
Nivel 11
Operar sobre listas es un nuevo nivel de abstracci´on, puesto que se opera
sobre ellas con independencia de qu´e contengan: si n´
umeros, cadenas, combinaciones de ambos u otras listas. Por ejemplo, la longitud de una lista es
0 si la lista est´a vac´ıa y 1 m´as la longitud de la lista que resulta de saltar el
primer elemento en caso contrario. Esta definici´on es naturalmente recursiva,
y se puede formaliza as´ı:
(defun longitud (lista)
(if (null lista) 0 (+ 1 (longitud (rest lista)))))
La funci´on (first ) aplicada a una lista da como resultado el primer
elemento. La funci´on (rest ) aplicada a una lista da como resultado la
sublista formada por el segundo y sucesivos elementos.
Ahora podemos probar:
9
> (longitud ’(1 2 3 (4 5)))
> 4
Volviendo a la funci´on (media ) definida anteriormente, es natural escribir una nueva versi´on que calcule la media no de dos n´
umeros, sino de una
lista de longitud arbitraria. Para ello ser´a preciso calcular la suma de una
lista. Si la lista est´a vac´ıa, la suma de sus elementos es 0, si no, es el valor
del primer elemento m´as la suma del resto de elementos. Esta definici´on es
tambi´en naturalmente recursiva, y se escribe inmediatamente:
(defun suma (lista)
(if (null lista) 0 (+ (first lista) (suma (rest lista)))))
Y ahora:
(defun media (lista) (/ (suma lista) (longitud lista)))
Probemos:
> (media ’(1 2 3 4 3 2 1))
> 16/7
Un ejemplo m´as. Para calcular el m´aximo de una lista, suponiendo que
no est´a vac´ıa:
(defun maxlista (lista)
(if (null (rest lista))
(first lista)
(if (> (first lista) (maxlista (rest lista)))
(first lista)
(maxlista (rest lista)))))
En lenguaje claro: si la lista contiene un s´olo elemento, ´el es el m´aximo,
si no, puede suceder que el primer elemento sea mayor que el m´aximo del
resto, y entonces es ´el el m´aximo, o puede que no, y entonces el m´aximo es
el m´aximo del resto de la lista. La recursi´on parece magia.
10
14.
Nivel 12
Es una operaci´on frecuente aplicar una funci´on sobre todos los elementos de una lista. En una secci´on anterior hemos escrito una funci´on llamada
(duplica ). En este nivel, generalizamos la idea y nos proponemos aplicar
una funci´on cualquiera a los elementos de una lista. Esto implica que las funciones puedan pasarse como par´ametros. Para completar, una funci´on deber´ıa
tambi´en ser capaz de devolver como resultado otra funci´on. Por ejemplo, la
suma de una serie es un ejemplo de lo primero: la funci´on encargada de la
suma ha de recibir como par´ametro la funci´on f que se aplica a cada n en el
intervalo desde n = a a n = b (f (n) es el t´ermino general). Un ejemplo de lo
segundo es la derivada: dada una funci´on, se obtiene otra. Cubriremos estos
aspectos en este nivel.
Lisp incorpora tres funciones que pueden tomar a otras funciones como
argumento. La primera es (funcall ), que toma una funci´on y los argumentos sobre los que se aplicar´a esa funci´on:
> (funcall #’+ 2 3 4 5)
> 15
La segunda es (apply ) que toma una funci´on y una lista. La funci´on se
aplicar´a a los elementos de la lista:
> (apply #’+ ’(2 3 4 5 6))
> 21
La tercera es (mapcar ), que toma una funci´on de una variable y una
lista y aplica la funci´on a cada elemento de la lista, obteniendo otra:
> (mapcar #’sqrt ’(2 3 4 5 6))
> (1.4142135 1.7320508 2 2.236068 2.4494898)
Aplicaremos estas ideas para escribir una funci´on capaz de calcular la
integral num´erica por la f´ormula de los trapecios. Esta funci´on tomar´a como
argumentos: 1) la funci´on que se quiere integrar; 2) el extremo inferior a de
la integral; 3) el extremo superior b de la integral y 4) el n´
umero de puntos
N >= 2 que se tomar´an para la integral, incluyendo a a y b.
En primer lugar, escribiremos una funci´on que devuelva una lista con
valores igualmente espaciados, entre a y b, de N elementos:
(defun valores (a b N)
(if (= N 2)
(cons a (cons b nil))
(cons a (valores (+ a (/ (- b a)(- N 1))) b (- N 1)))))
11
A continuaci´on, la funci´on (mapcar ) producir´a una lista de valores, que
ser´an los valores que toma la funci´on que se desea integrar en cada punto de
la lista primera. Una vez hecho esto la integral se obtiene enseguida, pues es
el producto de (b − a)/(N − 1) por: la suma de la lista menos la semi-suma
de los extremos. Sabemos c´omo sumar una lista, y sabemos c´omo obtener el
primer elemento. ¿C´omo obtener el u
´ltimo? Con la funci´on (last ). Pero, al
contrario que (first ), que devuelve el primer elemento de la lista, (last
) devuelve una lista que contiene s´olo el u
´ltimo elemento. Por tanto, (car
(last lista)) devuelve el u
´ltimo elemento. Ahora, la funci´on que suma la
lista completa y le resta la semi-suma de los elementos primero y u
´ltimo es
trivial:
(defun sumatorio (lista)
(- (suma lista)
(/ (+ (first lista) (car (last lista))) 2.0)))
Con todo esto, es posible escribir la funci´on que integra num´ericamente
una funci´on cualquiera entre dos extremos:
(defun trapecios (f a b N)
(* (/ (- b a) (- N 1))
(sumatorio (mapcar f (valores a b N)))))
Ahora podemos calcular la integral de una funci´on pas´andola como argumento a (trapecios ). Por ejemplo:
> (defun cuadrado (x) (* x x))
> (trapecios #’cuadrado 1.0 10.0 1000)
> 332.99918
15.
Segundo intermedio
Una macro es un programa que escribe un programa, que despu´es se
ejecuta. El alcance de las macros en Lisp va much´ısimo m´as lejos de lo que
pretendemos en estas notas, pero a t´ıtulo ilustrativo, escribiremos una macro
que tome como argumento una funci´on y escriba la expresi´on que despu´es se
ejecutar´a para darnos la integral:
> (defmacro trapmac (f a b N)
‘(* (/ (- ,b ,a)(- ,N ,1))
(sumatorio (mapcar #’,f (valores ,a ,b ,N)))))
12
despu´es de lo cual es posible escribir
> (trapmac cuadrado 1.0 2.0 10)
> 2.3353913
16.
Nivel 12, segunda parte
La segunda parte de este nivel consiste en ver que una funci´on puede
devolver otra funci´on. La funci´on devuelta es creada en el interior de otra,
y devuelta como resultado, de la misma forma que se calculan y devuelven
n´
umeros. La sintaxis es casi trivial:
(defun f (x)
#’(lambda (n) (+ x n)))
f es una funci´on que toma un argumento x y que devuelve otra funci´on
que toma un argumento n y devuelve la suma (+ x n). lambda es un nombre
especial con el que Lisp se refiere a funciones an´onimas. A veces se quiere
hacer un peque˜
no c´alculo dentro de una funci´on y para eso se usa otra cuya
utilidad es local: no vale la pena ponerle un nombre especial porque no va
a ser llamada desde otras instancias. Entonces se usa la forma lambda. De
manera que si se llama (f 3), el resultado no ser´a un n´
umero, sino una
funci´on, que a su vez requerir´a de otro argumento y dar´a un resultado.
>
>
>
>
>
>
(funcal (f 3) 5)
8
(apply (f 3) ’(5))
8
(mapcar (f 3) ’(1 2 3 4))
(4 5 6 7)
De manera que, en este punto, las funciones realizan c´alculos donde
pueden manipular no s´olo n´
umeros, sino tambi´en otras funciones, y pueden
tomarlas como argumento y devolverlas como resultado.
17.
Nivel 13
Este es el u
´ltimo nivel que vamos a presentar aqu´ı, pero no el u
´ltimo al
que permite acceder Lisp. De hecho, Lisp est´a hecho de tal forma que puede
generar nuevas abstracciones a partir de otras anteriores, del nivel que se
13
quiera. Trataremos ahora de la forma en que es posible combinar funciones
en una sola unidad. Hasta ahora, hemos escrito funciones individuales, o funciones que usaban funciones anteriores, aut´onomas. Por ejemplo, en el programa que calcula la integral de una funci´on por el m´etodo de los trapecios,
hemos usado las funciones (sumatorio ) y (valores ), que fueron definidas
antes. A su vez, (sumatorio ) hace uso de (suma ). Puede ser que todas
estas funciones tengan utilidad en s´ı mismas o puede que no. Por ejemplo
(suma ) puede ser u
´til fuera del contexto del c´alculo de la integral, pues hace
la operaci´on gen´erica de sumar los elementos de una lista. Pero (sumatorio
) no parece tener otra utilidad que la de servir a (trapecios ).
Entonces, se plantea la necesidad, o al menos la conveniencia, de poder
aglutinar junto con una determinada funci´on, todas aquellas funciones auxiliares que no tienen otro prop´osito que el de ayudar dentro de su contexto,
y no tienen por qu´e ser conocidas fuera de ese contexto. Estas funciones
auxiliares se declaran en Lisp mediante (labels ). Vamos a reescribir la
funci´on (trapecios ) de tal forma que las funciones en que se apoya sean
parte suya y s´olo conocidas por ella. Antes de eso, un par de ejemplos para
aclarar la sintaxis:
(defun f (n)
(labels ( (a (n) (+ n 4))) (* n (a 7))))
Aqu´ı, inmediatamente despu´es de declarar nuestra intenci´on de crear una
funci´on de nombre f que tomar´a como argumento un n´
umero n, abrimos un
´ambito con (labels . Este ´ambito se extiende hasta el pen´
ultimo par´entesis,
que cierra (labels , justo antes del u
´ltimo, que cierra (defun . Dentro de
ese ´ambito, y conocida s´olo ah´ı, se declara la funci´on a. Obs´ervese que no
es preciso usar (defun ). Esa funci´on hace un peque˜
no c´alculo, y servir´a de
auxiliar a f. El c´alculo es simple: toma su argumento y le suma 4. Una vez
terminada de definir a, se pasa a definir f, que toma su argumento y lo
multiplica por el resultado de hacer (a 7). Obs´ervese que hemos llamado n
tanto al argumento de f como al de a. Pero no son la misma n. La primera
es el argumento de f, y la segunda es el argumento de a. Son dos variables
distintas, pero tienen el mismo nombre. (a ) es llamada con argumento 7:
cuando se realiza la llamada, se calcula (a ) con n=7. Cuando se llama a f,
se llama con el argumento que se quiera. Por supuesto, tambi´en podr´ıamos
haber escrito:
(defun f (n)
(labels ( (a (x) (+ x 4))) (* n (a 7))))
14
(labels ) puede contener m´as de una funci´on, y esas funciones locales
pueden llamarse entre s´ı, e incluso pueden ser recursivas. Un segundo ejemplo,
en el que usamos una indentaci´on diferente para hacer m´as claro el ´ambito
determinado por (labels ):
(defun g (n)
(labels (
(a (x) (+ x 5))
(b (y) (* y (a 4)))
)
(* n (a 1) (b 2))
)
)
y ahora
> (mapcar #’g ’(1 2 3 4))
> (108 216 324 432)
La funci´on g se apoya en a y b, y b se apoya a su vez en a. Ni a ni b
son conocidas fuera de g, y por tanto no pueden ser llamadas: si se intenta, se obtendr´a un mensaje de error. Ahora, podemos reescribir la funci´on
(trapecios ). Usamos una indentaci´on ”generosa” para hacer expl´ıcito cada
bloque.
(defun trapecios (f a b N)
(labels (
(suma (lista)
(if (null lista)
0
(+ (first lista) (suma (rest lista)))
)
)
(valores (a b N)
(if (= N 2)
(cons a (cons b nil))
(cons a (valores (+ a (/ (- b a)(- N 1))) b (- N 1)))
)
)
(sumatorio (lista)
(- (suma lista)
(/ (+ (first lista) (car (last lista))) 2.0)
15
)
)
)
(* (/ (- b a) (- N 1))
(sumatorio (mapcar f (valores a b N)))
)
;;
;; trapecios
;;
)
)
Ahora se puede calcular:
> (trapecios #’cuadrado 0.0 1.0 1000)
> 0.33333328
18.
Conclusi´
on
Aunque hemos s´olo ara˜
nado la superficie de Lisp, ha sido suficiente para
hacer el recorrido t´ıpico que lleva a un estudiante desde la escuela primaria
a la Universidad: desde la suma de peque˜
nas cantidades al an´alisis funcional,
donde los elementos que se manipulan no son n´
umeros, sino funciones. Hemos
visto c´omo cada nuevo nivel de abstracci´on puede formalizarse mediante un
lenguaje de programaci´on, y c´omo esa formalizaci´on tiene un valor a˜
nadido:
no s´olo se expresan conceptos en un nuevo lenguaje, lo que obliga a expresarlos de forma clara y precisa, sino que como resultado se adquieren herramientas y utilidades duraderas. Como ejemplo trivial, una vez que se dispone de
la funci´on (media ) se tiene la herramienta para calcular medias de listas
de n´
umeros de cualquier tipo y longitud arbitraria. (media ) puede ser u
´til
en muchos contextos y, sobre todo, puede usarse de forma independiente o
como una pieza para construir funciones m´as complejas.
En el camino, tambi´en se adquieren formas de pensar u
´tiles. Por ejemplo,
se aprende a plantear problemas usando recursividad. Dado el car´acter de
estas notas, no hemos explorado las abstracciones de datos, con lo cual se
podr´a organizar y manipular la informaci´on con estructuras naturales para
cada tipo de problema.
Para finalizar, tenemos que citar la obra Structure and Interpretation of
Computer Programas, de Harold Abelson, Gerald Jay Sussman y Julie Sussman. Es una obra maestra y una fuente de placer intelectual. El lector interesado puede acudir a otras muchas obras: Practical Common Lisp, de Peter
Seibel; Successful Lisp: How to Understand and Use Common Lisp, de David
B. Lamkins; Land of Lisp, de Conrad Barski M. D.; Common Lisp: a Gentle Introduction to symbolic computation, de David S. Touretzky; Common
16
Lisp, de Paul Graham. Omitimos otros libros, de gran calidad pero muy alto
nivel. Por supuesto, hay muchas p´aginas sobre Lisp que pueden consultarse
tambi´en. 1
1
Sugerencias, erratas: [email protected]
17