Docsity
Docsity

Prepara tus exámenes
Prepara tus exámenes

Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity


Consigue puntos base para descargar
Consigue puntos base para descargar

Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium


Orientación Universidad
Orientación Universidad


Introduccion al LISP, Apuntes de Ingeniería Aeronáutica

Asignatura: Introduccion a la inteligencia artificial, Profesor: , Carrera: Gestió Aeronàutica, Universidad: UAB

Tipo: Apuntes

Antes del 2010

Subido el 21/07/2008

garlak
garlak 🇪🇸

4.1

(40)

33 documentos

1 / 17

Toggle sidebar

Esta página no es visible en la vista previa

¡No te pierdas las partes importantes!

bg1
Introducción a Lisp
Dr. Alejandro Guerra-Hernández
Departamento de Inteligencia Artificial
Universidad Veracruzana
Facultad de Física e Inteligencia Artificial
Sebastián Camacho No 5, Xalapa, Ver., México 91000
www.uv.mx/aguerra
8 de diciembre de 2006
El objetivo de esta sesión es que puedan programar en Lisp tan pronto como sea
posible. Al final de la misma, conocerán lo suficiente de Lisp como para comenzar a
escribir programas.
1. Expresiones
Es partícularmente cierto que la mejor forma de aprender Lisp es usándolo, porque
se trata de un lenguaje interactivo. Cualquier sistema Lisp, incluye una interfaz inte-
ractiva llamada top-level. Uno escribe expresiones Lisp en el top-level, y el sistema
despliega sus valores. El sistema normalmente despliega un indicador (el prompt >) de
que está esperando que una expresión sea escrita. Ej. Una de las expresiones Lisp más
simples es el entero, si escribímos el entero 1 después del prompt y tecleamos enter,
tenemos:
> 1
1
>
el sistema despliega el valor de la expresión, seguida de un nuevo prompt, indicando
que está listo para evaluar una nueva expresión. En este caso, el sistema desplegó lo
mismo que tecleamos porque los números evalúan a si mismos. Las cosas son más
interesantes cuando una expresión necesita algo de trabajo para ser evaluado. Ej. Sumar
dos números:
> (+ 2 3)
5
>
1
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff

Vista previa parcial del texto

¡Descarga Introduccion al LISP y más Apuntes en PDF de Ingeniería Aeronáutica solo en Docsity!

Introducción a Lisp

Dr. Alejandro Guerra-Hernández

Departamento de Inteligencia Artificial

Universidad Veracruzana

Facultad de Física e Inteligencia Artificial

Sebastián Camacho No 5, Xalapa, Ver., México 91000

[email protected]

www.uv.mx/aguerra

8 de diciembre de 2006

El objetivo de esta sesión es que puedan programar en Lisp tan pronto como sea posible. Al final de la misma, conocerán lo suficiente de Lisp como para comenzar a escribir programas.

1. Expresiones

Es partícularmente cierto que la mejor forma de aprender Lisp es usándolo, porque se trata de un lenguaje interactivo. Cualquier sistema Lisp, incluye una interfaz inte- ractiva llamada top-level. Uno escribe expresiones Lisp en el top-level, y el sistema despliega sus valores. El sistema normalmente despliega un indicador (el prompt >) de que está esperando que una expresión sea escrita. Ej. Una de las expresiones Lisp más simples es el entero, si escribímos el entero 1 después del prompt y tecleamos enter, tenemos:

1 1

el sistema despliega el valor de la expresión, seguida de un nuevo prompt, indicando que está listo para evaluar una nueva expresión. En este caso, el sistema desplegó lo mismo que tecleamos porque los números evalúan a si mismos. Las cosas son más interesantes cuando una expresión necesita algo de trabajo para ser evaluado. Ej. Sumar dos números:

(+ 2 3) 5

En la expresión (+ 2 3) el + es llamado el operador y los números 3 y 4 son sus argumentos. Como el operador viene al principio de la expresión, esta notación se conoce como prefija y aunque parezca extraña al principio, veremos que es uno de los mejores aspectos de Lisp. Por ejemplo, si queremos sumar tres números en notación “normal”, necesitaríamos usar dos veces el operador +: 2+ 3 +4. En Lisp sólo necesitamos agregar otro argumento. Ej. Las siguientes sumas son válidas en Lisp:

(+) 0 (+ 2) 2 (+ 2 3) 5 (+ 2 3 5) 10

Como los operadores pueden tomar un número variable de argumentos, es nece- sario utilizar los paréntesis para indicar donde inicia y donde termina una expresión. Las expresiones pueden anidarse, esto es, el argumento de una expresión puede ser otra expresión compleja. Ej.

(/ (- 7 1)(- 4 2)) 3

En español esto corresponde a siete menos uno, dividido por cuatro menos dos. Estética minimalista, esto es todo lo que hay que decir sobre la notación en Lisp. Toda expresión Lisp es un átomo, como 1, o bien es una lista que consiste de cero o más expresiones delimitadas por paréntesis. Como veremos, código y datos usan esta notación en Lisp.

2. Evaluación

Veamos más en detalle como las expresiones son evaluadas para desplegar su valor en el top-level. En Lisp, + es una función y (+ 2 3) es una llamada a la función. Cuando Lisp evalúa una llamada a alguna función, lo hace en dos pasos:

  1. Los argumentos de la llamada son evaluados de izquierda a derecha. En este caso, los valores de los argumentos son 2 y 3, respectivamente.
  2. Los valores de los argumentos son pasados a la función nombrada por el opera- dor. En este caso la función + que regresa 5.

Si alguno de los argumentos es a su vez una llamada de función, será evaluado con las mismas reglas. Ej. Al evaluar la expresión (/ (- 7 1) (- 4 2)) pasa lo siguiente.

  1. Lisp evalúa el primer argumento de izquierda a derecha (- 7 1). 7 es evaluado como 7 y 1 como 1. Estos valores son pasados a la función - que regresa 6.

Observen que un sólo quote protege a toda la expresión, incluidas las expresiones en ella. Se puede construir listas usando el operador list que es una función, y por lo tanto, sus argumentos son evaluados. Ej.

(list ’mis (+ 4 2) "colegas") (MIS 6 COLEGAS)

Estética minimalista y pragmática, observen que los programas Lisp se representan como listas. Si el argumento estético no bastará para defender la notación de Lisp, esto debe bastar –Un programa Lisp puede generar código Lisp! Por eso es necesario quote. Si una lista es precedida por el operador quote, la evaluación regresa la misma lista, en otro caso, la lista es evaluada como si fuese código. Ej.

(list ’(+ 2 3) (+ 2 3)) ((+ 2 3) 5)

En Lisp hay dos formas de representar la lista vacia, como un par de paréntesis o con el símbolo NIL. Ej.

() NIL NIL NIL

4. Operaciones con listas

La función cons construye listas. Si su segundo argumento es una lista, regresa una nueva lista con el primer argumento agregado en el frente. Ej.

(cons ’a ’(b c d)) (A B C D) (cons ’a (cons ’b nil)) (A B)

El segundo ejemplo es equivalente a:

(list ’a ’b) (A B)

Las funciones primitivas para accesar los elementos de una lista son car y cdr. El car de una lista es su primer elemento (el más a la izquierda) y el cdr es el resto de la lista (menos el primer elemento). Ej.

(car ’(a b c)) A (cdr ’(a b c)) (B C)

Se pueden usar combinaciones de car y cdr para accesar cualquier elemento de la lista. Ej.

(car (cdr (cdr ’(a b c d)))) C (caddr ’(a b c d)) C (third ’(a b c d)) C

5. Valores de verdad

En Lisp, el símbolo t es la representación por default para verdadero. La represen- tación por default de falso es nil. Ambos evaluan a si mismos. Ej. La función listp regresa verdadero si su argumento es una lista:

(listp ’(a b c)) T (listp 34) NIL

Una función cuyo valor de regreseo se intérpreta como un valor de verdad (ver- dadero o falso) se conoce como predicado. En lisp es común que el símbolo de un predicado termine en p. Como nil juega dos roles en Lisp, las funciones null (lista vacía) y not (negación) hacen exactamente lo mismo:

(null nil) T (not nil) T

El condicional (estructura de control) más simple en Lisp es if. Normalmente to- ma tres argumentos: una expresión test, una expresión then y una expresión else. La expresión test es evaluada, si su valor es verdadero, la expresión then es evaluada; si su valor es falso, la expresión else es evaluada. Ej.

(if (listp ’(a b c d)) (+ 1 2) (+ 3 4)) 3 (if (listp 34) (+ 1 2) (+ 3 4)) 7

(tercero ’(a b c d e)) C

Ahora que hemos introducido el concepto de variable, es más sencillo entender lo que es un símbolo. Los símbolos son nombres de variables, que existen con derechos propios en el lenguaje Lisp. Por ello símbolos y listas deben protegerse con quote para ser accesados. Una lista debe protegerse porque de otra forma es procesada como si fuese código; un símbolo debe protegerse porque de otra forma es procesado como si fuese una variable. Podríamos decir que la definición de una función corresponde a la versión genera- lizada de una expresión Lisp. Ej. La siguiente expresión verifica si la suma de 1 y 4 es mayor que 3:

(> (+ 1 4) 3) T

Substituyendo los números partículares por variables, podemos definir una función que verifica si la suma de sus dos primeros argumentos es mayor que el tercero:

(defun suma-mayor-que (x y z) (> (+ x y) z)) SUMA-MAYOR-QUE (suma-mayor-que 1 4 3) T

Lisp no distigue entre programa, procedimiento y función; todos cuentan como funciones y de hecho, casi todo el lenguaje está compuesto de funciones. Si se desea considerar una función en partícular como main, es posible hacerlo, pero cualquier fun- ción puede ser llamada desde el top-level. Entre otras cosas, esto significa que posible probar nuestros programas, pieza por pieza, conforme los vamos escribiendo, lo que se conoce como programación incremental (bottom-up).

7. Recursividad

Las funciones que hemos definido hasta ahora, llaman a otras funciones para hacer una parte de sus cálculos. Ej. suma-mayor-que llama a las funciones + y >. Una función puede llamar a cualquier otra función, incluida ella misma. Una función que se llama a si misma se conoce como recursiva. Ej. En Lisp la función member verifica cuando algo es miembro de una lista. He aquí una versión recursiva simplificada de esta función:

(defun miembro (obj lst) (if (null lst) nil (if (eql (car lst) obj) lst (miembro obj (cdr lst))))) MIEMBRO

El predicado eql verifica si sus dos argumentos son idénticos, el resto lo hemos visto previamente. La llamada a miembro es como sigue:

(miembro ’b ’(a b c)) (B C) (miembro ’z ’(a b c)) NIL

La descripción en español de lo que hace la función miembro es como sigue:

  1. Primero, verificar si la lista lst está vacía, en cuyo caso es evidente que obj no es un miembro de lst.
  2. De otra forma, si obj es el primer elemento de lst entonces es miembro de la lista.
  3. De otra forma, obj es miembro de lst únicamente si es miembro del resto de lst.

Traducir una función recursiva a una descripción como la anterior, siempre ayuda a entenderla. Al principio, es común toparse con dificultades para entender la recursi- vidad. En gran medida esto se debe a que usamos la metáfora equivocada de función. Tenemos la tendencia de pensar en una función como cierta forma de máquina, donde las materias primas llegan como parámetros. Parte del trabajo se da por encargo a otras funciones y el resultado final se empaqueta y se expide como el valor de la función. Si se usa esta metáfora la recursividad resulta paradójica – ¿Qué sentido tiene darse por encargo trabajo cuando uno ya está ocupado? Una metáfora más adecuada es ver a las funciones como procesos que vamos resol- viendo. Solemos usar procesos recursivos en nuestras actividades diarias. Por ejemplo, supongan a un historiador interesado en los cambios de población a través de la historia de Europa. El proceso que el historiador utilizaría para examinar un documento es el siguiente:

  1. Obtener una copia del documento que le interesa.
  2. Buscar en él la información relativa a cambios de población en Europa.
  3. Si el documento menciona otros documentos que puede ser útiles, examinarlos.

Piensen en miembro como las reglas que definen cuando algo es miembro de una lista y no como una máquina que computa si algo es miembro de una lista. De esta forma, la paradoja desaparece.

8. Leyendo y escribiendo Lisp

Estética. Si bien los paréntesis delimitan las expresiones en Lisp, un programador en realidad usa los margenes en el código para hacerlo más legible. Casi todo editor puede configurarse para verificar paréntesis bien balanceados. Ej. :set sm en el editor vi; o M-x lisp-mode en Emacs. Cualquier hacker en Lisp tendría problemas para leer algo como:

Se debe mencionar que read hace mucho más que leer caracteres, es un auténtico parser de Lisp que evalua su entrada y regresa los objetos que se hallan generado. La función pregunta, aunque corta, muestra algo que no habíamos visto antes: su cuerpo incluye más de una expresión Lisp. El cuerpo de una función puede incluir cualquier número de expresiones. Cuando la función es llamada, las expresiones en su cuerpo son evaluadas en orden y la función regresará el valor de la última expresión evaluada. Hasta el momento, lo que hemos mostrado se conoce como Lisp puro, esto es, Lisp sin efectos colaterales. Un efecto colateral es un cambio en el sistema Lisp producto de la evaluación de una expresión. Cuando evaluamos (+ 2 3), no hay efectos colatera- les, el sistema simplemente regresa el valor 5. Pero al usar format, además de obtener el valor nil, el sistema imprime algo, esto es un tipo de efecto colateral. Cuando se escribe código sin efectos colaterales, no hay razón alguna para definir funciones cuyo cuerpo incluye más de una expresión. La última expresión evaluada en el cuerpo producirá el valor de la función, pero el valor de las expresiones evaluadas antes se perderá.

10. Variables

Uno de los operadores más comunes en Lisp es let, que permite la creación de nuevas variables locales. Ej.

(let ((x 1)(y 2)) (+ x y)) 3

Una expresión let tiene dos partes: Primero viene una lista de expresiones defi- niendo las nuevas variables locales, cada una de ellas con la forma (variable expre- sión). Cada variable es inicializada con el valor que regrese la expresión asociada a ella. En el ejemplo anterior se han creado dos variables, x e y, con los valores 1 y 2 respectivamente. Esas variables son válidas dentro del cuerpo de let. Después de la lista de variables y valores, viene el cuerpo de let constituido por una serie de expresiones que son evaluadas en orden. En el ejemplo, sólo hay una llamada a +. Presento ahora como ejemplo una función preguntar más selectiva:

(defun preguntar-num () (format t "Por favor, escriba un numero: ") (let ((val (read))) (if (numberp val) val (preguntar-num))))

Esta función crea la variable local var para guardar el valor que regresa read. Como este valor puede ahora ser manipulado por Lisp, la función revisa que se ha escrito para decidir que hacer. Si el usuario ha escrito algo que no es un número, la función vuelve a llamarse a si misma:

(preguntar-num) Por favor, escriba un numero: a Por favor, escriba un numero: (un numero) Por favor, escriba un numero: 3 3

Las variables de este estilo se conocen como locales porque sólo son válidas en cierto contexto. Existe otro tipo de variables llamadas globales, que son visibles donde sea^2. Se puede crear una variable global con un símbolo dado y un valor, usando defparameter:

(defparameter glob 1970) GLOB

Esta variable es visible donde sea, salvo en contextos que definan una variable local con el mismo nombre. Para evitar errores accidentales con los nombre de las variables, se usa la convención de nombrar a las variables globales con símbolos que inicien y terminen en asterisco. Se pueden definir también constantes globales usando defconstant:

(defconstant limit (+ glob 1))

No hay necesidad de dar a las constantes nombres distintivos porque si se intenta usar el nombre de una constante para una variable se produce un error. Para verificar si un símbolo es el nombre de una variable global o constante, se puede usar boundp:

(boundp ’glob) T

11. Asignaciones

En Lisp el operador de asignación más común es setf. Se puede usar para asignar valores a cualquier tipo de variable. Ej.

(seft glob 2000) 2000 (let ((n 10)) (setf n 2) n) 2

Cuando el primer argumento de setf es un símbolo que no es el nombre de una variable local, se asume que se trata de una variable global.

(^2) La distinción propia es entre variables lexicas y especiales, pero por el momento no es necesario entrar en detalles

¿Por qué no decir simplemente que remove remueve un objeto dado de una lista? Porque esto no es lo que la función hace. La lista original no es modificada:

lst (K A R A T E)

Si se desea que la lista original sea afectada, se puede evaluar la siguiente expresión:

(setf lst (remove ’a lst)) (K R T E) lst (K R T E)

La programación funcional significa, escencialmente, evitar setf y otras expresio- nes con el mismo tipo de efecto colateral. Esto puede parecer contra intuitivo y hasta no deseable. Si bien programar totalmente sin efetos colaterales es inconveniente, a medida que practiquen Lisp, se soprenderán de lo poco que en realidad se necesita este tipo de efecto. Una de las ventajas de la programación funcional es que permite la verificación interactiva. En código puramente funcional, se puede verificar cada función a medida que se va escribiendo. Si la función regresa los valores que esperamos, se puede confiar en que es correcta. La confienza agregada al proceder de este modo, hace una gran diferencia: un nuevo estilo de programación.

13. Iteración

Cuando deseamos programar algo repetitivo, algunas veces la iteración resulta más natural que la recursividad. El caso típico de iteración consiste en generar algún tipo de tabla. Ej.

(defun cuadrados (inicio fin) (do ((i inicio (+ i 1))) ((> i fin) ’final) (format t "~A ~A ~%" i (* i i)))) CUADRADOS (cuadradros 2 5) 2 4 3 9 4 16 5 25 FINAL

La macro do es el operador fundamental de iteración en Lisp. Como let, do puede crear variables y su primer argumento es una lista de especificación de variables. Cada elemento de esta lista toma la forma (variable valor-inicial actualización). En cada iteración el valor de las variables definidas de esta forma, cambia como lo especifica la actualización.

En el ejemplo anterior, do crea unicamente la variable local i. En la primer iteración i tomará el valor de inicio y en las sucesivas iteraciones su valor se incrementará en

El segundo argumento de do debe ser una lista que incluya una o más expresiones. La primera expresión se usa como prueba para determinar cuando debe parar la itera- ción. En el ejemplo, esta prueba es (> i fin). El resto de la lista será evaluado en orden cuando la iteración termine. La última expresión evaluada será el valor de do, por lo que cuadrados, regresa siempre el valor ’final. El resto de los argumentos de do, constituyen el cuerpo del ciclo y serán evaluados en orden en cada iteración, donde: las variables son actualizadas, se evalua la prueba de fin de iteración y si esta falla, se evalua el cuerpo de do. Para comparar, se presenta aquí una versión recursiva de cuadrados:

(defun cuadrados (inicio fin) (if (> inicio fin) ’final (progn (format t "~A ~A ~%" i (* i i)) (cuadrados (+ inicio 1) fin)))) CUADRADOS

La única novedad en esta función es progn que toma cualquier número de expre- siones como argumentos, las evalua en orden y regresa el valor de la última expresión evaluada. Lisp provee operadores de iteración más sencillos para casos especiales, por ejem- plo, dolist para iterar sobre los elementos de una lista. Ej. Una función que calcula la longitud de una lista:

(defun longitud (lst) (let ((len 0)) (dolist (obj lst) (setf len (+ len 1))) len)) LONGITUD (longitud ’(a b c)) 3

El primer argumento de dolist toma la forma (variable expresión), el resto de los argumentos son expresiones que constituyen el cuerpo de dolist. Este cuerpo será evaluado con la variable instanciada con elementos sucesivos de la lista que regresa expresión. La función del ejemplo, dice – por cada obj en lst, incrementar en uno len. La versión recursiva obvia de esta función longitud es:

(defun longitud-rec (lst) (if (null lst) 0

(apply #’+ 1 2 ’(3 4 5)) 15

La función funcall hace lo mismo, pero no necesita que sus argumentos esten empaquetados en forma de lista. Ej.

(funcall #’+ 1 2 3) 6

La macro defun crea una función y le da un nombre, pero las funciones no tienen porque tener nombre y no necesitamos defun para crearlas. Como los otros tipos de objeto en Lisp, podemos definir a las funciones literalmente. Para referirnos literalmente a un entero usamos una secuencia de dígitos; para refe- rirnos literalmente a una función usamos una expresión lambda cuyo primer elemento es el símbolo lambda, seguido de una lista de parámetros y el cuerpo de la función. Ej.

(lambda (x y) (x + y)) Una expresión lambda puede considerarse como el nombre de una función. Ej. Puede ser el primer elemento de una llamada de función:

((lambda (x) (+ x 100)) 1) 101

o usarse con funcall:

(funcall #’(lambda (x) (+ x 100)) 1) 101

Entre otras cosas, esta notación nos permite usar funciones sin necesidad de nom- brarlas.

15. Tipos

Lisp utiliza un inusual enfoque flexible sobre tipos. En muchos lenguajes, las varia- bles tienen un tipo asociado y no es posible usar una variable sin especificar su tipo. En Lisp, los valores tienen un tipo, no las variables. Imaginen que cada objeto en Lisp tiene asociada una etiqueta que especifica su tipo. Este enfoque se conoce como tipificación manifiesta. No es necesario declarar el tipo de una variable porque cualquier variable puede recibir cualquier objeto de cualquier tipo. De cualquier forma, es posible definir el tipo de una variable para optimizar el código antes de la compilación. Lisp incluye una jerarquía predefinida de subtipos y supertipos. Un objeto siempre tiene más de un tipo. Ej. el número 27 es de tipo fixnum, integer, rational, real, number, atom y t, en orden de generalidad incremental. El tipo t es el supertipo de todo tipo. La función typep toma como argumentos un objeto y un especificador de tipo y regreta t si el objeto es de ese tipo. Ej.

(typep 27 ’integer) T

16. Consideraciones

Aunque este documento presenta un bosquejo rápido de Lisp, es posible apreciar ya el retrato de un lenguaje de programación inusual. Un lenguaje con una sola sintáxis para expresar programas y datos. Esta sintáxis se basa en listas, que son a su vez objetos en Lisp. Las funciones, que son objetos del lenguaje también, se expresan como listas. Y Lisp mismo es un programa Lisp, programado casi por completo con funciones Lisp que en nada difieren a las que podemos definir. No debe preocuparles que la relación entre todas estas ideas no sea del todo clara. Lisp introduce tal cantidad de conceptos nuevos que toma tiempo acostumbrarse a ellos y como usarlos. Solo una cosa debe estar clara: Las ideas detrás Lisp son extremada- mente elegantes. Si C es el lenguaje para escribir UNIX^3 , entonces podríamos describir a Lisp co- mo el lenguaje para describir Lisp, pero eso es una historia totalmente diferente. Un lenguaje que puede ser escrito en si mismo, es algo distinto de un lenguaje para es- cribir una clase partícular de aplicaciones. Ofrece una nueva forma de programar: así como es posible escribit un programa en el lenguaje, ¡el lenguaje puede mejorarse para acomodarse al programa! Esto es un buen inicio para entender la escencia de Lisp.

(^3) Richard Gabriel