LECCION 6. ENTRADA/SALIDA Y ERRORES. En esta lección se exponen y aplican las construcciones Lisp que permiten la comunicación con el mundo exterior (E/S), así como algunas de las que permiten la señalización y el manejo de errores de ejecución. Al finalizar la lección, el alumno debe ser capaz de -escribir funciones que se comuniquen con el usuario a través del teclado y la pantalla (ej. 1, 2 y 3). -manejar datos de tipo carácter y cadena (ej. 2) -escribir funciones que se comuniquen con ficheros del sistema (ej. 4 y 5). -modificar el lector del sistema Lisp mediante el uso de caracteres macro (ej. 6 y 7). -escribir funciones que se comporten de forma robusta ante los errores de ejecución (ej. 8, 9 y 10) "Hello world!" (Programador C anónimo). Inteligencia Artificial e I.C.. 2002/2003 6.1 Dpto. Leng. Ciencias Comp. Inteligencia Artificial e I.C.. 2002/2003 6.2 Dpto. Leng. Ciencias Comp. R.6.1. E/S de supervivencia. a) Escribir una función sin argumentos que devuelva la suma de dos números leídos del teclado. b) Escribir una función sin argumentos que devuelva NIL y como efecto lateral escriba en la pantalla: i) dos mensajes solicitando cada uno que se teclee un número y ii) el resultado de su suma. c) Escribir una función EVALQUOTE que modifique la interacción con LISP de la siguiente forma: - el aviso (“prompt”) del intérprete debe ser EVALQUOTE> -si el usuario teclea >EVAL, se vuelve al modo habitual -en otro caso, el usuario debe teclear un nombre de función (en sentido estricto, es decir, no valen formas especiales ni macros) y la lista de sus argumentos sin comillas. Por ejemplo > EVALQUOTE EVALQUOTE> CONS(A (B)) (A B) EVALQUOTE> EVAL > ************************** SOLUCION: a) READ es una función que tiene el siguiente significado funcional predefinido: (READ) -ningún argumento (de momento). -el valor devuelto se toma de la corriente de entrada por defecto. Por ahora, podemos suponer que se toma del teclado. Según esto, la solucion será (DEFUN KK1 () (+ (READ) (READ))) b) PRIN1 es una función que tiene el siguiente significado funcional predefinido: (PRIN1 expresión) -un único argumento. -el valor devuelto es del argumento. -como efecto lateral, el valor es enviado a la corriente de salida por defecto. Por ahora podemos suponer que el valor se escribe en la pantalla. (TERPRI) escribe una línea en blanco. (PRINT expresión) es como PRIN1, con la siguiente diferencia: -como efecto lateral, el valor es enviado a la corriente de salida por defecto, precedido por un salto de línea y seguido por un espacio. Por el ap. anterior, sabemos que (+ (READ) (READ)) calcula la suma de dos números leídos del teclado. Para escribirla tendremos que hacer (PRINT (+ (READ) (READ))) Sin embargo, de esta forma no consegimos escribir los mensajes que piden los datos. Para ello hay que añadir para cada READ un PRINT que lo abrace: (PRINT (+ (CADR (LIST (PRINT '(DAME UN NUMERO)) (READ))) (CADR (LIST (PRINT '(DAME UN NUMERO)) (READ))))) y finalmente (DEFUN KK2 () (PRINT (+ (CADR (LIST (PRINT '(DAME UN NUMERO)) (READ))) (CADR (LIST (PRINT '(DAME UN NUMERO)) (READ))))) NIL) NOTA: El intérprete escribe siempre en la pantalla el valor de la expresión que se le teclea. Por tanto, si la anterior expresión se teclea directamente al intérprete, el resultado aparecerá dos veces en la pantalla: una, por el funcionamiento normal del intérprete; otra, como efecto lateral de su evaluación. Inteligencia Artificial e I.C.. 2002/2003 6.3 Dpto. Leng. Ciencias Comp. NOTA: [Ch] afirman (p. 29): "LISP tiene varias funciones, como PRINT o MAPC, que existen solamente por sus efectos laterales. No es en absoluto una buena idea depender de sus valores". Sin embargo, el valor de PRINT está perfectamente especificado en la definición de Common Lisp, y el empleo de este valor no debe ocasionar problemas. c) Una definición recursiva por la cola: (DEFUN EVALQUOTE () (PRINT 'EVALQUOTE>) (LET ((F (READ))) (COND ((EQ F '>EVAL) NIL) ( T (PRINT (APPLY F (READ))) (EVALQUOTE))))) ;escribir nueva línea y el aviso ;leer la funcion f ;si es >EVAL, acabar, si no ;leer lista, aplicar f, ;escribir nueva línea y f(lista) ;y seguir en evalquote NOTA. El funcionamiento normal del intérprete puede describirse como un bucle READ-EVALPRINT: se lee una expresión con READ, se evalúa con EVAL y se escribe el resultado con PRINT. El ejercicio anterior muestra cuán fácilmente se puede simular o modificar este bucle. Inteligencia Artificial e I.C.. 2002/2003 6.4 Dpto. Leng. Ciencias Comp. R.6.2. Caracteres y cadenas. a) Evaluar las siguientes expresiones: #\A #\Space (CHARACTER 41) (EQ #\A #\a) (EQ #\A #\A) (EQL #\A #\A) (CHAR= #\A #\A) (CHAR= #\SPACE (CHARACTER 32)) (CHAR-EQUAL #\A #\a) b) Evaluar las siguientes expresiones: "Hoy es jueves" (EQUAL #\A "A") (EQL "AB" "AB") (EQUAL "AB" "AB") (EQUAL (STRING 'A) (STRING #\A)) (EQ (INTERN "ABC") 'ABC) (ELT (STRING 'PEPE) 1) (LENGTH (STRING 'PEPE)) (CONCATENATE 'STRING "ABC" "XYZ") (EQUAL "PEPE" "PEPE") (STRING= "PEPE" "pepe") (STRING-EQUAL "PEPE" "pepe") c) Indicar qué se imprime al introducir sucesivamente las siguientes expresiones: (PRINC #\k) (PRINC "pEPE") (PRINC "C:\\PEPE") (PRINC "PEPE\"") (PRIN1 "PEPE") (DEFUN MI-PRINC (S) (TERPRI) (PRINC S)) (MI-PRINC "PEPE") ************************** SOLUCION: a) Además de números, símbolos, funciones y listas, CommonLisp tiene predefinidos otros tipos de datos: por ejemplo, los caracteres (character). Los caracteres son átomos y se evalúan a sí mismos. Para indicar que algo es un carácter, el usuario (y PRINT) lo escribe precedido de #\. La función (CHARACTER n) devuelve el carácter designado por n (la forma concreta de designación depende de la implementación; por ejemplo, n puede ser el código ASCII del carácter). Por tanto #\A =>#\A #\Space =>#\Space (CHARACTER 41) => #\) No es obligado que distintas apariciones de un mismo carácter sean EQ, pero sí que sean EQL. Las funciones especializadas CHAR= y CHAR-EQUAL sirven para comparar únicamente caracteres. CHAREQUAL no distingue entre mayúsculas y minúsculas. Por supuesto, un símbolo cuyo nombre conste de un solo carácter no es lo mismo que un carácter. Por tanto (EQ #\A #\a) => NIL (EQ #\A #\A) => ...depende de la implementación... (EQL #\A #\A) => T (CHAR= #\A #\A) => T (CHAR= #\SPACE (CHARACTER 32)) => T (CHAR-EQUAL #\A #\a) => T b) El tipo cadena (string) está también predefinido. Las cadenas son átomos y se evalúan a sí mismas. Una cadena de un solo carácter no es lo mismo que un carácter. Para indicar que algo es una cadena, el usuario (y PRINT) lo escribe entre comillas dobles. La función STRING transforma caracteres y símbolos en cadenas. La función INTERN transforma cadenas en símbolos. Por tanto Inteligencia Artificial e I.C.. 2002/2003 6.5 Dpto. Leng. Ciencias Comp. "Hoy es jueves" => "Hoy es jueves" (EQUAL #\A "A") => NIL (EQL "AB" "AB") => NIL (EQUAL "AB" "AB") => T (EQUAL (STRING 'A) (STRING #\A)) => T (EQ (INTERN "ABC") 'ABC) => T La función (ELT cadena n) extrae el n-ésimo carácter de cadena (el primero es el 0-ésimo). (ELT (STRING 'PEPE) 1) => #\E La función LENGTH da la longitud de una cadena: (LENGTH (STRING 'PEPE)) => 4 Nótese que la misma función LENGTH sirve para listas y de cadenas. De la misma forma, ELT puede emplearse también para extraer el enésimo elemento de una lista. La razón es que estas funciones, y otras muchas, están definidas para el tipo sucesión (sequence), que comprende listas, cadenas y vectores. La función CONCATENATE, por ejemplo, sirve para APPENDar tato listas como cadenas. Su primer argumento es un símbolo que indica el tipo del resultado. Por tanto (CONCATENATE 'STRING "ABC" "XYZ") => "ABCXYZ" y también tendríamos (CONCATENATE 'LIST '(A B C) '(D C)) => (A B C D C) (CONCATENATE 'LIST "ABC" "XYZ") => (#\A #\B #\C #\X #\Y #\Z) No es obligado que distintas apariciones de una misma cadena sean EQL, pero sí que sean EQUAL. Las funciones especializadas STRING= y STRING-EQUAL sirven para comparar únicamente cadenas, análogamente a CHAR= y CHAR-EQUAL. Por tanto (EQUAL "PEPE" "PEPE") => T (STRING= "PEPE" "pepe") => NIL (STRING-EQUAL "PEPE" "pepe") => T c) La función PRINC es análoga a PRIN1, con la diferencia de que escribe sin delimitadores cadenas y caracteres: > (PRINC #\k)k #\k > (PRINC "pEPE")pEPE "pEPE" > (PRIN1 "PEPE")"PEPE" "PEPE" El carácter \ sirve como carácter de escape dentro de una cadena, para indicar que el siguiente carácter debe tomarse literalmente. Su empleo es necesario si se desea que " o \ formen parte de la cadena: > (PRINC "C:\\PEPE")C:\PEPE "C:\\PEPE" > (PRINC "PEPE\"")PEPE" "PEPE\"" > (DEFUN MI-PRINC (S) (TERPRI) (PRINC S)) MI-PRINC > (MI-PRINC "PEPE") PEPE "PEPE" . NOTA: La diferencia entre PRIN1 y PRINC puede resumirse como sigue: PRIN1 escribe su argumento respetando las convenciones exigidas por READ; PRINC escribe su argumento de forma que un ser humano lo lea cómodamente. Inteligencia Artificial e I.C.. 2002/2003 6.6 Dpto. Leng. Ciencias Comp. R.6.3. FORMAT Escribir una función KK1 que lleve a cabo la siguiente tarea: solicitar al usuario que introduzca una expresión numérica y escribir en distintos renglones los valores de su cubo y su cuadrado, con los correspondientes mensajes, en formato decimal y con dos decimales. En caso de introducir un dato no numérico se dará al usuario un mensaje adecuado y se le solicitará de nuevo un valor. La función devuelve siempre T. Por ejemplo: Dime una expresión: PEPE PEPE no es un número, lo siento. Dime una expresión: 2E-1 El cuadrado es 0.04 y el cubo es 0.00. Adiós. T ************************** SOLUCION: Para conseguir salidas formateadas se emplea la función FORMAT: (FORMAT destino cadena-control expresión*) donde destino es la corriente de salida. Si es T, se toma la corriente de salida por defecto (normalmente, la pantalla); si es NIL, se crea una cadena que contiene la salida. expresión* es una sucesión de expresiones, que se evalúan secuencialmente para obtener valor1, ..., valorn. cadena-control es una cadena. Si destino es NIL, FORMAT devuelve la cadena descrita más adelante. En otro caso, FORMAT se evalúa a NIL y como efecto lateral escribe en destino los valores valor1, ..., valorn, formateados de acuerdo a las instrucciones de la cadena de control. Concretamente: -La cadena de control se escribe literalmente en la corriente de salida, a excepción de los caracteres precedidos por ~, que se denominan directivas de formateo. -La directiva ~S hace que en su lugar se escriba (como con PRIN1) uno de los valores valor1, ..., valorn. La directiva ~A hace que en su lugar se escriba (como con PRINC) uno de los valores valor1, ..., valorn. El valor es determinado secuencialmente (el primer valor con el primer ~S o ~A, ...). Si hay más directivas que valores, se produce un error. Si hay más valores que directivas, no se produce por ello error: simplemente, los últimos valores no se escriben. -La directiva ~% escribe un salto de línea. -La directiva ~<nueva línea> ignora la <nueva línea> y los blancos que la sigan. Por ejemplo >(FORMAT T "~%Hoy es ~S de ~S de ~S." 12 "enero" 1492) Hoy es 12 de "enero" de 1492. NIL >(FORMAT T "~%Hoy es ~S de ~A de ~S." 12 "enero" 1492) Hoy es 12 de enero de 1492. NIL >(FORMAT NIL "~%Hoy es ~S de ~A de ~S." 12 "enero" 1492) " Hoy es 12 de enero de 1492." -La directiva ~n,mF escribe un número en formato decimal, como mínimo con un total de n caracteres, empleando m decimales. Por ejemplo >(FORMAT T "~% Un número:~10,3F.~% El mismo número:~5,2F.~% Y otra vez ~ el mismo número:~2,0F." 123.1 123.1 123.1) Un número: 123.100. El mismo número:123.10. Y otra vez el mismo número:123.. NIL Según esto (DEFUN MENSAJE (CADENA) Inteligencia Artificial e I.C.. 2002/2003 6.7 Dpto. Leng. Ciencias Comp. (PRINC CADENA) (READ)) (DEFUN KK1 () (LET ((S (MENSAJE "Dime una expresion: "))) (COND((NUMBERP S) (FORMAT T "~%El cuadrado es ~6,2F y el cubo es ~6,2F~ .~%Adios." (* S S) (* S S S)) T) (T (FORMAT T "~%~S no es un numero, lo siento." S) (KK1))))) NOTA: (FORMAT T ...) devuelve NIL. Para respetar la definición especificada de KK1, es necesario por tanto introducir explícitamente T como valor del COND. Compárese con PRIN1 o PRINT, que devuelven el valor de su argumento. NOTA. La definición completa de FORMAT ocupa 29 páginas de CLtL2 (581-609), y otras tantas del estándar ANSI. El alumno interesado puede consultar allí todas las posibilidades de formateo (numérico, alfanumérico, condicional, mediante funciones, ...) que FORMAT proporciona. Inteligencia Artificial e I.C.. 2002/2003 6.8 Dpto. Leng. Ciencias Comp. R.6.4. Secuenciación. Evaluar las siguientes expresiones: (PROGN (+ 1 2 3) (CAR '(A B))) (PROG1 (+ 1 2 3) (CAR '(A B))) (LOOP (+ 1 2 3) (CAR '(A B))) (LOOP (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN)) (PRINT 'OIDO)) (LOOP (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN T)) (PRINT 'OIDO)) (BLOCK PEPE (+ 1 2 3) (CAR '(A B))) (BLOCK PEPE (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN-FROM PEPE)) ESTO ES BASURA) (BLOCK PEPE (BLOCK JUAN (PRINT 'DIGA?) (IF (EQ (READ) 'PEPE) (RETURN-FROM PEPE 'ADIOS) (RETURN-FROM JUAN 'JA))) ESTO ES BASURA) ************************** SOLUCION: La secuenciación es la forma básica en que fluye la ejecución de los programas imperativos. Sin embargo, en los programas funcionales la secuenciación tiene una importancia más reducida. De hecho, hasta ahora no la hemos mencionado explícitamente. Sin embargo, al programar mediante "efectos laterales" (entrada/salida, asignación), es inevitable el uso de la secuenciación. Los operadores más sencillos son PROGN y PROG1, que evalúan secuencialmente las expresiones de su cuerpo y devuelven el último valor o el primero, respectivamente: (PROGN (+ 1 2 3) (CAR '(A B))) => A (PROG1 (+ 1 2 3) (CAR '(A B))) => 6 NOTA. Muchas de las construcciones ya estudiadas (DEFUN, COND, ...) ejecutan siempre secuencialmente las expresiones de su cuerpo. Pero nótese que en IF y en las inicializaciones y actualizaciones de LET y DO es necesario para ello un PROGN explícito. El operador LOOP evalúa secuencialmente las expresiones de su cuerpo, una y otra vez; en principio, no devuelve ningún valor: (LOOP (+ 1 2 3) (CAR '(A B))) => ...computación infinita... Sin embargo, si dentro del cuerpo se evalúa una forma (RETURN), finaliza el cálculo y LOOP devuelve NIL. El ámbito desde el cual se puede RETURNar se determina léxicamente. (LOOP (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN)) (PRINT 'OIDO)) => DIGA? HOLA OIDO DIGA? FIN Inteligencia Artificial e I.C.. 2002/2003 6.9 Dpto. Leng. Ciencias Comp. NIL Si se evalúa (RETURN e), se devuelve el valor de e: (LOOP (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN T)) (PRINT 'OIDO)) => DIGA? HOLA OIDO DIGA? FIN T El operador más general que establece la posibilidad de RETURNar es BLOCK, que se emplea en formas como (BLOCK nombre expresión*) nombre es un símbolo que no se evalúa. Las restantes expresiones se evalúan secuencialmente y BLOCK devuelve el valor de la última. Pero si alguna de ellas es de una forma (RETURN-FROM nombre resultado) el valor de resultado pasa a ser el de la forma BLOCK y las restantes expresiones quedan sin evaluar. (BLOCK PEPE (+ 1 2 3) (CAR '(A B))) => A (BLOCK PEPE (PRINT 'DIGA?) (WHEN (EQ (READ) 'FIN) (RETURN-FROM PEPE)) ESTO ES BASURA) => DIGA? FIN NIL Los bloques pueden anidarse. En los anidamientos se siguen las reglas de ámbito léxico. (BLOCK PEPE (BLOCK JUAN (PRINT 'DIGA?) (IF (EQ (READ) 'PEPE) (RETURN-FROM PEPE 'ADIOS) (RETURN-FROM JUAN 'JA))) ESTO ES BASURA) => DIGA? PEPE ADIOS (BLOCK PEPE (BLOCK JUAN (PRINT 'DIGA?) (IF (EQ (READ) 'PEPE) (RETURN-FROM PEPE 'ADIOS) (RETURN-FROM JUAN 'JA))) ESTO ES BASURA) => DIGA? NO-PEPE Error: ESTO sin ligar. NOTA. DEFUN establece implícitamente un bloque con el nombre de la función. LOOP, DO y sus variantes establecen implícitamente un bloque con el nombre NIL. En lugar de RETURN-FROM NIL se puede escribir simplemente RETURN Inteligencia Artificial e I.C.. 2002/2003 6.10 Dpto. Leng. Ciencias Comp. R.6.5. Manejo elemental de archivos. Sea un sistema bajo MS-DOS. En un fichero llamado KK.DAT, situado en el directorio C:\DBVARIOS, se almacenan las lecturas de un sensor, tomadas cada hora. El primer dato del archivo es la fecha (formato dd-mm-aa), el segundo la hora de la primera lectura y el tercero los minutos de la primera lectura. a) Evaluar las siguientes expesiones, indicando los efectos laterales: (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) 1 2 3 F1) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (READ F1)(READ F1)) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\QQ.DAT" :DIRECTION :OUTPUT) (PRIN1 'PEPE F1) (PRIN1 2 F1)) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (READ F1))) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (READ F1 NIL))) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (UNLESS (READ F1 NIL) (RETURN NIL)))) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) EE) b) Escribir una función LISP LEE1 que lea el archivo y escriba en pantalla una tabla como la siguiente: Datos del día 17-2-96 HORA LECTURA ==== ======= 00:20 123.14 01:20 0.00 02:20 23.01 ... c) Escribir una función LISP LEE2 que lea el archivo y escriba una tabla como la anterior en un fichero KK.TXT. ************************** SOLUCION: En CommonLisp, una fuente o sumidero de datos se denomina corriente (stream). Una corriente es un objeto Lisp, y por tanto puede ser el valor de la ligadura de un símbolo. Por otra parte, una corriente debe corresponder a un objeto del mundo exterior, de donde se toman los datos o a donde se envían. Las funciones de E/S ya vistas (READ, PRINT, ...) pueden operar sobre cualquier corriente. Para ello la corriente se introduce como argumento adicional: (READ expresión-c) (PRIN1 expresión expresión-c) (PRINC expresión expresión-c) (PRINT expresión expresión-c) En cuanto a FORMAT, ya hemos dicho que su primer argumento puede ser, además de T o NIL, cualquier corriente: (FORMAT expresión-c cadena-control expresión*) En todos estos casos, expresión-c debe evaluarse a una corriente. Una corriente puede ser binaria o de caracteres; puede usarse para sólo lectura, sólo escritura o ambas operaciones a la vez; y puede estar abierta (accesible como fuente o sumidero de datos) o cerrada (inaccesibe). La manera más sencilla y recomendable de crear corrientes para comunicarse con ficheros, y además ligarlas a símbolos, es emplear la forma WITH-OPEN-FILE: (WITH-OPEN-FILE (símbolo expr-fichero opción*)expr*) donde expr-fichero es una expresión que debe evaluarse a un nombre de fichero valido según las convenciones del sistema operativo subyacente. La evaluación de WITH-OPEN-FILE crea primeramente una corriente ligada a este fichero, que queda ligada a símbolo en todo el ámbito de la evaluación. Esta corriente se abre de la forma señalada por opción*. De esta forma se evalúan las expresiones siguientes. Finalmente se cierra la corriente. El valor devuelto por WITH-OPEN-FILE es el de la última expresión. El archivo siempre queda cerrado, aun cuando la evaluación de WITH-OPENFILE haya terminado prematuramente. Inteligencia Artificial e I.C.. 2002/2003 6.11 Dpto. Leng. Ciencias Comp. La corriente se describe mediante los siguientes parámetros clave: :DIRECTION. Puede ser :INPUT, :OUTPUT, :IO. Por defecto es :INPUT :ELEMENT-TYPE. Puede ser CHARACTER, BIT, SIGNED-BYTE, UNSIGNED-BYTE... Por defecto es CHARACTER (en realidad, un subtipo de CHARACTER). Por tanto a) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) 1 2 3 F1) => #<closed-file-stream C:\DBVARIOS\KK.DAT #xE2F5D0> (Se ha creado una corriente para lectura de caracteres asociada al archivo C:\DBVARIOS\KK.DAT, se ha abierto y se ha cerrado). (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (READ F1)(READ F1)) => 0 (Se ha creado una corriente para lectura de caracteres asociada al archivo C:\DBVARIOS\KK.DAT y se ha abierto. El primer READ ha leído el primer dato del archivo -la fecha- y el segundo, el segundo dato -la hora de la primera lectura, que es 0- Finalmente, el archivo se ha cerrado). (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\QQ.DAT" :DIRECTION :OUTPUT) (PRIN1 'PEPE F1) (PRIN1 2 F1)) =>2 (Se ha creado una corriente para escritura de caracteres asociada al archivo C:\DBVARIOS\QQ.DAT y se ha abierto. El primer PRIN1 ha escrito PEPE y el segundo, 2. Finalmente, el archivo se ha cerrado. Nótese que PRIN1 no añade blancos ni saltos de línea, por lo que el contenido del archivo es ahora PEPE2). Los siguiente ejemplos hacen uso del operador LOOP. En el caso más sencillo, (LOOP cuerpo) evalúa una y otra vez las expresiones que forman cuerpo. Por tanto (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (READ F1))) => Error: intento de leer más allá del fin del fichero. (Se ha creado una corriente para lectura de caracteres asociada al archivo C:\DBVARIOS\KK.DAT y se ha abierto. Se han ejecutado los READ hasta llegar al fin del fichero, donde se ha producido un error). El error anterior se puede evitar, añadiendo más argumentos a READ: (READ expresión-c error-eof-p [valor-eof]) Si error-eof-p es falso, no se produce el error de fin de fichero, sino que READ devuelve valoreof (valor por defecto, NIL) cuando intenta ir más allá. Por tanto: (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (READ F1 NIL))) ... (bucle infinito: no se produce error y READ se evalúa a NIL una vez alcanzado el fin del fichero) (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LOOP (UNLESS (READ F1 NIL) (RETURN NIL)))) => NIL (DEFUN F (N) (+ N EE)) => F (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (F 1)) => Error: EE sin ligar. Tanto en este caso como en alguno de los anteriores, se ha producido un error de ejecución mientras se estaba leyendo o escribiendo un fichero. En general, esto puede tener consecuencias fatales para la información contenida en él; sin embargo, la forma WITH-OPEN-FILE se asegura de que el fichero no se perjudique. Sea cual sea el error que haga abortar la evaluación de una forma WITH-OPENFILE, el sistema garantiza que el fichero queda cerrado. Aún más: el fichero queda cerrado independientemente de que el error se haya producido en un entorno –como el del último ejemplodonde léxicamente no estaba vigente la ligadura que lo señala. Inteligencia Artificial e I.C.. 2002/2003 6.12 Dpto. Leng. Ciencias Comp. NOTA. Este efecto puede programarse explícitamente con una forma UNWIND-PROTECT. b) Hay que abrir para lectura de caracteres el fichero C:\DBVARIOS\KK.DAT. Primero se lee la fecha, hora y minutos iniciales. A continuación se leen los datos del sensor. Como no sabemos cuántos hay, leemos con (READ NIL 'EOF) para evitar el error de fin de fichero: (DEFUN LEE1 () (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (LET ((FECHA (READ F1)) (HORA (READ F1)) (M (READ F1))) (FORMAT T "~%Datos del día ~A" FECHA) (FORMAT T "~%HORA LECTURA") (FORMAT T "~%==== =======") (DO ((L (READ F1 NIL 'EOF)(READ F1 NIL 'EOF)) (H HORA (1+ H))) ((EQ L 'EOF) NIL) (FORMAT T "~%~2,D:~2,D ~6,2F" H M L))))) c) Al programa anterior hay que añadirle la creación de una corriente de salida. Nótese que pueden crearse cuantas corrientes sea necesario, así como tenerlas simultáneamente abiertas. Nótese también que FORMAT escribe en el fichero exactamente lo que escribiría en la pantalla si el primer argumento fuera T. (DEFUN LEE2 () (WITH-OPEN-FILE (F1 "C:\\DBVARIOS\\KK.DAT" :DIRECTION :INPUT) (WITH-OPEN-FILE (FS "C:\\DBVARIOS\\KK.TXT" :DIRECTION :OUTPUT) (LET* ((FECHA (READ F1)) (HORA (READ F1)) (M (READ F1))) (FORMAT FS "~%Datos del día ~A" FECHA) (FORMAT FS "~%HORA LECTURA") (FORMAT FS "~%==== =======") (DO ((L (READ F1 NIL 'EOF)(READ F1 NIL 'EOF)) (H HORA (1+ H))) ((EQ L 'EOF) NIL) (FORMAT FS "~%~2,D:~2,D ~6,2F" H M L)))))) Inteligencia Artificial e I.C.. 2002/2003 6.13 Dpto. Leng. Ciencias Comp. R.6.6. Lectura de líneas y caracteres. a) Escribir una función CODIFICAR, que tenga como argumentos dos nombres de fichero ENTRADA y SALIDA y produzca como efecto lateral la escritura en SALIDA de los caracteres contenidos en ENTRADA codificados según la siguiente regla: si el código ASCII de c es n, su codificación es el carácter de código (n +23) mod 128. b) Escribir en LISP una función TFD que corresponda a un traductor finito determinista para convertir una cadena de caracteres en una lista de tokens. Se suponen los tokens "variable" y "constante" y las reglas de Prolog para definir ambos. Para separar los diversos identificadores en la entrada se admiten blancos, comas, tabuladores y saltos de línea. La función debe devolver para cada par (estado, carácter de entrada) el nuevo estado y el token emitido correspondiente (NIL si no se emite ninguno). El fin de la cadena de entrada se supone señalado por el carácter "$". c) Escribir en LISP una función TRADUCIR que tenga como argumentos un TFD y una cadena de caracteres, y devuelva la traduccion de la cadena hasta donde haya sido posible hacerla. Además, como valor secundario, se devolverá T si la traducción se ha podido efectuar por completo, NIL si ha acabado en fracaso. d) Escribir en LISP una función TRADUCIR-FICHERO-TEXTO que tenga como argumentos el cierre léxico de un TFD y dos nombres de fichero ENTRADA y SALIDA. La función escribirá como efecto lateral la traduccion de ENTRADA en SALIDA hasta donde haya sido posible hacerla. El valor de la función será T si la traducción se ha podido efectuar por completo, NIL si ha acabado en fracaso. ************************** SOLUCION: Hemos dicho que READ lee una a una las expresiones LISP. Pero en realidad no sólo lee, sino que lleva a cabo un análisis lexicográfico y sintáctico de lo leído, para determinar, por ejemplo, dónde acaba cada expresión. Existen otras funciones de lectura más elementales, como por ejemplo READCHAR y READ-LINE, que sí se limitan a leer de una corriente. (READ-CHAR expresión-c error-eof-p [valor-eof]) lee un carácter. La función devuelve precisamente ese carácter. (READ-LINE expresión-c error-eof-p [valor-eof]) lee una línea completa, es decir, lee todos los caracteres hasta encontrar un salto de línea. La función devuelve la cadena formada por todos los caracteres de la línea (salvo el de salto). Es por ello muy útil para leer del teclado. READ-LINE devuelve también un segundo valor: "verdadero", si la línea acaba con el carácter de fin de fichero, "falso" en otro caso. Por tanto a) La función que realiza la codificación de un carácter es (LAMBDA (C) (CHARACTER (MOD (+ (CHAR-CODE C) 23) 128))) Leeremos de ENTRADA carácter a carácter, realizaremos la transformación y escribiremos en SALIDA carácter a carácter: (DEFUN CODIFICAR (&KEY ENTRADA SALIDA) (FLET ((DESPLAZAR (C) (CHARACTER (MOD (+ (CHAR-CODE C) 23) 128)))) (WITH-OPEN-FILE (CE ENTRADA :DIRECTION :INPUT) (WITH-OPEN-FILE (CS SALIDA :DIRECTION :OUTPUT) (LOOP (LET ((C (READ-CHAR CE NIL :EOF))) (WHEN (EQ C :EOF) (RETURN)) (PRINC (DESPLAZAR C) CS))))))) b) Un TFD queda definido por los siguientes elementos: -la tabla de transiciones. Cada transición es una tripla o cuádrupla de la forma (estado carácter-leído nuevo-estado [símbolo-emitido]). Cuando el TFD está en estado y lee carácter-leído pasa a nuevoestado y emite símbolo-emitido, caso de que exista. Si en la tabla no hay ninguna transición que empiece por esta combinación (estado carácter-leído ...), la traducción acaba en fracaso. Nunca hay más de una transición para una misma combinación (estado carácter-leído ...). -un conjunto de estados finales. Si al acabar de leer la cadena de entrada el TFD se halla en un estado final, la traducción ha acabado con éxito, caso contrario, en fracaso. Inteligencia Artificial e I.C.. 2002/2003 6.14 Dpto. Leng. Ciencias Comp. -un estado inicial, en el que se halla el TFD antes de leer ningún carácter. Representaremos los elementos anteriores por los siguientes datos LISP: - El segundo elemento de cada transición será un predicado, en lugar de un carácter. De esta forma reducimos considerablemente el tamaño de la tabla. La transición se considera aplicable a todos los caracteres que satisfacen el predicado. Según la gramática de Prolog, son variables las cadenas alfanuméricas que comienzan por una letra mayúscula o por "_". Son constantes las cadenas alfanuméricas que empiezan por una letra minúscula. (DEFUN SEPARADORP (C) (MEMBER C (LIST #\Newline #\Space #\Tab #\,))) (DEFUN TERMINADORP (C) (EQL C #\$)) (DEFUN TERMINAR (CAD) (CONCATENATE 'STRING CAD "$")) (DEFUN VACIAP (CADENA) (= 0 (LENGTH CADENA))) El TFD será (todos los estados finales) letra, dígito, _ E1 separador mayúscula, _ E0 minúscula letra, dígito, _ E2 Para considerar la terminación de la cadena, añadimos un estado T al que se llega al leer "$" en un estado final: letra, dígito, _ E1 separador $ mayúscula, _ E0 T minúscula $ letra, dígito, _ E2 En Lisp Inteligencia Artificial e I.C.. 2002/2003 6.15 Dpto. Leng. Ciencias Comp. (DEFUN TFD1 (ESTADO C-LEIDO) (CASE ESTADO (E0 (COND ((TERMINADORP C-LEIDO) (VALUES T NIL)) ((FUNCALL #'UPPER-CASE-P C-LEIDO) (VALUES 'E1 NIL)) ((FUNCALL #'LOWER-CASE-P C-LEIDO) (VALUES 'E2 NIL)) ((FUNCALL #'(LAMBDA (C) (EQL C #\_)) C-LEIDO) (VALUES 'E1 NIL)) ((FUNCALL #'SEPARADORP C-LEIDO) (VALUES 'E0 NIL)))) (E1 (COND ((TERMINADORP C-LEIDO) (VALUES T 'VAR)) ((FUNCALL #'ALPHA-CHAR-P C-LEIDO) (VALUES 'E1 NIL)) ((FUNCALL #'(LAMBDA (C) (EQL C #\_)) C-LEIDO) (VALUES 'E1 NIL)) ((FUNCALL #'DIGIT-CHAR-P C-LEIDO) (VALUES 'E1 NIL)) ((FUNCALL #'SEPARADORP C-LEIDO) (VALUES 'E0 'VAR)))) (E2 (COND ((TERMINADORP C-LEIDO) (VALUES T 'CTE)) ((FUNCALL #'ALPHA-CHAR-P C-LEIDO) (VALUES 'E2 NIL)) ((FUNCALL #'(LAMBDA (C) (EQL C #\_)) C-LEIDO) (VALUES 'E2 NIL)) ((FUNCALL #'DIGIT-CHAR-P C-LEIDO) (VALUES 'E2 NIL)) ((FUNCALL #'SEPARADORP C-LEIDO) (VALUES 'E0 'CTE)))))) c) La función empleará por acumuladores, uno para la salida y otro para el estado en que se encuentra el traductor. Por otra parte, la entrada se completa en un primer paso con el carácter terminador: (DEFUN TRADUCIR (TFD ENTRADA &OPTIONAL SALIDA (ESTADO 'E0)) (LABELS ((TTRAD (TFD ENTRADA SALIDA ESTADO) (COND ((VACIAP ENTRADA) (VALUES (REVERSE SALIDA) ESTADO)) (T (MULTIPLE-VALUE-BIND (NUEVO-E NUEVA-SAL) (FUNCALL TFD ESTADO (ELT ENTRADA 0)) (COND ((NOT NUEVO-E) (VALUES (REVERSE SALIDA) NIL)) ((NOT NUEVA-SAL) (TTRAD TFD (SUBSEQ ENTRADA 1) SALIDA NUEVO-E)) (T (TTRAD TFD (SUBSEQ ENTRADA 1) (CONS NUEVA-SAL SALIDA) NUEVO-E)))))))) (LET ((ENTRADA (TERMINAR ENTRADA))) (TTRAD TFD ENTRADA SALIDA ESTADO)))) d) No hay más que llamar sucesivamente a TRADUCIR con las diversas líneas de ENTRADA, escribiendo al tiempo las traducciones en SALIDA. (DEFUN TRADUCIR-FICHERO-TEXTO (&KEY TRADUCTOR ENTRADA SALIDA) (WITH-OPEN-FILE (CORR1 ENTRADA :DIRECTION :INPUT) (WITH-OPEN-FILE (CORR2 SALIDA :DIRECTION :OUTPUT) (LOOP (LET ((LINEA (READ-LINE CORR1 NIL :EOF))) (WHEN (EQ LINEA :EOF) (RETURN T)) (MULTIPLE-VALUE-BIND (TRADUCC EXITO) (TRADUCIR TRADUCTOR LINEA) (WHEN (NOT EXITO) (RETURN)) (PRINT TRADUCC CORR2))))))) Inteligencia Artificial e I.C.. 2002/2003 6.16 Dpto. Leng. Ciencias Comp. R.6.7. Caracteres Macro. a) Evaluar en el orden dado las siguientes expresiones: (SET-MACRO-CHARACTER #\? #'(LAMBDA (CORRIENTE CARACTER) (LIST 'A 'B 'C)))) '(? (?)) (SET-MACRO-CHARACTER #\? #'(LAMBDA () (LIST 'A 'B 'C)))) '(? (?)) b) Queremos representar en un programa fórmulas de la lógica de primer orden. Para ello las variables lógicas serán listas de la forma (VAR X), cuyo CAR es el símbolo VAR y cuyo CADR es el nombre X de la variable. Modificar el significado de READ de manera que ?expresión se lea como (VAR expresión). ************************** SOLUCION: Un carácter macro es un carácter tal que READ lo trata de manera especial cuando lo encuentra, expandiéndolo en un conjunto de caracteres. Ya conocemos un carácter macro predefinido, el apóstrofo: cuando READ (bien en el ciclo normal del intérprete, bien en una llamada explícita) encuentra 'expresión, lo transforma inmediatamente en (QUOTE expresión). Es posible definir nuevos caracteres macro mediante la función SET-MACRO-CHARACTER: (SET-MACRO-CHARACTER carácter función) donde carácter es el caracter macro que se define y función es la función de expansión, que que determina lo que debe sustituir al carácter macro. Esta función debe tener dos argumentos: una corriente y un carácter. SET-MACRO-CHARACTER devuelve T y como efecto lateral modifica el significado de READ. A partir de este momento, cuando READ encuente el carácter macro lo sustituirá por el resultado de aplicar la función. Por tanto (SET-MACRO-CHARACTER #\? #'(LAMBDA (CORRIENTE CARACTER) (LIST 'A 'B 'C)))) => T '(? (?)) =>((A B C) ((A B C))) En este sencillo caso la función de expansión no emplea sus argumentos. Sin embargo, es necesario que figuren en su lista lambda: (SET-MACRO-CHARACTER #\? #'(LAMBDA () (LIST 'A 'B 'C)))) =>T '(? (?)) =>Error: número equivocado de argumentos. De hecho, lo más habitual será que la función de expansión contenga una llamada a (READ corriente ...). En este caso hay que añadir un cuarto argumento a READ con valor T (¿por qué? vd. nota). b) La función siguiente realiza la modificación especificada: (DEFUN HAZ-MI-READ () (SET-MACRO-CHARACTER #\? #'(LAMBDA (CORRIENTE CARACTER) (LIST 'VAR (READ CORRIENTE T NIL T))))) Inteligencia Artificial e I.C.. 2002/2003 6.17 Dpto. Leng. Ciencias Comp. > (HAZ-MI-READ) T > '?X (VAR X) > '??X (VAR (VAR X)) NOTA: Es bastante peligroso definir como caracteres macro caracteres alfabéticos o de uso común: a partir de ese momento, cualquier aparición de ellos se verá sustituida por su expansión. Por ejemplo, tras ejecutar la función HAZ-MI-READ de este ejercicio, no es posible emplear en el programa el signo de interrogación (salvo usando caracteres de escape). NOTA: Los caracteres macro se pueden agrupar en una tabla de lectura (readtable). Pueden existir varias tablas de lectura alternativas; según la que se cargue, READ leerá de una manera o de otra. NOTA: la definición completa de READ es (READ expresión-c error-eof-p valor-eof llamada-recursiva-p) donde todos los argumentos son opcionales. El cuarto, llamada-recursiva-p, (valor por defecto, NIL) indica si el READ se ejecuta en el nivel superior o bien proviene de la ejecución de otro READ. Puede ser importante distinguir entre ambos casos (vd. CLtL, pg. 568-569) Inteligencia Artificial e I.C.. 2002/2003 6.18 Dpto. Leng. Ciencias Comp. R.6.8. Caracteres macro de envío. a) Evaluar en el orden dado las siguientes expresiones: (MAKE-DISPATCH-MACRO-CHARACTER #\z) (SET-DISPATCH-MACRO-CHARACTER #\z #\z #'(LAMBDA (CORRIENTE SUBCAR NUM) (LIST 'A 'B 'C))) 'zz 'za 'ZZ b) Ahora queremos escribir el cuantificador universal como $E, abreviatura de la lista (EXISTE variable) y el universal como $U, abreviatura de la lista (PARA-TODO variable). Modificar adecuadamente el significado de READ. ************************** SOLUCION: a) Un carácter macro de envío (dispatching macro character) también es un carácter tal que READ lo trata de manera especial cuando lo encuentra, expandiéndolo en un conjunto de caracteres. Pero en este caso la expansión realizada depende, no sólo del carácter macro, sino también del siguiente carácter leído (subcaracter). Ya conocemos un carácter macro de envío predefinido, el sostenido: cuando READ (bien en el ciclo normal del intérprete, bien en una llamada explícita) encuentra #'expresión, lo transforma inmediatamente en (FUNCTION expresión). Es posible definir nuevos caracteres macro de envío mediante las funciones MAKE-DISPATCHMACRO-CHARACTER y SET-DISPATCH-MACRO-CHARACTER: (MAKE-DISPATCH-MACRO-CHARACTER carácter) Esta función devuelve T y hace que carácter sea un carácter macro de envío. Pero aún no establece su significado. Para ello es necesario emplear además (SET-DISPATCH-MACRO-CHARACTER carácter subcarácter función) donde carácter es el caracter macro de envío, subcarácter es el subcaracter macro de envío, que se define y función es la función de expansión, que determina por lo que se debe sustituir esta cadena de dos caracteres. Esta función debe tener tres argumentos: una corriente y un carácter, y un número (vd. nota; si así lo desea, prescinda el alumno de conocer el significado de este número, pero es un parámetro requerido). SET-DISPATCH-MACRO-CHARACTER devuelve T y como efecto lateral modifica el significado de READ. A partir de este momento, cuando READ encuente la combinación carácter-subcarácter la sustituirá por el resultado de aplicar la función. Por tanto (MAKE-DISPATCH-MACRO-CHARACTER #\z) => T (SET-DISPATCH-MACRO-CHARACTER #\z #\z #'(LAMBDA (CORRIENTE SUBCAR NUM) (LIST 'A 'B 'C))) => T 'zz => (A B C) 'za => Error: combinación ilegal z a Nótese que READ señala un error cuando encuentra una combinación carácter-subcarácter no definida. 'ZZ => ZZ 'zZ => (A B C) Nótese que en el subcarácter no se distingue entre mayúsculas y minúsculas, pero sí en el carácter. b) Agrupemos en una sola función HAZ-MI-READ-2 todas las modificaciones pedidas en este ejercicio y el anterior: (DEFUN HAZ-MI-READ-2 () (SET-MACRO-CHARACTER #\? #'(LAMBDA (CORRIENTE CARACTER) Inteligencia Artificial e I.C.. 2002/2003 6.19 Dpto. Leng. Ciencias Comp. (LIST 'VAR (READ CORRIENTE T NIL T)))) (MAKE-DISPATCH-MACRO-CHARACTER #\$) (SET-DISPATCH-MACRO-CHARACTER #\$ #\E #'(LAMBDA (CORRIENTE SUBCAR NUM) (LIST 'EXISTE (READ CORRIENTE T NIL T)))) (SET-DISPATCH-MACRO-CHARACTER #\$ #\U #'(LAMBDA (CORRIENTE SUBCAR NUM) (LIST 'PARA-TODO (READ CORRIENTE T NIL T))))) y ahora >(HAZ-MI-READ) T >'$E?X (EXISTE (VAR X)) >'$U ?X (PARA-TODO (VAR X)) NOTA: la función de expansión de un carácter macro de envío debe tener 3 parámetros: la corriente, el subcarácter y un número. Este número puede aparecer, como un natural en notación decimal, entre el carácter y el subcarácter. De esta forma los caracteres macros de envío tienen mayor flexibilidad. Si no hay ningún número, el tercer parámetro de la función de expansión se liga a NIL. Inteligencia Artificial e I.C.. 2002/2003 6.20 Dpto. Leng. Ciencias Comp. R.6.9. Manejo de errores (I). Implementar versiones de la función factorial tales que a) (FACTORIAL N) devuelva NIL cuando N no sea un número natural. b) (FACTORIAL N) señale un error y aborte la computación cuando N no sea un número natural. c) (FACTORIAL N) señale un error y aborte la computación cuando N no sea un número natural. Opcionalmente, si el argumento proporcionado es numérico, se calculará el factorial de un número “parecido” al dado: por ejemplo, el factorial de |n| si n es negativo, o de int(n) si n es fraccionario. d) (FACTORIAL N) señale un error y aborte la computación cuando N no sea un número natural. Opcionalmente, se solicitará al usuario un nuevo valor y se calculará el factorial de éste. ************************** SOLUCION: a) Partamos, por ejemplo, de la implementación recursiva ingenua: (DEFUN FACTORIAL (N) (COND ((ZEROP N) 1) (T (* N (FACTORIAL (1- N)))))) ¿Qué ocurre cuando FACTORIAL recibe un argumento inadecuado, por ejemplo, -9.9? Nunca se alcanzará el caso base y la recursión proseguirá hasta que se agota la memoria disponible. La forma más burda de evitar esto es la especificada en este apartado: (DEFUN FACT-EXTENDIDO (S) (COND ((NOT (NUMBERP S)) (PRINT 'ERROR) NIL) ((NOT (INTEGERP S)) (PRINT 'ERROR) NIL) ((MINUSP S) (PRINT 'ERROR) NIL) (T (FACTORIAL S)))) Y ahora será (FACT-EXTENDIDO 9.9)=> NIL escribéndose además el mensaje ERROR El lector debe ser consciente de que FACT-EXTENDIDO no implementa la función matemática factorial , sino una versión extendida de ella: factorial(X), si X es un número natural fact-extendido(X) = NIL, en otro caso. Ello puede llevar a errores algo difíciles de comprender. Por ejemplo, (* 2 (FACT-EXTENDIDO 9.9)) => Error: NIL no es un número. b) Por lo dicho en a), es mejor señalar explícitamente un error cuando el argumento no es adecuado. Para ello, CommonLisp proporciona la función ERROR: (ERROR cadena-control expresión*) Al intentar evaluar ERROR, se genera un mensaje de error –formateado según las reglas de FORMATque se envía a la corriente establecida para los mismos; se entra en el entorno de depuración. Todas las evaluaciones pendientes quedan abortadas y es imposible continuarlas. Según esto, la implementación pedida en este apartado será (DEFUN FACT-PROTEGIDO (S) (COND ((NOT (NUMBERP S)) (ERROR "~S no es un numero." S)) ((NOT (INTEGERP S)) (ERROR "~D no es entero." S)) ((MINUSP S) (ERROR "~D es negativo." S)) (T (FACTORIAL S)))) Y ahora tendremos: (FACT-PROTEGIDO 9.9) => Error: 9.9 no es entero. (* 2 (FACT-PROTEGIDO 9.9)) => Error: 9.9 no es entero. Inteligencia Artificial e I.C.. 2002/2003 6.21 Dpto. Leng. Ciencias Comp. NOTA. Es costumbre que los mensajes de error sean frases completas, acabadas con un punto. No es necesario mencionar en ellos la función en la que se produce el error, ya que se supone que el entorno del intérprete o compilador será capaz de añadir automáticamente esta información. c) Además de los errores irrecuperables empleados en b), el programador puede definir errores “continuables” o “recuperables”. Para ello, CommonLisp proporciona la función CERROR: (CERROR cadena-control1 cadena-control2 expresión*) Al intentar evaluar CERROR, se genera un mensaje de error –formateado según las reglas de FORMAT aplicadas a cadena-control2 y a expresión*- que se envía a la corriente establecida para los mismos. Además, se genera otro mensaje error –formateado según las reglas de FORMAT aplicadas a cadena-control1 y a expresión*- que informa al usuario de lo que ocurrirá si la computación no es abortada. Se entra en el entorno de depuración y el usuario puede elegir entre abortar las evaluaciones pendientes o continuar. Si elige continuar, CERROR devuelve NIL y se continúa normalmente con las evaluaciones pendientes. Según esto, la implementación pedida puede ser (DEFUN FACT-CERROR (S) (COND ((NOT (NUMBERP S)) (ERROR "~S no es un numero." S)) ((NOT (INTEGERP S)) (CERROR "Se calculara el factorial de INT(~D)." "~D no es entero." S) (FACT-CERROR (FLOOR S))) ((MINUSP S) (CERROR "Se calculara el factorial de |~D|." "~D es negativo." S) (FACTORIAL (- S))) (T (FACTORIAL S)))) Y ahora tendremos el siguiente diálogo (usuario en cursiva): > (FACT-CERROR –9) Error: -9 es negativo. Para continuar, teclee CONTINUE Se calculará el factorial de |-9|. CONTINUE 362880 Pero > (FACT-CERROR –9) Error: -9 es negativo. Para continuar, teclee CONTINUE Se calculará el factorial de INT(-9). ABORT > d) Lo que se pide podría implementarse mediante CERROR. Sin embargo, existe una manera más cómoda de hacerlo, empleando una forma ASSERT. (ASSERT expresión-test [(símbolo*) [cadena-control expresión*]]) Al intentar evaluar una forma ASSERT, se realiza el siguiente proceso: 1. Se evalúa expresión-test. Si es verdadera, ASSERT devuelve NIL y se continúa normalmente. 2. Si expresión-test es falsa, ASSERT señala un error y envía un mensaje de error indicando este hecho. 3. Si figura la lista (símbolo*), se proporciona al usuario la posibilidad de modificar el entorno vigente, asignando nuevos valores a cada uno de los símbolos de la lista. Si el usuario elige esta opción, se volverá al paso 1. 4. Si el usuario así lo elige, se abortarán las computaciones pendientes. Los argumentos cadena-control expresión* sirven para formar –según las reglas de FORMAT-el mensaje que se envía al usuario. Inteligencia Artificial e I.C.. 2002/2003 6.22 Dpto. Leng. Ciencias Comp. Por ejemplo, tras definir (DEFUN FACT-ASSERT1 (S) (ASSERT (AND (NUMBERP S) (INTEGERP S) (OR (ZEROP S) (PLUSP S)))) (FACTORIAL S)) se produce el siguiente diálogo: >(FACT-ASSERT1 -9) Error: la aserción (AND (NUMBERP S) (INTEGERP S) (OR (ZEROP S) (PLUSP S))) ha fracasado. Tras definir (DEFUN FACT-ASSERT2 (S) (ASSERT (AND (NUMBERP S) (INTEGERP S) (OR (ZEROP S) (PLUSP S))) (S)) (FACTORIAL S)) se produce el siguiente diálogo: >(FACT-ASSERT2 -9) Error: la aserción (AND (NUMBERP S) (INTEGERP S) (OR (ZEROP S) (PLUSP S))) ha fracasado. Teclee otra expresión para S: 9 362880 o bien >(FACT-ASSERT2 -9) Teclee otra expresión para S: ABORT Y tras definir (DEFUN FACT-ASSERT3 (S) (ASSERT (AND (NUMBERP S) (INTEGERP S) (OR (ZEROP S) (PLUSP S))) (S) "Lo siento, ~S no es un argumento valido para factorial." S) (FACTORIAL S)) El diálogo es >(FACT-ASSERT3 -9) Lo siento, -9 no es un argumento valido para factorial. Teclee otra expresión para S: 9 362880 NOTA. Es obvio que la forma concreta del diálogo entre sistema y usuario depende de la implementación del intérprete o compilador. Inteligencia Artificial e I.C.. 2002/2003 6.23 Dpto. Leng. Ciencias Comp. R.6.10. Manejo de errores (II). a) Implementar una versión de EVALQUOTE (vd. R.6.1.c) que nunca señale error. b) Implementar una versión de EVALQUOTE que nunca señale error, y además nunca salga del ciclo EVALQUOTE a causa de un error. c) Implementar una versión de EVALQUOTE que nunca señale error, y además salga del ciclo EVALQUOTE solamente a causa de los errores aritméticos. ************************** SOLUCION. En R.6.9 hemos aprendido a definir errores. En este ejercicio aprenderemos a programar el manejo de errores, tanto predefinidos como definidos por el programa. a) La siguiente forma CommonLisp nos permite realizar esto de una manera sencilla: (IGNORE-ERRORS expresión*) IGNORE-ERRORS evalúa sus expresiones secuencialmente y si no se producen errores devuelve el valor de la última. Por el contrario, si durante la evaluación se señala un error, IGNORE-ERRORS devuelve de manera inmediata NIL (y además el objeto error) y vuelve al nivel superior de interacción, sin entrar en el entorno de depuración. Nótese que el ámbito de IGNORE-ERRORS es dinámico, no léxico, es decir, los errores despreciados son aquellos que se producen durante la ejecución de expresión* o de cualquier otra expresión que se haya llamado desde ellas. Según esto, la función pedida será (DEFUN EEVALQUOTE0 () (IGNORE-ERRORS (PRINT 'EVALQUOTE>) (LET ((F (READ))) (COND ((EQ F 'EVAL>) NIL) (T (PRIN1 (APPLY F (READ))) (EEVALQUOTE0)))))) y ahora tendremos el siguiente diálogo: > (EEVALQUOTE0) EVALQUOTE> / (2 0) NIL #<SIMPLE-ERROR @ #xE204F4> > b) CommonLisp permite manejar los errores de forma flexible. Para ello proporciona los siguientes recursos: -los errores son objetos Lisp (más concretamente, objetos CLOS de tipo condición). -existe una jerarquía predefinida de errores. -se puede programar explícitamente el manejador de cualquier clase de error, es decir, se pueden indicar las formas que se evaluarán cuando se señale un error de esa clase. Para esto último se emplea la forma HANDLER-CASE, que en su formato más sencillo es (HANDLER-CASE expresión {(tipo-error (símbolo) expresión*)}*) Se evalúa expresión (nótese que es exactamente una). Si durante su evaluación se señala un error de un tipo mencionado en una de las cláusulas (tipo-error expresión*), entonces se transfiere el control a las expresión* y símbolo se liga al objeto error señalado. Si el error satisface varios tipos, se transfiere el control a la primera de las cláusulas cuyo tipo lo satisfaga. tipo-error no se evalúa. . Nótese que el ámbito de HANDLER-CASE es dinámico, no léxico Según esto, podemos conseguir lo especificado con la siguiente definición: (DEFUN EEVALQUOTE1 () (HANDLER-CASE (PROGN (PRINT 'EVALQUOTE>) (LET ((F (READ))) (COND ((EQ F 'EVAL>) NIL) Inteligencia Artificial e I.C.. 2002/2003 6.24 Dpto. Leng. Ciencias Comp. (T (PRIN1 (APPLY F (READ))) (EEVALQUOTE1))))) (ERROR (CONDICION) (FORMAT T "Error ~S. ~%Se desprecia y se sigue en EVALQUOTE." CONDICION) (EEVALQUOTE1)))) y ahora el diálogo puede ser > (EEVALQUOTE1) EVALQUOTE> / (2 0) Error #<SIMPLE-ERROR @ #xE21AAC>. Se desprecia y se sigue en EVALQUOTE. EVALQUOTE> C) Nótese que no es necesario emplear un único manejador de errores, como se ha hecho en el apartado anterior. Para resolver el nuevo problema emplearemos dos (el segundo se empleará sólo si el primero no es aplicable al error señalado): (DEFUN EEVALQUOTE2 () (HANDLER-CASE (PROGN (PRINT 'EVALQUOTE>) (LET ((F (READ))) (COND ((EQ F 'EVAL>) NIL) (T (PRIN1 (APPLY F (READ))) (EEVALQUOTE2))))) (ARITHMETIC-ERROR (CONDICION) (FORMAT T "Error ~S. ~%Se desprecia y se sale de EVALQUOTE." CONDICION) 0) (ERROR (CONDICION) (FORMAT T "Error ~S. ~%Se desprecia y se sigue en EVALQUOTE." CONDICION) (EEVALQUOTE2)))) y ahora el diálogo puede ser > (EEVALQUOTE2) EVALQUOTE> CAR (2) Error #<SIMPLE-ERROR @ #xE21ADA>. Se desprecia y se sigue en EVALQUOTE. EVALQUOTE> / (2 0) Error #<SIMPLE-ERROR @ #xE21AAC>. Se desprecia y se sale de EVALQUOTE. 0 > Inteligencia Artificial e I.C.. 2002/2003 6.25 Dpto. Leng. Ciencias Comp. EJERCICIOS PROPUESTOS. P.6.1. Evaluar las siguientes expresiones, indicando los efectos laterales que se produzcan durante la evaluación: a) (READ X Y Z) b) (PRIN1 (LIST (READ) (READ) (READ))) c) (APPEND (QUOTE (READ)) (READ)) d) (PRINT (LIST 'TU 'NOMBRE 'ES (CADR (PRINT '(COMO TE LLAMAS?)) (READ)))) e) (PRINT (PRIN1 (PRINT (PRIN1 (+ 3 7))))) f) (PRINT (+ 3 (PRIN1 (PRIN1 (PRIN1 7))))) g) (+ 3 (PRINT (PRINT (PRINT (PRINT 7))))) h) (FORMAT T “¡Vaya lío! ~S” (+ 3 (PRINT (PRINT (PRINT (PRINT 7)))))) i) (FORMAT NIL “¡Vaya lío! ~S” (+ 3 (PRINT (PRINT (PRINT (PRINT 7)))))) P.6.2. Implementar la función VARIACIONES-FORMATO-1, que tiene un argumento numérico y lo escribe en pantalla en formato decimal sin parte decimal, decimal con 3 decimales, exponencial y hexadecimal. P.6.3. Implementar las siguientes funciones: a) FUGA-DE-VOCALES, que tiene como argumento una cadena c y devuelve la cadena obtenida sustituyendo en c cada vocal por un *: (FUGA-DE-VOCALES “AeifghoU”) => “***fgh**” b) RESTAR-CADENAS, que tiene dos cadenas c1, c2 como argumentos, y devuelve la cadena obtenida quitando de c2 todas las apariciones como subcadena de la cadena c2: (RESTAR-CADENAS “ABCXYBAZABC” “AB”) => (CXYBAZC) P.6.4. Implementar la función COPIAR(fich-origen fich-destino), cuyos argumentos son cadenas, que copia el fichero cuyo nombre es fich-origen en el fichero cuyo nombre es fich-destino. P.6.5. Supóngase que queremos evitar que cierta cadena de caracteres c aparezca en un fichero de texto. Escribir la correspondiente función CENSURA(nombre-archivo cadena). Nótese que no basta eliminar las apariciones actuales de cadena, ya que al quitarlas pueden crearse otras nuevas: por ejemplo, al eliminar "AR" de "AARR" queda "AR", donde de nuevo aparece la cadena censurada. P.6.7. a) Definir ? como un carácter macro, de manera que ?expresión1 expresión2 se lea como expresión2: >?PEPE '(A B C) (A B C) b) Ciertas aplicaciones MS-DOS escriben uno o varios ^Z al final del archivo. Sin embargo, READ no acepta este carácter como nombre de símbolo, ni lo reconoce como marca de final de archivo. Definir ^Z como carácter macro de forma que se evite este problema. P.6.8. a) Definir ! como un carácter macro de envío y R como un subcarácter, de manera que !Rcadena se lea como el número cuya notación romana es cadena: >!RXVIII 18 b) Definir ! como un carácter macro de envío e I como un subcarácter, de manera que !Icadena se lea como el contenido del archivo cuyo nombre es cadena (se supone que el contenido del archivo es una sola expresión Lisp). P.6.9. a) Implementar versiones robustas de las funciones para el manejo de polígonos de R.2.8, de manera que señalen los errores adecuados cuando reciban argumentos que no sean de tipo correcto. Inteligencia Artificial e I.C.. 2002/2003 6.26 Dpto. Leng. Ciencias Comp. b) Implementar versiones de las mismas funciones, de forma que los errores sean recuperables, proporcionando al usuario la posibilidad de cambiar los argumentos proporcionados. P.6.10. a) Implementar una versión de EVALQUOTE que procese adecuadamente las formas especiales. b) Implementar una versión de EVALQUOTE que desprecie los errores aritméticos y los de entrada/salida. Inteligencia Artificial e I.C.. 2002/2003 6.27 Dpto. Leng. Ciencias Comp. NOTAS ERUDITAS Y CURIOSAS. La función EVALQUOTE de R.6.1. tiene una larga historia en LISP. Hubo un tiempo en que muchos intérpretes interaccionaban de esa manera con el usuario; pero hace ya bastantes años que, debido a la falta de homogeneidad que introducen en el lenguaje, han ido cayendo en desuso. Como ha comprobado el alumno, no es demasiado difícil pasar del intérprete 'normal' (al a que se solía denomInar 'intérprete EVAL') al intérprete EVALQUOTE, y viceversa. En los viejos tiempos, la interacción con ficheros se llevaba a cabo mediante las típicas operaciones OPEN y CLOSE (que aún están disponibles en CommonLisp). Sin embargo, la manera más recomendable de llevarla a cabo es emplear WITH-OPEN-FILE, entre otras razones por el implícito UNWIND-PROTECT que lleva consigo. El manejo de errores es una de las cosas importantes que no se suelen contar en los cursos académicos de programación. El programador del mundo real, sin embargo, hará bien si emplea siempre versiones “seguras” o “protegidas” de sus procedimintos y funciones. Ello supone unas cuantas líneas de código y unos microsegundos adicionales de tiempo de ejecución, pero los beneficios obtenidos suelen compensar más que sobradamente estas molestias. CommonLisp es especialmente potente en cuanto al manejo de errores. La clase “error” se define como subclase de la más general “condición” y existe un amplio repertorio de funciones de bajo nivel para señalar y manejar condiciones. La contrapartida es que la especificación del lenguaje es complicada y a veces confusa para el lector inexperto. Como afirma Steele al hablar de la definición de condiciones [CLtL2, p.898], “...el lector debe tomarse esta sección con cierta precaución y dos aspirinas, y llamar a un hacker”. Inteligencia Artificial e I.C.. 2002/2003 6.28 Dpto. Leng. Ciencias Comp.