Académique Documents
Professionnel Documents
Culture Documents
Esta sección trata acerca del lenguaje de programación Lisp, (que toma su nombre de
procesamiento de listas, en inglés List Processing), presentando algunos conceptos básicos acerca del
manejo de símbolos y programación básica en Lisp.
Dentro una computadora, todo es una cadena de dígitos binarios, ceros y unos, llamados bits.
Desde este punto de vista, las sucesiones de bits se pueden interpretar como una representación para
los dígitos decimales. Sin embargo, desde otra perspectiva, éstas se pueden considerar como un código
para objetos semejantes a palabras y oraciones.
• En Lisp los elementos fundamentales formados a partir de bits son objetos semejantes a palabras y
se denominan átomos.
arco
dintel
poste1
poste2
• Los grupos de átomos forman objetos parecidos a las oraciones y se denominan listas. Las listas
pueden agruparse para formar a su vez listas de nivel superior (listas de listas o listas anidadas).
Las siguientes son expresiones simbólicas que pueden tener sentido para un ingeniero civil:
'(trabe1
(debe-estar-sostenido-por columna1)
(debe-estar-sostenido-por columna2))
Las expresiones simbólicas también sirven para representar listas de operaciones, tanto
como si fuesen datos, así como expresiones que se pueden evaluar. Por ejemplo la siguiente expresión:
Es una lista compuesta por tres elementos, cada uno de ellos son a su vez operaciones
algebraicas. Pero si se le suministra al compilador tal y como se muestra, Lisp la entenderá como un
conjunto de datos y no devolverá cálculo alguno. La siguiente expresión sí lo hará:
La anterior es una lista anidada cuyo primer elemento es la función eval, la cual toma como
argumento al siguiente elemento de la lista, que a su vez es otra lista, cuyo primer elemento es la función
first, y a su vez toma como argumento a la lista de operaciones algebraicas definidas con anterioridad.
El resultado de esta expresión es la evaluación del primer elemento de dicha lista, y da como resultado
10.
Un programa que maneja símbolos se vale de expresiones simbólicas para trabajar con datos y
procedimientos, igual que las personas emplean lápiz, papel y algún lenguaje para hacer lo mismo. Un
programa que maneja símbolos, suele tener procedimientos para reconocer expresiones simbólicas
particulares, separar elementos de ellas y formar nuevas expresiones. Por ejemplo, la siguiente expresión
construye una lista que representa una operación algebráica:
La anterior genera como resultado (+ 1 2 3 4), que como se ha visto anteriormente, es una
expresión que puede evaluarse en un momento dado.
A continuación se muestran dos ejemplos de expresiones simbólicas más complejas que tratan de
representar conocimiento. Los paréntesis indican el principio y el final de las listas. El primer ejemplo, es
una descripción de una estructura formada por bloques de juguete, el segundo, la descripción de una
universidad.
'(arco (partes dintel poste1 poste2)
(dintel debe-estar-sostenido-por poste1)
(dintel debe-estar-sostenído-por poste2)
(dintel es-un-tipo-de cuña)
(poste1 es-un-tipo-de ladrillo)
(poste2 es-un-tipo-de ladrillo)
(poste1 no-debe-tocar poste2)
(poste2 no-debe-tocar postel))
(identifica6
((? animal) tiene dientes puntiagudos)
((? animal) tiene garras)
((? animal) tiene ojos al-frente)
((?animal) es carnívoro))
Acabamos de expresar de otra forma la idea de que un animal con dientes puntiagudos, garras y
ojos al frente probablemente es un carnívoro. Para emplear tal regla, un programa debe aislarla,
determinar si los patrones que involucran dientes, garras y ojos son compatibles con la información que
de algún animal específico se tenga en la base de datos y, en caso afirmativo, agregar a la base de datos
la conclusión de que el animal es carnívoro. Todo esto se realiza mediante el manejo de símbolos.
• Expertos en resolver problemas. Uno de los primeros programas en Lisp resolvía problemas de
cálculo como los que resuelven los estudiantes universitarios de primer año. Otro de estos
programas resolvía problemas de analogías geométricas como los que se incluyen en pruebas de
inteligencia. Desde entonces se han creado nuevos programas para configurar computadoras,
diagnosticar infecciones de la sangre, entender circuitos electrónicos, evaluar formaciones
geológicas, planear diversiones, planificar fábricas, disertar cerraduras, inventar matemáticas
interesantes y mucho más. Todos ellos escritos en Lisp.
• Razonamiento con sentido común. Gran parte del pensamiento humano parece implicar una
pequeña cantidad de razonamiento y una gran cantidad de conocimiento. La representación del
conocimiento implica elegir un vocabulario de símbolos y establecer algunos acuerdos en cuanto a
cómo disponerlos. Las buenas representaciones hacen que las cosas correctas resulten explícitas.
Lisp es un lenguaje en el cual se realiza la mayoría de las investigaciones acerca de la
representación.
• Aprendizaje. Muchos han sido los trabajos que se han realizado acerca del aprendizaje de
conceptos por computadora y ciertamente la mayor parte de lo realizado también se apoya en los
avances logrados en las técnicas de representación del conocimiento. Una vez más, Lisp domina
en esta área.
• Interfaces en lenguaje natural. Existe una creciente demanda de programas que interactúen con
las personas en inglés u otros lenguajes (a los cuales les denominaremos "naturales"). Se han
construido sistemas prácticos para recuperar información de bases de datos, que de otra forma son
difíciles de usar.
• Lenguaje y visión. Se ha probado que es sumamente difícil entender la manera como escuchamos
y vemos. Parece que aún no logramos conocer lo suficiente acerca de cómo el mundo físico limita
lo que percibimos por medio del tambor de nuestro oído o nuestra retina. Sin embargo, se ha
progresado en este sentido y gran parte de este avance se ha realizado a través de Lisp, aunque
se requiere una buena parte (le programación orientada a la aritmética. A decir verdad, Lisp no
ofrece ventajas especiales para hacer programación orientada a la aritmética, pero tampoco
plantea desventajas en ese aspecto.
En consecuencia, la gente que desea saber de la inteligencia utilizando las computadoras necesita
de una manera u otra entender lenguajes para procesamientos simbólico, tales como los basados en
Lisp.
• Programación de aplicaciones. Los programadores talentosos se han dado cuenta que Lisp puede
incrementar enormemente su productividad al permitirles escribir programas extensos mucho más
rápido y a un costo mucho menor. Esto puede tener efectos notables en la forma en que los
programas extensos se desarrollan. Anteriormente se empezaba por destinar varios años para la
especificación, luego se asignaban varios años más para la programación, y esto acababa en
sistemas que producían usuarios decepcionados y malhumorados. Actualmente se construyen
prototipos en pocos meses, con la evolución simultánea de la especificación y la programación, así
como de los desarrollos logrados en ambientes de programación gráficos, y con la constante
participación de los usuarios en la producción del resultado final.
• Programación de sistemas. Las máquinas LISP fueron estaciones de trabajo de gran potencia,
programadas enteramente en Lisp. El sistema operativo, los programas para uso general, los
editores, los compiladores y los intérpretes están todos escritos en Lisp, lo que demuestra el poder
y la versatilidad de este lenguaje. Con la evolución de la tecnología de computadoras que
actualmente se ha logrado, es posible utilizar todo el poder de aquellas estaciones de trabajo, en
computadoras de escritorio.
• Educación en ciencias de la computación. Lisp facilita la abstracción de procedimientos y de datos,
dando énfasis con ello a (los ideas de suma importancia en la programación. Y en vista de que
LISP es un lenguaje orientado a la implantación, resulta adecuado para construir intérpretes y
compiladores para una amplia gama de lenguajes, incluyendo el propio Lisp.
Existen numerosos lenguajes de programación, de los cuales, sólo unos cuantos se destinan al
manejo de símbolos. De éstos, Lisp es el más usado en el continente americano. Una vez que Lisp se
entiende, la mayoría de los otros lenguajes para manejo de símbolos son relativamente fáciles de
aprender.
¿Por qué Lisp ha llegado a ser el lenguaje más utilizado para el manejo de símbolos, y últimamente para
muchas cosas más? A continuación se dan una serie de argumentos, cada uno con sus propios
partidarios:
• Interacción. Lisp está diseñado para programar en una terminal con respuesta rápida. Todos los
procedimientos y los datos pueden presentarse o modificarse a voluntad del usuario.
• Ambiente. Lisp fue utilizado por una comunidad que requirió y obtuvo lo mejor en herramientas
para edición y depuración. Durante más de dos décadas, en los centros de inteligencia artificial
más importantes del mundo, se han creado complejos ambientes de programación en torno a Lisp,
creando una combinación inmejorable para escribir programas extensos y complicados.
• Uniformidad. Los procedimientos y datos en Lisp tienen la misma forma. Un programa en Lisp
puede usar otro programa como dato, incluso puede crear otro programa y usarlo.
Una razón por la que Lisp es fácil de aprender, es que su sintaxis es sencilla. Como dato curioso,
la sintaxis actual de Lisp tiene raíces extrañas. El inventor de Lisp, John McCarthy, en un principio usó
una especie de Lisp antiguo, que es casi tan difícil de leer como el inglés antiguo. Sin embargo, en
determinado momento él quiso utilizar Lisp para hacer un trabajo de matemáticas donde se requería que
tanto los procedimientos como los datos tuvieran la misma forma sintáctica. La forma resultante de Lisp
se dedujo rápidamente.
El Lisp que se usa en este libro es COMMON LISP, dado que es moderno, estándar, y está
ampliamente disponible. COMMON Lisp también es el estándar aceptado para uso comercial. El lector
no deberá dejarse confundir por referencias a otros lenguajes extrañamente llamados Lisp. La mayoría
de ellos son dialectos obsoletos o COMMON Lisp con extensiones de alguna compañía en especial.
En efecto, esto fue cierto durante algún tiempo. Este problema se ha corregido mediante el
desarrollo de programas adecuados que permiten traducir el material producido por los programadores en
instrucciones, que pueden ser ejecutadas por una computadora de manera directa y sin más
descomposiciones. Dicho de otra forma, el problema se ha corregido mediante el diseño de eficaces
compiladores de Lisp.
Lisp es lento.
También esto fue cierto durante algún tiempo. Al principio, Lisp se usaba exclusivamente en
investigaciones donde la interacción era el aspecto fundamental, en tanto que a la alta velocidad para
depurar los programas orientados a aplicaciones se le confería una menor importancia. En la actualidad,
se han desarrollado excelentes compiladores de Lisp para apoyar la creciente demanda comercial para
programas en Lisp.
En realidad este no es un mito. Algunos programas son grandes, pero esto se debe a que Lisp
permite crear programas lo suficientemente grandes como para realizar una gran cantidad de tareas.
Hace unos cuantos años, para empezar con Lisp se necesitaba un millón de dólares. Hoy día, se
pueden tener excelentes sistemas Lisp para computadoras personales que cuestan sólo unos cuantos
cientos de dólares (que ya por si misma es una gran diferencia), y aun los sistemas complejos que
operan en estaciones de trabajo orientadas a LISP tienen un costo inferior a los cien mil dólares.
Lisp es difícil de leer y depurar por todos los paréntesis que requiere.
En realidad, el problema de los paréntesis desaparece en cuanto se aprende a usar un editor para
programas en Lisp que permite poner las cosas en la pantalla de edición, de manera apropiada. Nadie
encontraría especialmente claro el siguiente ejemplo:
Los editores orientados a Lisp facilitan la producción de versiones con formato, a medida que se
escriben.
Aclaraciones
Antes de seguir adelante con esta introducción, hay que hacer algunas aclaraciones.
1) Operaciones
Ventajas: Las expresiones cotidianas (al menos como comúnmente las aprendemos en las
escuelas) son de esta manera. Ej. 2 + 3, 4 + 4, 2 * 2 , 2 / 3, etc.
Desventajas: No son muy adecuadas para la computadora, pues la máquina espera primero que se le
proporcionen las acciones y luego los datos. Normalmente en los lenguajes como Fortran
o Basic, esto se soluciona mediante un proceso de pre-compilación o "parsing."
Ventajas: Las expresiones computacionales están más de acuerdo con la manera en que la
computadora las maneja. Se tiene la posibilidad de aplicar un operador a varios
elementos. Ej. en la expresión "+ x y z a b c" el operador "+" se aplica a todos los
elementos ella y solamente se declara una vez. En notación infija, la expresión anterior se
tendría que escribir de la siguiente manera "x + y + z + a + b + c"
Desventajas: No es la manera común en que las personas manejan las operaciones ni matemáticas ni
simbólicas.
Lisp estuvo pensado desde un principio para ser programado con notación prefija. Esto no debe de
ahuyentar al lector. Con un poco de práctica, es común que el estudiante del lenguaje se acostumbre a
ella. Incluso después de ello, muchas personas prefieren este tipo de notación, por sobre otras
encontradas en lenguajes tales como Pascal, Basic, o FORTRAN.
2) El uso de paréntesis
Una de las características más obvias de Lisp son los paréntesis. Aunque de primera instancia los
programas escritos en este lenguaje se asemejan a un conjunto de paréntesis sin mucho código de por
medio, estos paréntesis son necesarios para delimitar el "ámbito de aplicabilidad" de una operación. Por
ejemplo, en la expresión (+ n 1), el operador "+" sólo se aplica a "n" y a "1". Así mismo, en la
expresión (or (= n 0) (= n l)), el operador "or" sólo aplica al resultado de "(= n 0)" y "(= n
l)".
Casi todos los lenguajes de programación tienen alguna u otra forma delimitación del ámbito de
operaciones. En C/C++ lo son los corchetes "{...}", los paréntesis rectangulares "[...]", o el punto y
coma ";". En Basic, Visual Basic y demás versiones de dicho lenguaje, lo son el retorno de carro (o línea
nueva), los "If-Then-ElseIf-Else-Endif", los "Do-EndDo", etc. Lisp utiliza únicamente los
paréntesis para delimitar. Esto puede verse como una ventaja, si se considera la economía en notación.
3) Programación funcional
Lisp es un lenguaje "funcional". Esto quiere decir que todas las operaciones a realizar se
consideran "funciones" que devuelven un resultado siempre. Por ejemplo, (+ 1 2 3)se considera como
una función que devuelve "6", así como (first '(a b c d e)) se considera como una función que
devuelve "a". Para el compilador, el primer elemento de una lista siempre se considera una función,
cuyos argumentos son el (los) siguiente(s) elemento(s). Si la función está definida ya sea por el usuario o
porque sea parte del lenguaje, Lisp la tratará de evaluar siempre. Si uno desea suministrar una lista o un
átomo como si fuese solamente dato, la debe de suministrar antecedida por un apóstrofe sencillo ( ' ).
Ejemplo:
a d
c b e
...etc. Para obtener las coordenadas cartesianas de un nodo basta con escribir la directiva get, el
nombre del nodo y el argumento coordenadas. Ejemplo:
...etc.
Para obtener la distancia entre dos nodos, es necesario definir una función que utilice las
coordenadas cartesianas de ambos nodos para calcularla. La distancia entre dos puntos está definida
(para superficies planas) como:
[( X 2 − X 1 ) + (Y2 − Y1 )
2 2
]
Para definir una función, se debe de observar de cerca los componentes que la conforman. Esto es
necesario para cualquier lenguaje de programación. Para el caso particular de la función anterior, esta
está compuesta por cuatro operaciones que se deben de realizar a los componentes de los pares
cartesianos de asignados a cada nodo. Con anterioridad se definieron las coordenadas cartesianas de
cada nodo como una lista compuesta por dos números, de los cuales se sobreentiende que el primero
corresponde a X y el segundo a Y.
En Lisp las funciones se definen con la directiva defun, el nombre que se le quiere asignar a ella y
entre paréntesis los argumentos que utiliza. La función que nos proponemos construir entonces recibirá
como argumentos a dos nodos, a los cuales llamaremos nodo-1 y nodo-2.
El paso siguiente será que la función busque los pares cartesianos que corresponden a los dos
nodos que se le suministraron. Cabe hacer la aclaración que todos los resultados de las operaciones que
se realizan en un ámbito, no permanecen en la memoria para ser utilizados en otros lugares de una
función. Para nuestro caso en particular, se utiliza la directiva get de nuevo, pero se necesita que el
resultado que se devuelva quede almacenado en un lugar de la memoria, por lo que definiremos dos
variables propias de la función para este objetivo. A estas variables "temporales" las llamaremos
"coordenadas-1 " y "coordenadas-2 ". La directiva let permite tanto definir como asignar valores a
las variables dentro de una función. La sintaxis de la directiva let es de la siguiente manera:
Tanto "coordenadas-1 " y "coordenadas-2 " recibirán sendas listas que representan cada una
de las coordenadas de cada nodo. Hecho lo anterior, nuestra función quedaría de la siguiente manera:
Para obtener las coordenadas X, utilizaremos la directiva first, que devuelve al primer elemento de
una lista. Para las coordenadas Y, utilizaremos la directiva second, que obtiene al segundo elemento de
una lista.
(first coordenadas-1)
(first coordenadas-2)
(second coordenadas-1)
(second coordenadas-2)
Respectivamente obtendremos las diferencias entre las coordenadas X e Y, para cada pareja de
coordenadas:
(- (first coordenadas-1)
(first coordenadas-2) )
(- (second coordenadas-1)
(second coordenadas-2) )
Para exponenciación, Lisp posee la directiva expt, cuya sintaxis es la siguiente: (expt valor
exponente) donde valor y exponente son números reales. Para nuestro caso:
Una vez definidas, las funciones se pueden llamar desde cualquier punto de un programa, o
inclusive dentro de otra función, pero en lugar de proporcionar los parámetros dentro de los paréntesis,
tanto el nombre de la función como sus argumentos se proporcionan con una lista. Ejemplo:
Ejercicios
Elabora un programa que haga lo mismo que el anterior, pero para ternas (X Y Z), con la siguiente
información:
nodos: vecinos (X Y Z)
g: h j (1 2 4)
h: g i j (3 2 1)
i: h k m (9 -1 10)
k: i (5 2 4)
j: g h m (-2 1 0)
m: i j z (0 0 -7)
z: m (-1 -1 -1)