leccion 7. asignación. - Universidad de Málaga

Anuncio
LECCION 7.
ASIGNACIÓN.
En esta lección se exponen y aplican las construcciones Lisp que complican el modelo de
entornos y ligaduras visto en las lecciones anteriores, fundamentalmente la asignación. Estas
construcciones se emplean para representar objetos con estado mediante cierres léxicos. Ello nos
sirve para seguir implementando un pequeño sistema de objetos con paso de mensajes.
"Todo debe hacerse tan sencillo como sea posible,
pero no más"
(A. Einstein)
Inteligencia Artificial e I.C. 2002/2003
7.1
Dpto. L.C.C (Universidad de. Málaga).
Inteligencia Artificial e I.C. 2002/2003
7.2
Dpto. L.C.C (Universidad de. Málaga).
R.7.1. Asignación: SETQ.
Evaluar las siguientes expresiones en el orden indicado:
((LAMBDA (Y)
(SETQ Y 2) Y)
10)
(LET ((X 10))
(SETQ X 2))
X
(LET ((X 1))
(LET ((X 10))
(SETQ X 2)))
(LET ((X 1))
(LET ((X 10))
(SETQ X 2))
X)
(LET (X Y Z)
(SETQ X 1
Y (1+ X)
Z (1+ Y))
(LIST X Y Z))
(LET ((X 10))
(SETF X 2) X)
(LET ((X 3) (Y 10))
(DOTIMES (I X)
(PRIN1 I) (SETF X (+ X I)))))
(LET ((K NIL) (L (LIST 'A 'B 'C)))
(DOLIST (E L K)
(SETF K (CONS E K))))
**************************
SOLUCION:
Hasta ahora hemos creado ligaduras mediante llamadas a funciones y mediante formas como LET,
DO, WITH-OPEN-FILE. Con estos y otros procedimientos se puede ligar cualquier símbolo a
cualquier valor. Sin embargo, las ligaduras son difíciles de modificar una vez creadas: la única
manera que conocemos es la actualización en la forma DO. Aún así, hemos podido implementar
procedimientos bastante complicados.
CommonLisp proporciona también la posibilidad de modificar libremente cualquier ligadura
existente (asignación), de manera no estructurada. Esto se consigue mediante la forma especial
SETQ:
(SETQ símbolo expresión)
SETQ es una forma especial que no evalúa su primer argumento, que debe ser un símbolo. El valor
devuelto es el del segundo argumento. SETQ se emplea por su efecto lateral: modificar la ligadura
vigente de símbolo, de manera que símbolo pasa a estar ligado al valor de expresión. Por
tanto
((LAMBDA (Y)
(SETQ Y 2) Y)
; se modifica el entorno e Y<-2
10)
; se crea un entorno con Y<-10
=>2
(LET ((X 10))
; se crea un entorno con X<-10
(SETQ X 2) X)
; se modifica el entorno y X<-2
=> 2
X
=> Error: X sin ligar.
(LET ((X 1))
(LET ((X 10))
(SETQ X 2)))
=> 2
; se crea un entorno con X<-1
; se crea otro entorno con X<-10, que lo ensombrece
; en este se asigna X<-2
Inteligencia Artificial e I.C. 2002/2003
7.3
Dpto. L.C.C (Universidad de. Málaga).
(LET ((X 1))
(LET ((X 10))
(SETQ X 2))
X)
=> 1
; se crea un entorno con X<-1
; se crea otro entorno con X<-10, que lo ensombrece
; en este se asigna X<-2
El número de argumentos de SETQ es par, pero indeterminado: pueden realizarse varias
asignaciones con una misma llamada a SETQ.
(SETQ [símbolo expresión]*)
En ese caso, las asignaciones se realizan secuencialmente. Por tanto,
(LET (X Y Z)
(SETQ X 1
Y (1+ X)
Z (1+ Y))
(LIST X Y Z))
=> (1 2 3)
SETF es otra forma especial equivalente a SETQ:
(SETF [símbolo expresión]*)
Por tanto,
(LET ((X 10))
(SETF X 2) X)
=> 2
PSETF tiene un significado parecido:
(PSETF [símbolo expresión]*)
con la salvedad de que realiza en paralelo la evaluación de las expresión*.
NOTA: SETQ es en realidad un caso particular de SETF. SETF admite como primer argumento no
sólo un símbolo, sino muchas otras cosas señaladas por punteros (lugares o variables
generalizadas).
Supóngase que se desea realizar lo siguiente: dados un símbolo contador, una expresión numérica
veces que se evalúa al entero n, una expresión opcional expr-final (NIL por defecto), y una
sucesión de expresiones expr-cuerpo*, evaluar expr-cuerpo* sucesivamente con contador ligado a
los valores enteros 0, ..., n-1 y finalmente devolver el resultado de evaluar expr-final con contador
ligado a n. CommonLisp proporciona un operador DOTIMES para llevar a cabo esta tarea:
(DOTIMES (contador veces [expr-final]) expr-cuerpo*)
Por tanto
(LET ((X 3) (Y 10))
(DOTIMES (I X)
(PRIN1 I) (SETF X (+ X I)))))
012 (efecto lateral)
=> NIL
Supóngase que se desea realizar lo siguiente: dados un símbolo variable, una expresión expr-lista
que se evalúa a la lista lista, una expresión opcional expr-final (NIL por defecto), y una sucesión
de expresiones expr-cuerpo*, evaluar expr-cuerpo* sucesivamente con variable ligado a los valores
car(lista), cadr(lista), ..., car(last(lista)) y finalmente devolver el resultado de evaluar expr-final con
variable ligada a NIL. CommonLisp proporciona un operador DOLIST para llevar a cabo esta
tarea:
(DOLIST (variable lista [expr-final]) expr-cuerpo*)
Por tanto
(LET ((K NIL) (L (LIST 'A 'B 'C)))
(DOLIST (E L K)
(SETF K (CONS E K))))
=> (C B A)
Inteligencia Artificial e I.C. 2002/2003
7.4
Dpto. L.C.C (Universidad de. Málaga).
R.7.2. Variables globales. Variables dinámicas.
Evaluar las siguientes expresiones en el orden indicado:
a)
(DEFVAR *X* 1000)
*X*
(LET ((*X* 2)) (SETQ *X* (1+ *X*)))
*X*
(SETQ *X* 100)
*X*
(DEFUN KKX1 (*X*)
(KKX2))
(DEFUN KKX2 ()
*X*)
(KKX1 1)
b)
(SETQ *Y* 100)
*Y*
(LET ((*Y* 2)) (SETQ *Y* (1+ *Y*)))
*Y*
(DEFUN KKY1 (*Y*)
(KKY2))
(DEFUN KKY2 ()
*Y*)
(KKY1 1)
**************************
SOLUCION:
En R.7.1 hemos introducido el concepto de asignación y SETQ como forma de realizarla. Pero
hemos tenido buen cuidado de asignar valores a variables ligadas léxicamente, es decir, variables
definidas en el entorno léxico donde se realiza la asignación.
En Lisp existe una forma alternativa de crear variables, variables que no son léxicas sino que
reciben la denominación de variables dinámicas o especiales. Es la forma DEFVAR:
(DEFVAR símbolo [expresión])
DEFVAR asigna a símbolo el valor de expresión. Si expresión no figura, o si símbolo ya tiene un
valor, no se realiza ninguna asignación. Pero DEFVAR hace algo más: declara que símbolo es una
variable especial, es decir, que el ámbito de sus ligaduras no se determina según las reglas léxicas,
sino según las reglas dinámicas: sus valores se buscan en los entornos desde los cuales se ha
creado el actual. Además, la variable así creada es una variable global: su valor es accesible
desde cualquier punto del programa. Es decir, además de los entornos vistos hasta ahora,
CommonLisp mantiene un entorno tope o entorno global cuyas ligaduras están accesibles
siempre. Según esto, tendremos
(DEFVAR *X* 1000)
=> *X*
(DEFVAR devuelve el símbolo)
*X*
=> 1000
Efectivamente, se ha realizado la asignación global.
(LET ((*X* 2)) (SETQ *X* (1+ *X*)))
=> 3
*X*
=> 1000
La ligadura recuperada y modificada ha sido la del LET, no el valor global.
(SETQ *X* 100)
=> 100
*X*
=> 100
El valor global de *X* puede modificarse con SETQ. Hasta aquí no hay nada sorprendente. Pero
obsérvese lo que ocurre a continuación:
Inteligencia Artificial e I.C. 2002/2003
7.5
Dpto. L.C.C (Universidad de. Málaga).
(DEFUN KKX1 (*X*)
(KKX2))
=> KKX1
(DEFUN KKX2 ()
*X*)
=> KKX2
(KKX1 1)
=> 1
Desde el entorno de evaluación creado por la llamada a (KKY1 1) (con *X* ligado a 1), la
llamada a (KKY2) crea otro entorno, donde finalmente se evalua *X*. Puesto que *X* es una
variable especial, la ligadura vigente se determina por las reglas dinámicas, acudiendo al entorno
desde donde se creó el actual. En él *X* vale 1, que es el valor devuelto finalmente.
b) Hasta ahora hemos tenido buen cuidado de no asignar valores a variables libres. Un variable se
dice que está libre cuando aparece fuera de cualquier entorno donde esté ligada. Por suerte o por
desgracia, este cuidado no es exigido por la definición de CommonLisp, y las implementaciones son
libres de hacer lo que deseen con estas variables. Normalmente, pese a que *Y* está libre, no se
producirá error:
(SETQ *Y* 100)
=> 100
¿En qué ámbito tiene vigencia la asignación realizada? Explicaremos lo que hace CLisp.
*Y*
=> 100
ya que esta aparición, *Y* está libre y se supone global. Sin embargo
(LET ((*Y* 2)) (SETQ *Y* (1+ *Y*)))
=> 3
ya que esta aparición, *Y* está ligada a 2. Aún más,
*Y*
=> 100
La asignación del último SETQ no ha afectado al entorno tope, sino al entorno léxico del LET dentro
del cual aparecía *Y*. De la misma forma
(DEFUN KKY1 (*Y*)
(KKY2))
=> KKY1
(DEFUN KKY2 ()
*Y*)
=> KKY2
(KKY1 1)
=> 100
Desde el entorno de evaluación creado por la llamada a (KKY1 1) (con *Y* ligado a 1), la
llamada a (KKY2) crea otro entorno, donde finalmente se evalua *Y*. Puesto que *Y* está libre,
se usa su valor global, es decir, 100. Es decir, *Y* es una variable con un valor global, pero sus
ligaduras siguen comportándose léxicamente. (Pero vd. nota b))
NOTAS IMPORTANTES.
a) Es buena práctica de programación identificar las variables especiales mediante asteriscos al
comienzo y final de su nombre.
b) Todas las variables globales deberían definirse con DEFVAR. El comportamiento de las variables
globales no declaradas puede variar de implementación en implementación (unas las suponen
dinámicas, otras no).
c) Las variables globales o dinámicas deben emplearse con mucha parsimonia. Su uso sólo está
justificado para describir el estado global del sistema Lisp o del programa (p. ej. cuál es el
dispositivo de E/S por defecto, etc.).
Inteligencia Artificial e I.C. 2002/2003
7.6
Dpto. L.C.C (Universidad de. Málaga).
R.7.3. Funciones con memoria.
Implementar la función MEMORIZA, que produce funciones “con memoria”. MEMORIZA tiene como
argumento una función f (de un argumento), y devuelve una implementación de f tal que nunca se
vuelven a calcular los valores de f ya calculados.
**************************
SOLUCION:
Llamemos F' al resultado de (MEMORIZA F). F' debe almacenar de alguna forma los valores ya
calculados: por ejemplo, en una lista L de la forma
((a1 f(a1)) (a2 f(a2)) ...)
donde a1, a2, ... son los argumentos ya procesados en llamadas anteriores a la función. El valor
inicial de L será la lista vacía. Por otra parte, L debe ser una lista accesible sólo desde la función F';
por tanto, F' debe ser un cierre léxico. Ya sabemos, por tanto, que MEMORIZA tiene la siguiente
estructura:
(DEFUN MEMORIZA (FUNCION)
(LET ((L NIL))
#'(LAMBDA (X)
...)))
Ahora nos queda implementar el cuerpo de (LAMBDA (X) ...). El proceso es sencillo:
1. Comprobar si en la lista L figura un elemento de la forma (X valor). Ello se consigue con la
forma (MEMBER X L :TEST#'(LAMBDA (E1 E2) (EQUAL E1 (CAR E2)))
2. en caso afirmativo, devolver este valor como F'(X).
3. en caso negativo,
! Calcular f(X) mediante (FUNCALL FUNCION X); llamémosle OTRO-VALOR
! Almacenar en L el par (X f(X)) mediante (SETQ L (CONS (LIST X OTRO-VALOR) L)
! Devolver OTRO-VALOR
En resumen,
(DEFUN MEMORIZA (FUNCION)
(LET ((L NIL))
#'(LAMBDA (X)
(LET ((CALCULADO (MEMBER X L
:TEST #'(LAMBDA (E1 E2)
(EQUAL E1 (CAR E2))))))
(IF CALCULADO
(CADAR CALCULADO)
(LET ((OTRO-VALOR (FUNCALL FUNCION X)))
(SETQ L (CONS (LIST X OTRO-VALOR) L))
OTRO-VALOR))))))
Veamos un ejemplo del uso de MEMORIZA. Supongamos la definición
(DEFUN KK (S)
(DOTIMES (I 100000 S)
(- I I)))
Creemos una versión “memoriosa” de KK:
(SETQ MEMO-KK (MEMORIZA #'KK))
=> #<cierre 1 #xE0F984>
Preguntemos dos veces sucesivas cuánto tarda el sistema en calcular KK(Pepe):
(TIME (KK 'PEPE))
=> ...
Timing: (KK 'PEPE)
Execution time: 2.15 seconds plus 1.37 seconds garbage collecting (2145,
1370 clock ticks)
=> PEPE
Inteligencia Artificial e I.C. 2002/2003
7.7
Dpto. L.C.C (Universidad de. Málaga).
(TIME (KK 'PEPE)) => ...
Timing: (KK 'PEPE)
Execution time: 2.22 seconds plus 1.09 seconds garbage collecting (2220,
1090 clock ticks)
=> PEPE
Hemos comprobado que la segunda vez se tarda el mismo tiempo que la primera. Veamos qué
ocurre con la versión memoriosa:
(TIME (FUNCALL MEMO-KK 'PEPE))=>...
Timing: (FUNCALL MEMO-KK'PEPE)
Execution time: 2.27 seconds plus 1.55 seconds garbage collecting (2268,
1550 clock ticks)
PEPE
(TIME (FUNCALL MEMO-KK 'PEPE))=> ...
Timing: (FUNCALL MEMO-KK 'PEPE)
Execution time: 0.00 seconds (0 clock ticks)
PEPE
NOTA. Almacenar en una lista los valores ya calculados es una solución muy burda: en efecto, si se
han calculado ya N valores, comprobar si un valor ha sido ya calculado consume un tiempo del
orden O(N).La solución buena es almacenarlos en una tabla hash (vd. R. 7.6).
Inteligencia Artificial e I.C. 2002/2003
7.8
Dpto. L.C.C (Universidad de. Málaga).
R.7.4. Generadores.
a) Supongamos un retículo indefinido de lado unidad, cuyos nodos se identifican por un par (x,y),
x,y∈Ζ. Definir una función LISP HAZ-RETICULO (X0 XF Y0 YF) que devuelva un cierre léxico
capaz de generar un nodo cualquiera (x, y) del retículo, es decir, una lista (x, y) tal que x0≤x≤xf,
y0≤y≤yf. Dos llamadas al cierre nunca devolverán el mismo nodo, a menos que ya se hayan
generado todos los nodos posibles.
b) Supuesto a), asignar dos generadores distintos a las variables *R1* y *R2*.
**************************
SOLUCION:
Habrá n=(xf -xo +1)(yf – y0 +1) puntos, que pueden numerarse 1, 2, ..., i,..., n. En función de y, las
coordenadas x, y serán
x = i mod (xf -xo +1)
y = i div (xf -xo +1)
Cada vez que se llame al cierre, el valor de i pasará a ser (i + 1) mod n. En resumen
(DEFUN HAZ-RETICULO (X0 XF Y0 YF)
(LET* ((NX (1+ (- XF X0)))
(NY (1+ (- YF Y0)))
(N (* NX NY))
(I -1))
;numero de columnas
;numero de filas
;numero total de puntos
;num. de orden del último punto
;generado
(WHEN (OR (< NX 1) (< NY 1))
(ERROR "Final antes del principio"))
#'(LAMBDA ()
(SETQ I (MOD (1+ I) N))
;siguiente punto
(LIST (+ X0 (MOD I NX))
;siguiente fila
(+ Y0 (FLOOR I NX)))))) ;siguiente columna
b) Ello se conseguirá con dos SETQ:
(SETQ *R1* (HAZ-RETICULO 0 2 10 11))
=> #<cierre 0 #xE10E58>
(SETQ *R2* (HAZ-RETICULO 0 2 10 11))
=> #<cierre 0 #xE1C59C>
Si ahora llamamos a ambos cierres, vemos que los valores forman dos sucesiones diferentes:
(FUNCALL *R1*)
=> (0 10)
(FUNCALL *R1*)
=> (1 10)
(FUNCALL *R1*)
=> (2 10)
(FUNCALL *R2*)
=> (0 10)
(FUNCALL *R1*)
=> (1 11)
Pero si ahora realizamos una nueva llamada a HAZ-RETICULO y la asignamos a *R1*, la sucesión
empieza de nuevo:
(SETQ *R1* (HAZ-RETICULO 0 2 10 11))
=> #<cierre 0 #xE31858>
(FUNCALL *R1*)
=> (0 10)
Inteligencia Artificial e I.C. 2002/2003
7.9
Dpto. L.C.C (Universidad de. Málaga).
R.7.5. Objetos con estado.
Deseamos diseñar un programa para representar y manejar cuadrados y triángulos equiláteros,
definidos ambos por las coordenadas de su centro y la longitud de su lado. El programa debe ser
capaz de calcular las áreas y los perímetros de estas figuras, así como de
a) Dilatar la figura un factor alfa.
b) Trasladar la figura a un nuevo centro (x', y')
c) Trasladar la figura a un nuevo centro (x + deltax, y + deltay) donde (x, y) es el centro actual.
Estas operaciones no se llevarán a cabo creando un nuevo objeto, sino modificando el objeto
original.
**************************
SOLUCION:
El lector habrá notado que este ejercicio es continuación de R.4.6. Pero, a diferencia de lo
especificado entonces, ahora se nos exige considerar mensajes capaces de modificar el objeto
original. Para ello será necesario manipular las ligaduras del cierre léxico, cambiando el valor al
que estén ligadas las variables que almacenan las coordenadas del centro y el lado de la figura.
Recordemos que en la representación basada en cierres léxicos cada objeto se encarga
de definir las funciones que lo manejan. Por tanto, dentro de cada objeto habrá que añadir
funciones para dilatarlo y trasladarlo. El valor que devuelvan será irrelevante, ya que las
emplearemos por su efecto lateral: modificar los valores de las ligaduras del cierre léxico, es decir,
del objeto. Concretamente, las funciones que debemos añadir son:
a) Para dilatar la figura: si el lado era L, ahora será alfa*L; el centro sigue igual.
b) Para trasladar la figura a un nuevo centro: el centro ahora será (x', y'). El lado sigue igual
c) Para trasladar la figura cierta distancia: si el centro era (x, y), ahora será (x + deltax, y + deltay).
El lado sigue igual.
Otra diferencia con R.4.3. es que ahora las operaciones sobre los objetos requieren
argumentos, que habrán de pasarse en el mensaje. Cada operación puede requerir un número
diferente de argumentos: por ejemplo, dilatar requiere uno sólo (alfa), mientras que nuevo-centro
requiere dos (x' e y'), al igual que trasladar (deltax y deltay). Ello significa que la función SEND tiene
un número variable de argumentos, lo cual se puede implementar sencillamente como
(DEFUN SEND (MSJ OBJ &REST ARGS)
(APPLY OBJ (CONS MSJ ARGS)))
Y si el lector recuerda la definición de APPLY –cuya justificación aparece ahora patente- podrá
escribir de forma más compacta
(DEFUN SEND (MSJ OBJ &REST ARGS)
(APPLY OBJ MSJ ARGS))
El constructor de triángulos será
(DEFUN HACER-TRIANGULO (X Y L)
#'(LAMBDA (MSJ &REST ARGS)
(CASE MSJ
((LADO) L)
((CENTROX) X)
((CENTROY) Y)
((ALTURA) (* 0.5 (SQRT 3) X))
((AREA) (* 0.5 L (* 0.5 (SQRT 3) X)))
((PERIMETRO) (* 3 L))
((DILATAR) (SETQ L (* L (CAR ARGS))))
((NUEVO-CENTRO)
(LET ((XPRIMA (CAR ARGS))
(YPRIMA (CADR ARGS)))
(SETQ X XPRIMA
Y YPRIMA)))
((TRASLADAR)
(LET ((DELTAX (CAR ARGS))
(DELTAY (CADR ARGS)))
(SETQ X (+ X DELTAX)
Y (+ Y DELTAY)))))))
Inteligencia Artificial e I.C. 2002/2003
7.10
Dpto. L.C.C (Universidad de. Málaga).
Y el constructor de cuadrados será
(DEFUN HACER-CUADRADO (X Y L)
#'(LAMBDA (MSJ &REST ARGS)
(CASE MSJ
((LADO) L)
((CENTROX) X)
((CENTROY) Y)
((AREA) (* L L))
((PERIMETRO) (* 4 L))
((DILATAR) (SETQ L (* L (CAR ARGS))))
((NUEVO-CENTRO)
(LET ((XPRIMA (CAR ARGS))
(YPRIMA (CADR ARGS)))
(SETQ X XPRIMA
Y YPRIMA)))
((TRASLADAR)
(LET ((DELTAX (CAR ARGS))
(DELTAY (CADR ARGS)))
(SETQ X (+ X DELTAX)
Y (+ Y DELTAY)))))))
Ahora podremos mandener un diálogo como el siguiente:
(SETQ T1 (HACER-TRIANGULO 0 10 100))
=> #<closure 1 #xE46340>
(SEND 'LADO T1)
=> 100
(SEND 'DILATAR T1 10)
=> 1000
(SEND 'LADO T1)
=> 1000
(SEND 'TRASLADAR T1 10 0)
=> 10
(SEND 'CENTROX T1)
=> 10
(SEND 'CENTROY T1)
=> 10
(SEND 'NUEVO-CENTRO T1 200 20)
=> 20
(SEND 'CENTROX T1)
=> 200
(SEND 'CENTROY T1)
=> 20
NOTA. El lector puede apreciar que atributos y métodos son tratados de la misma forma en esta
implementación.
Inteligencia Artificial e I.C. 2002/2003
7.11
Dpto. L.C.C (Universidad de. Málaga).
R.7.6. Asignación a lugares. Tablas hash.
a) Empleando tablas hash, reimplementar la función MEMORIZA de R.7.3.
b) Supongamos el árbol de decisión de la figura 7.1. Representarlo en LISP, empleando para ello
una tabla hash que asocie etiquetas de nodo con cierres léxicos. Implementar una función
(INVESTIGAR etiqueta) que mantenga el diálogo necesario a partir del nodo etiqueta hasta
llegar a una conclusión.
¿Es un animal?
SI
NO
¿Corre?
¿Es un vegetal?
SI NO
Gacela
Tortuga
SI
Lechuga
NO
La piedra de Rosetta
Figura 7.1
**************************
SOLUCION:
CommonLisp proporciona el tipo de datos predefinido tabla hash. Para crear una tabla se emplea la
forma MAKE-HASH-TABLE:
(MAKE-HASH-TABLE &KEY (:test #'eql) (:size 10)
(:rehash-size 1.3) (:rehash-threshold 0.7))
Esta forma crea una nueva tabla y la devuelve como valor. :test debe ser EQ, EQL o
EQUAL (por defecto EQL). :size indica aproximadamente el tamaño inicial de la tabla. Cuando la
tabla está llena en un procentaje mayor o igual a :rehash-threshold, se aumenta
automáticamente su tamaño en el factor :rehash-size. Los valores por defecto de estos tres
parámetros dependen de la implementación.
Para acceder a un elemento de la tabla se emplea la función GETHASH
(GETHASH clave tabla- hash &OPTIONAL valor-defecto)
La función devuelve el valor asociado con clave en tabla-hash, o bien valor-defecto, si no se ha
encontrado ninguno.
¿Cómo se almacenan valores en la tabla? El valor asociado a una clave en una tabla es un
lugar. Como ya mencionamos en R.7.1, la forma SETF no sólo modifica las ligaduras de los
símbolos, sino que también puede alterar el contenido de cualquier lugar. Por tanto, para escribir
que valor está asociado a clave en la tabla tabla-hash emplearemos la construcción
(SETF (GETHASH clave tabla- hash) valor).
Por ejemplo
(SETQ T1 (MAKE-HASH-TABLE))
=> #<HASH-TABLE #xE34454>
(GETHASH 'PEPE T1)
=> NIL
NIL
(SETF (GETHASH 'PEPE T1) 18)
=> 18
(GETHASH 'PEPE T1)
=> 18
T
(SETF (GETHASH 'PEPE T1) 14)
=> 14
(GETHASH 'PEPE T1)
=>14
T
Inteligencia Artificial e I.C. 2002/2003
7.12
Dpto. L.C.C (Universidad de. Málaga).
a) Según esto la implementación pedida será
(DEFUN MEMORIZA (FUNCION)
(LET ((TF (MAKE-HASH-TABLE :TEST #'EQUAL)))
#'(LAMBDA (X)
(LET ((CALCULADO (GETHASH X TF)))
(IF CALCULADO
CALCULADO
(LET ((OTRO-VALOR (FUNCALL FUNCION X)))
(SETF (GETHASH X TF) OTRO-VALOR)))))))
NOTA. GETHASH devuelve también un valor secundario: “falso” si no hay valor asociado a la clave,
“verdadero” en otro caso. De esta forma se puede distinguir entre un valor NIL asociado a una
clave y un valor NIL que indica que no hay valor asociado a la clave.
(SETF (GETHASH 'JUAN T1) NIL)
=> NIL
(GETHASH 'JUAN T1)
=> NIL
T
b) La idea fundamental es que cada nodo es una función sin argumentos que se encarga de
-realizar al usuario la pregunta que corresponde a ese nodo;
-en función de su respuesta, dar la solución. Para ello será necesario llamar a los nodos hijos del
nodo actual.
Por simplificar, supondremos que las respuestas válidas del usuario son “SI” y “NO”.
Para representar el árbol de decisión en conjunto, identificaremos cada nodo con una etiqueta
NOMBRE, y emplearemos una tabla hash indexada por NOMBRE. La tabla será el valor de una
variable global *NODOS*
(DEFVAR *NODOS* (MAKE-HASH-TABLE))
La función DEFNODO nos servirá para construir los nodos y almacenarlos en la tabla hash. DEFNODO
tendrá como argumentos la etiqueta del nodo, el mensaje asociado al nodo y las etiquetas de sus
dos hijos. Obviamente, si el nodo es un nodo hoja, es decir, corresponde a una respuesta final,
DEFNODO no recibirá las etiquetas de los hijos. De esta forma, la lista-lambda de DEFNODO será
(DEFUN DEFNODO (&KEY NOMBRE CONTENIDO NODO-SI NODO-NO) ...
En nuestro ejemplo, las llamadas necesarias a DEFNODO serán
;;un arbol de decision concreto
(DEFNODO :NOMBRE 'INICIO :CONTENIDO "¨Animal?" :NODO-SI 'ANIMAL
:NODO-NO 'NO-ANIMAL)
(DEFNODO :NOMBRE 'ANIMAL :CONTENIDO "¨Corre?" :NODO-SI 'CORREDOR
:NODO-NO 'NO-CORREDOR)
(DEFNODO :NOMBRE 'NO-ANIMAL :CONTENIDO "¨Vegetal?" :NODO-SI 'VEGETAL
:NODO-NO 'MINERAL)
(DEFNODO :NOMBRE 'CORREDOR :CONTENIDO "Gacela")
(DEFNODO :NOMBRE 'NO-CORREDOR :CONTENIDO "Tortuga")
(DEFNODO :NOMBRE 'VEGETAL :CONTENIDO "Lechuga")
(DEFNODO :NOMBRE 'MINERAL :CONTENIDO "La piedra de Rosetta")
El valor que devuelve la función es el siguiente:
-si el nodo es una hoja, es decir, si el argumento opcional NODO-SI no figura en la llamada,
DEFNODO devuelve la función sin argumentos que devuelve el mensaje almacenado en
CONTENIDO.
-en otro caso, DEFNODO devuelve la función sin argumentos que lee la respuesta del usuario y, en
función de ella, devuelve el valor que devolvería NODO-SI o el que devolvería NODO-NO.
Definimos también una función INVESTIGAR que tiene como argumento el nombre del nodo inicial
de la búsqueda. En resumen,
Inteligencia Artificial e I.C. 2002/2003
7.13
Dpto. L.C.C (Universidad de. Málaga).
(DEFUN DEFNODO (&KEY NOMBRE CONTENIDO NODO-SI NODO-NO)
(SETF (GETHASH NOMBRE *NODOS*)
(IF NODO-SI
#'(LAMBDA ()
(FORMAT T "~%~A~%>> " CONTENIDO)
(CASE (READ)
(SI (INVESTIGAR NODO-SI))
(NO (INVESTIGAR NODO-NO))
(T (ERROR "Respuesta no valida."))))
#'(LAMBDA () CONTENIDO))))
(DEFUN INVESTIGAR (NOMBRE-NODO)
(FUNCALL (GETHASH NOMBRE-NODO *NODOS*)))
Ahora tendremos, por ejemplo,
(INVESTIGAR 'INICIO)
¿Animal?
>> SI
¿Corre?
>> NO
"Tortuga"
Inteligencia Artificial e I.C. 2002/2003
7.14
Dpto. L.C.C (Universidad de. Málaga).
R.7.7. Estructuras.
Definir una estructura para representar los estados del problema de misioneros y caníbales.
**************************
SOLUCION:
CommonLisp proporciona la posibilidad de definir nuevos tipos de datos estructurados llamados
estructuras. Para definir una estructura se emplea la forma DEFSTRUCT:
(defstruct nombre {descripción-slot}*)
donde cada descripción-slot es de la forma nombreslot o bien de la forma
(nombreslot valor-inicial)
DEFSTRUCT tiene las siguientes efectos laterales:
-define procedimientos de lectura para los valores de los slots (ranuras o atributos) llamados
nombre-nombreslot.
-permite que SETF pueda modificar los valores de los slots.
-define un predicado de tipo (llamado nombre-p)
-define un constructor del tipo nombre llamado make-nombre. Este constructor tiene tantos
argumentos clave como slots se hayan definido.
Un ejemplo aclarará lo anterior:
(defstruct alumno
nombre
(diligencia 'ESTUDIOSO)
(curso 4))
=> ALUMNO
(defvar *alumno-1*
(make-alumno :nombre 'pepe :curso 5))
=> *ALUMNO-1*
(defvar *alumno-2*
(make-alumno))
=> *ALUMNO-2*
(alumno-curso *alumno-1*)
=> 5
(alumno-diligencia *alumno-2*)
=> ESTUDIOSO
(setf (alumno-diligencia *alumno-2*) 'VAGO)
=> VAGO
(alumno-diligencia *alumno-2*)
=> VAGO
(alumno-p *alumno-2*)
=> T
(alumno-p '(list 'juan 'ESTUDIOSO 4))
=> NIL
Así que lo que nos piden es simplemente
(defstruct estado-mc
b
m
c)
=> ESTADO-MC
Inteligencia Artificial e I.C. 2002/2003
7.15
Dpto. L.C.C (Universidad de. Málaga).
R.7.8. Objetos con herencia.
a) Empleando tablas hash para representar los objetos, escribir las funciones necesarias para
manejar los centros y lados de triángulos equiláteros y cuadrados.
b) Empleando tablas hash para representar los objetos, reimplementar el ejercicio R.7.6. Añadir
una clase más general FIGURA para la cual se definan las funciones comunes a triángulos y
cuadrados.
**************************
SOLUCION:
a) Representaremos un objeto como un conjunto de pares atributo-método. Este conjunto será
almacenado en una tabla hash indexada por los atributos. Si el valor del atributo es, por ejemplo,
27, el método almacenado será la función que siempre devuelve 27. Nos será útil por tanto la
siguiente definición:
(DEFUN F-CTE (E)
#'(LAMBDA (&REST XS) E))
Los constructores de triángulos y cuadrados serán las siguientes funciones:
(DEFUN HACER-TRIANGULO (&KEY CENTROX CENTROY LADO)
(LET ((OBJ (MAKE-HASH-TABLE)))
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE CENTROX)
(GETHASH 'CENTROY OBJ)
(F-CTE CENTROY)
(GETHASH 'LADO OBJ)
(F-CTE LADO))
OBJ))
(DEFUN HACER-CUADRADO (&KEY CENTROX CENTROY LADO)
(LET ((OBJ (MAKE-HASH-TABLE)))
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE CENTROX)
(GETHASH 'CENTROY OBJ)
(F-CTE CENTROY)
(GETHASH 'LADO OBJ)
(F-CTE LADO))
OBJ))
La función de paso de mensajes debe aplicar el método, correspondiente al mensaje y al objeto, a
los argumentos adicionales:
(DEFUN SEND (MSJ OBJ &REST ARGS)
(APPLY (GETHASH MSJ OBJ) ARGS))
b) Ahora tendremos que definir para cada objeto un método que nos dé su padre. De esta forma,
los constructores de triángulos y cuadrados serán:
(DEFUN HACER-TRIANGULO (&KEY CENTROX CENTROY LADO PADRE)
(LET ((OBJ (MAKE-HASH-TABLE)))
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE CENTROX)
(GETHASH 'CENTROY OBJ)
(F-CTE CENTROY)
(GETHASH 'LADO OBJ)
(F-CTE LADO)
(GETHASH 'PADRE OBJ)
(F-CTE PADRE))
OBJ))
Inteligencia Artificial e I.C. 2002/2003
7.16
Dpto. L.C.C (Universidad de. Málaga).
(DEFUN HACER-CUADRADO (&KEY CENTROX CENTROY LADO PADRE)
(LET ((OBJ (MAKE-HASH-TABLE)))
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE CENTROX)
(GETHASH 'CENTROY OBJ)
(F-CTE CENTROY)
(GETHASH 'LADO OBJ)
(F-CTE LADO)
(GETHASH 'PADRE OBJ)
(F-CTE 'CLASE-CUADRADO))
OBJ))
La función de paso de mensajes debe aplicar al objeto el método correspondiente al mensaje (y los
argumentos adicionales). Este método puede que esté almacenado directamentamente el el objeto
(en cuyo caso nos lo dará la función METODO-NOH), o bien puede que esté almacenado en algún
antepasado (en cuyo caso nos lo dará la función METODO-H):
(DEFUN METODO-NOH (MSJ OBJ)
(GETHASH MSJ OBJ))
(DEFUN METODO-H (MSJ OBJ)
(LET ((M (METODO-NOH MSJ OBJ))
(M-PADRE (METODO-NOH 'PADRE OBJ)))
(COND (M)
(M-PADRE (METODO-H MSJ (FUNCALL M-PADRE OBJ)))
(T (ERROR "No hay método para el mensaje ~S." MSJ)))))
(DEFUN SEND (MSJ OBJ &REST ARGS)
(APPLY (METODO-H MSJ OBJ) OBJ ARGS))
Las clases “triángulo” y “cuadrado” almacenarán distintos métodos para el cálculo de alturas,
perímetros y áreas:
(DEFUN HACER-CLASE-TRIANGULO (&KEY PADRE)
(LET ((CLASE (MAKE-HASH-TABLE)))
(SETF (GETHASH 'PADRE CLASE)
(F-CTE PADRE)
(GETHASH 'ALTURA CLASE)
#'(LAMBDA (OBJ)
(* 0.5 (SQRT 3) (SEND 'LADO OBJ)))
(GETHASH 'AREA CLASE)
#'(LAMBDA (OBJ)
(* (SEND 'ALTURA OBJ) (SEND 'LADO OBJ)))
(GETHASH 'PERIMETRO CLASE)
#'(LAMBDA (OBJ)
(* 3 (SEND 'LADO OBJ))))
CLASE))
(DEFUN HACER-CLASE-CUADRADO (&KEY PADRE)
(LET ((CLASE (MAKE-HASH-TABLE)))
(SETF (GETHASH 'PADRE CLASE)
(F-CTE PADRE)
(GETHASH 'AREA CLASE)
#'(LAMBDA (OBJ)
(* (SEND 'LADO OBJ) (SEND 'LADO OBJ)))
(GETHASH 'PERIMETRO CLASE)
#'(LAMBDA (OBJ)
(* 4 (SEND 'LADO OBJ))))
CLASE))
Inteligencia Artificial e I.C. 2002/2003
7.17
Dpto. L.C.C (Universidad de. Málaga).
La clase “figura” almacenará los métodos de dilatar y trasladar, que son comunes a todas las
clases:
(DEFUN HACER-CLASE-FIGURA (&KEY PADRE)
(LET ((CLASE (MAKE-HASH-TABLE)))
(SETF (GETHASH 'DILATAR CLASE)
#'(LAMBDA (OBJ ALFA)
(SETF (GETHASH 'LADO OBJ)
(F-CTE (* ALFA (SEND 'LADO OBJ)))))
(GETHASH 'NUEVO-CENTRO CLASE)
#'(LAMBDA (OBJ X1 Y1)
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE X1)
(GETHASH 'CENTROY OBJ)
(F-CTE Y1)))
(GETHASH 'TRASLADAR CLASE)
#'(LAMBDA (OBJ DX DY)
(SETF (GETHASH 'CENTROX OBJ)
(F-CTE (+ DX (GETHASH 'CENTROX OBJ)))
(GETHASH 'CENTROY OBJ)
(F-CTE (+ DY (GETHASH 'CENTROY OBJ))))))
CLASE))
Ahora podemos tener la siguiente sesión:
(SETQ F (HACER-CLASE-FIGURA))
=> #<HASH-TABLE #xE47458>
(SETQ T1 (HACER-CLASE-TRIANGULO :PADRE F))
=> #<HASH-TABLE #xE58E80>
(SETQ T2 (HACER-TRIANGULO :CENTROX 1 :CENTROY 10 :LADO 100 :PADRE T1))
=> #<HASH-TABLE #xE6CBDC>
(SEND 'PERIMETRO T2)
=> 300
(SEND 'DILATAR T2 2)
=> #<closure 0 #xE48F50>
(SEND 'LADO T2)
=> 200
(SEND 'LADO T1)
=> Error: No hay método para el mensaje LADO.
NOTA. El lector puede apreciar que en esta implementación clases e instancias son tratados de la
misma forma.
Inteligencia Artificial e I.C. 2002/2003
7.18
Dpto. L.C.C (Universidad de. Málaga).
R.7.9. Uso de arrays: Conecta-4.
a) Empleando arrays definir un tipo de datos tablero para el juego del conecta-4.
b) Definir las siguientes operaciones sobre tableros:
• (tablero-pos-valida tab f c), devolverá cierto si la posición (f,c) pertenece
al tablero tab, y falso en otro caso.
• (tablero-columna-libre tab c), devolverá cierto si la columna c no está
llena en el tablero tab, y falso en otro caso.
• (ver-tablero tab), que no devuelva ningún valor, y muestre adecuadamente el
contenido de un tablero por pantalla.
c) Empleando estructuras definir un tipo de datos estado para el juego del conecta-4.
d) Definir las siguientes operaciones sobre estados:
• (elegir-suc-nth c e), devolverá un nuevo estado resultante de soltar una ficha
en la columna c del tablero.
• (agotado e), devolverá cierto si el tablero del estado e está lleno.
• (finalp e), devolverá 1 si el estado e es final y ganador para el jugador 1, -1 si es
final y ganador para el jugador 2, o NIL en otro caso.
e) Escribir una función conecta-4 encargada de coordinar y mostrar por pantalla una partida de
conecta-4. La función recibirá como argumento dos funciones (max y min), encargadas de
proporcionar los movimientos de uno de los jugadores, mostrará por pantalla la evolución del
juego después de cada jugada, y terminará cuando uno de los dos jugadores haya ganado el
juego o el tablero esté completamente lleno.
El valor devuelto por la función será 1 si ganó max, -1 si ganó min, y 0 si hubo empate.
Las funciones de los jugadores recibirán como argumentos el estado del tablero y el último
movimiento realizado por el oponente, y devolverán el movimiento que desean realizar
**************************
SOLUCION:
Common Lisp proporciona el tipo de datos predefinido array. Para crear un array se emplea la
forma MAKE-ARRAY:
(MAKE-ARRAY dimensiones &KEY param-clave)
Esta forma crea un nuevo array y lo devuelve como valor. El parámetro necesario
dimensiones debe ser una lista de enteros no negativos que serán las dimensiones del array. La
longitud de la lista define la dimensionalidad del array. Entre los parámetros clave se encuentran
:element-type, :initial-element, e :initial-contents. El valor por defecto de
:element-type es T, lo cual significa que el contenido de las posiciones del array puede ser
cualquier objeto Lisp. Los parámetros :initial-element, e :initial-contents permiten
especificar el contenido inicial del array y son excluyentes. Si no se emplea ninguno de ellos el
contenido inicial del array no está definido. En el caso de :initial-element se debe
proporcionar un objeto del tipo definido por :element-type , que pasará a ser el contenido inicial
en todas las posiciones del array. En el caso de :initial-contents se puede proporcionar una
lista de listas anidadas que proporcionen el contenido de cada posición del array.
Para acceder a un elemento de un array se emplea la función AREF
(AREF array &REST subíndices)
La función devuelve el objeto guardado en la posición del array especificada por los subíndices. El
número de subíndices debe coincidir con la dimensionalidad del array, y cada uno debe encontrarse
dentro del rango correspondiente.
¿Cómo se almacenan valores en un array? Cada posición de un array es un lugar. Como ya
mencionamos en R.7.1, la forma SETF no sólo modifica las ligaduras de los símbolos, sino que
también puede alterar el contenido de cualquier lugar. Por tanto, para escribir que valor está
asociado a cada posición del array emplearemos la construcción
(SETF (AREF array subíndice*) valor).
Para concer la dimensionalidad de un array se emplea la función ARRAY-RANK
(ARRAY-RANK array).
Para conocer la longitud de una dimensión concreta del array se utiliza la función ARRAYDIMENSION
(ARRAY-DIMENSION array dimensión).
Inteligencia Artificial e I.C. 2002/2003
7.19
Dpto. L.C.C (Universidad de. Málaga).
La función EQUALP aplicada a dos arrays devolverá T si ambos tienen el mismo número de
dimensiones, las dimensiones coinciden y los elementos correspondientes son todos EQUALP.
Por ejemplo:
(SETQ A1 (MAKE-ARRAY ‘(2) :initial-element 25))
=> #<ARRAY #xf323>
(AREF A1 0)
=> 25
(SETF (AREF A1 0) 18
(AREF A1 1) 14)
=> 14
(AREF A1 0)
=> 18
(AREF A1 1)
=>14
(ARRAY-RANK A1)
=>1
(ARRAY-DIMENSION A1 0)
=>2
(SETQ A2 (MAKE-ARRAY ‘(2 3) :initial-contents ‘((1 2 3) (4 5 6))))
=> #<ARRAY #xf442>
(AREF A2 1 2)
=> 6
a) Cada tablero será un array de *nfilas* filas y *ncolumnas* columnas. Cada casilla del
tablero podrá estar vacía o contener una ficha que podrá ser de tipo ‘max’ o ‘min’.
(defvar *nfilas*)
(setf *nfilas* 6)
(defvar *ncolumnas*)
(setf *ncolumnas* 7)
(defconstant *vacio* '_)
(defconstant *jugador-1* 1)
(defconstant *jugador-2* 0)
;posición vacía del tablero
;ficha del jugador 1
;ficha del jugador 2
Como constructores consideraremos varias funciones. La primera, hacer-tablero-vacio,
creará un tablero nuevo con las casillas vacías. La segunda, copiar-tablero, devolverá un
tablero nuevo con las mismas dimensiones y contenidos que otro dado. Por último, tablerosoltar-ficha recibirá un tablero, una columna y una ficha, y devolverá dos valores: un nuevo
tablero igual que el original y donde además se ha soltano una ficha por la columna indicada; y
adicionalmente la fila donde quedó colocada la ficha.
(defun hacer-tablero-vacio (&optional (nfilas *nfilas*)
(ncolumnas *ncolumnas*))
(make-array (list nfilas ncolumnas) :initial-element *vacio*))
(defun copy-tablero (tab)
(let ((tab2 (make-array (array-dimensions tab))))
(dotimes (f (array-dimension tab 0))
(dotimes (c (array-dimension tab 1))
(setf (aref tab2 f c) (aref tab f c))))
tab2))
Inteligencia Artificial e I.C. 2002/2003
7.20
Dpto. L.C.C (Universidad de. Málaga).
(defun tablero-soltar-ficha (tab-orig c ficha)
(let ((tab (copy-tablero tab-orig)))
(dotimes (f (array-dimension tab 0))
(when (equal *vacio* (aref tab f c))
(setf (aref tab f c) ficha)
(return (values tab f))))))
Definiremos también selectores para consultar las dimensiones de un tablero, el contenido de una
posición dada, y si una posición dada pertenece o no al tablero.
(defun tablero-nfilas
(tab) (array-dimension tab 0))
(defun tablero-ncolumnas (tab) (array-dimension tab 1))
(defun tablero-contenido (tab f c) (aref tab f c))
b) Las definiciones serían las siguientes:
(defun tablero-pos-valida (tab f c)
(and (<= 0 f (1- (tablero-nfilas tab)))
(<= 0 c (1- (tablero-ncolumnas tab))))
(defun tablero-columna-libre (tab c)
(equal *vacio*
(tablero-contenido tab (1- (tablero-nfilas tab)) c)))
Para ver un tablero mostraremos su contenido adecuadamente: la primera fila será la inferior, y
la última la superior. Además mostraremos debajo de cada columna su índice (del 0 al 9).
(defun ver-tablero (tab)
(let ((nfilas (tablero-nfilas tab))
(ncolumnas (tablero-ncolumnas tab)))
;muestra el contenido del tablero en el primer cuadrante
(dotimes (f nfilas tab)
(format t "~%
")
(dotimes (c ncolumnas)
(princ (tablero-contenido tab (- nfilas (1+ f)) c))))
;muestra debajo de cada columna su índice
(format t "~%
")
(dotimes (c ncolumnas) (princ #\=))
(format t "~%
")
(dotimes (c ncolumnas) (princ (mod c 10)))
(terpri)
(values)))
c) La función conecta-4 conservará una representación del estado del juego para poder
mostrarla por pantalla, consultar al jugador en su turno, e informarle del último movimiento
realizado. Para ello definiremos un tipo de datos estado de la siguiente forma:
(defstruct estado
"situación del juego en un momento dado"
(it
0)
;número de jugadas realizadas
(jug1? nil)
;¿le toca jugar a jug1?
(tablero nil)
;contenido del tablero
(ultimo-mov nil)) ;ultimo movimiento realizado (posición marcada)
Inteligencia Artificial e I.C. 2002/2003
7.21
Dpto. L.C.C (Universidad de. Málaga).
(defun hacer-estado-inicial
(make-estado :it
:jug1?
:tablero
:ultimo-mov
()
0
T
(hacer-tablero-vacio)
nil))
(defun ver-estado (e)
(format t "~%It: ~S *****~%" (estado-it e))
(ver-tablero (estado-tablero e))
(format t "~%Le toca al jugador ~S~%" (if (estado-jug1? e) 1 2))
(values))
Cada vez que un jugador propone un movimiento (en este caso soltar una ficha en una
columna), es necesario comprobar que el movimiento es válido y a continuación actualizar el
estado del juego.
(defun posiblep (n e)
"¿Es posible soltar una ficha en la columna n en el estado e?"
(tablero-columna-libre (estado-tablero e) n))
;;elegir-suc-nth (n e)
;; Devuelve un estado resultado de que el jugador actual deje caer
;; una ficha por la columna c del tablero, nil si no es posible
(defun elegir-suc-nth (n e)
(multiple-value-bind (tab2 f)
(tablero-soltar-ficha (estado-tablero e) n
(if (estado-jug1? e) *jugador-1*
*jugador-2*))
(make-estado :it
(1+ (estado-it e))
:jug1?
(not (estado-jug1? e))
:tablero
tab2
:ultimo-mov (list f n))))
Cada vez que un jugador realiza un movimiento será necesario comprobar si ha ganado el
juego, para ello definiremos la función finalp, que devolverá la ficha del jugador del turno
anterior si este ganó el juego, y falso en otro caso. Para comprobar que hay 4 fichas conectadas
en diagonal, horizontal y vertical sólo será necesario comprobarlo a partir del último movimiento.
Para ello definiremos la operación tablero-conectadas.
(defvar *long-ganadora*)
(setf *long-ganadora* 4)
;núm. de fichas conectadas para ganar
(defun agotado (e) "¿está lleno el tablero del estado e?”
(= (estado-it e) (* (tablero-ncolumnas (estado-tablero e))
(tablero-nfilas
(estado-tablero e)))))
Inteligencia Artificial e I.C. 2002/2003
7.22
Dpto. L.C.C (Universidad de. Málaga).
(defun finalp (e) “Devuelve el ganador (1, -1) o NIL si no hay”
(let* ((mov (estado-ultimo-mov e))
(f
(car mov))
(c
(cadr mov))
(ficha (if (estado-jug1? e) *jugador-2* *jugador-1*))
(tab (estado-tablero e)))
(when (and mov
(<= *long-ganadora*
(max (tablero-conectadas tab f c 1 0 ficha)
(tablero-conectadas tab f c 0 1 ficha)
(tablero-conectadas tab f c -1 1 ficha)
(tablero-conectadas tab f c 1 1 ficha))))
ficha)))
(defun tablero-conectadas (tab f c df dc
&optional (ficha (tablero-contenido tab f c)))
"número de fichas conectadas con (f,c) en la dirección df, dc"
(+ (tablero-contar tab f c df dc ficha)
(tablero-contar tab f c (- df) (- dc) ficha)
-1)) ; (f,c) se cuenta 2 veces
(defun tablero-contar (tab f c df dc ficha &optional (ac 0))
(if (and (tablero-pos-valida tab f c)
(equal (tablero-contenido tab f c) ficha))
(tablero-contar tab (+ f df) (+ c dc) df dc ficha (1+ ac))
ac))
Podemos definir la función encargada de coordinar el juego del siguiente modo utilizando
recursión por la cola:
(defun conecta4 (jugador-1 jugador-2
&optional (e (hacer-estado-inicial)))
(ver-estado e)
(cond ((finalp e) (if (estado-jug1? e) -1 1)) ;ganador
((agotado e) 0)
;empate
(t
(conecta4 jugador-1 jugador-2
(elegir-suc-nth (if (estado-jug1? e)
(funcall jugador-1 e)
(funcall jugador-2 e))
e)))))
Por último podemos definir dos funciones para los jugadores. La primera símplemente pide por
pantalla un movimiento y lo lee de teclado. La segunda mueve aleatoriamente:
(defun estrategia0 (e)
(format t "~%¿En qué columna soltamos la ficha?
(let ((c (read)))
(if (posiblep c e) c (estrategia0 e))))
:
")
(defun estrategia-random (e)
(let ((c (random (tablero-ncolumnas (estado-tablero e)))))
(if (posiblep c e) c (estrategia-random e))))
De momento podemos jugar con la máquina empleando la llamada:
(conecta4 #'estrategia-random #'estrategia0)
Más adelante desarrollaremos un adversario más digno para nuestra inteligencia.
Inteligencia Artificial e I.C. 2002/2003
7.23
Dpto. L.C.C (Universidad de. Málaga).
EJERCICIOS PROPUESTOS.
P.7.1. Evaluar las siguientes expresiones en el orden indicado:
(DEFUN KK () (+ *X* *Y*))
(DEFUN QQ (*X* *Y*) (SETQ *X* (KK) *Y* (KK)))
(DEFVAR *X* 1000)
(DEFCONSTANT MI-PI 3.14)
(SETQ *Y* 2 *X* *Y*)
(SETQ MI-PI 3.1416)
(SETQ *Z* (KK) *X* 100 *Y* (KK))
(QQ *Y* *X*)
P.7.2. Escribir con LET, LOOP y SETQ expresiones equivalentes a
a)
(DOTIMES (N VECES NIL) (PRINT N) (PRIN (* N N)))
b)
(DO ((N 0 (1+ N))
(L NIL (CONS N L)))
((> N 5) L))
P.7.3. Definir un procedimiento de “memorización” válido también para las llamadas recursivas de
la función memorizada, y aplicarlo a la versión recursiva “ingenua” de la función (FIB N), que da
el enésimo elemento de la sucesión de Fibonacci.
P.7.4. a) Definir mediante un cierre léxico una cuenta corriente. La cuenta se creará con un saldo
inicial; a partir de entonces podrán ingresarse otras cantidades, consultarse el saldo en cada
momento, o retirar fondos por un importe no superior al saldo existente.
b) Añadir la posibilidad de producir intereses, que se añadirán cada vez que se consulte el saldo.
La tasa de interés se indicará opcionalmente (0 por defecto) al consultar el saldo.
P.7.5. Implementar mediante cierres léxicos el tipo de datos 'conjunto difuso' del ej. P.2.9.
P.7.6. Implementar mediante cierres léxicos un generador de números aleatorios.
P.7.7. Escribir una función 3-en-raya, análoga a la definida en R.7.9 para el juego del conecta-4,
encargada de coordinar y mostrar por pantalla una partida de 3-en-raya.
P.7.8. Escribir una función othello, análoga a la definida en R.7.9 para el juego del conecta-4,
encargada de coordinar y mostrar por pantalla una partida de Othello.
NOTA: El Othello se juega en un tablero de 8x8 que inicialmente contiene 4 fichas en el centro.
Dos jugadores alternan sus movimientos, cada uno con un color de ficha diferente. Las fichas
pueden colocarse sobre el tablero pero no moverse. Cada nueva ficha debe colocarse de modo
que “encierre” una o más fichas del oponente, es decir, debe haber una línea (horizontal, diagonal,
o vertical) que vaya desde la ficha recién colocada, a través de una o más fichas del oponente,
hasta otra ficha del mismo jugador. Las fichas del oponente se ven sustituidas entonces por otras
tantas del jugador. Si se “encierran” varias fichas del oponente en un mismo movimiento, todas
ellas cambian de color. Cuando un jugador no puede mover debe pasar el turno. El juego termina
cuando ningún jugador puede colocar nuevas fichas, y gana el que tenga más fichas de su color en
el tablero. A continuación se muestra una secuencia válida de movimientos al inicio de una partida:
Inteligencia Artificial e I.C. 2002/2003
7.24
Dpto. L.C.C (Universidad de. Málaga).
Descargar