2.1 Objetivo de la práctica
Para hacer esta práctica entra en un entorno de desarrollo de Lisp (p.e. el Allegro Common Lisp de PC, Mac o Unix) y vete leyendo atentamente las explicaciones para hacer el programa y comprueba incrementalmente su funcionamiento ejecutando código en el entorno de Lisp. Cuando encuentres alguna función que no conozcas dirígete a cualquier documentación de referencia de Common Lisp (algún libro de texto como el Winston, el Steele o accede a la ayuda del entorno de programación).
Además de comprender todo el código que se describe en la práctica, deberás realizar las tareas que se indican con el símbolo ".-".
2.2 Un programa sencillo en Common Lisp
2.2.1 Objetivos del programa: Generación automática de frases en inglés
El programa que se desarrollará en esta práctica genera sentencias aleatorias en inglés. Esta es una gramática simple para una pequeña porción de inglés:
Sentencia ^ Nombre_de _frase + Verbo_de
_frase
Nombre_de _frase ^ Artículo + Nombre
Verbo_de _frase ^ Verbo + Nombre_de _frase
Artículo ^the, a, …
Nombre ^ man, ball, woman, table …
Verbo ^ hit, took, saw, liked …
Para ser técnicos, esta descripción se llama gramática
de estructura-de-frase de contexto-libre, y el paradigma subyacente
se llama sintaxis generativa. La idea es que en cualquier sitio
donde se requiere una sentencia, podemos generarla con el nombre
de la frase seguido por el verbo de la frase. Donde se requiera el nombre
de la frase podemos poner en su lugar un artículo seguido
de un nombre. Donde se especifique un artículo, podemos
poner "the", "a", o cualquier otro artículo. El formalismo
es de contexto-libre porque las reglas se aplican en cualquier sitio
independientemente de las palabras que le rodean, y la aproximación
es generativa porque las reglas en conjunto definen el conjunto
completo de sentencias del lenguaje (y por contraste el conjunto de no-sentencias
también). A continuación mostramos la derivación de
una sentencia simple utilizando las anteriores reglas:
La aproximación más elemental es representar cada regla gramatical por una función Lisp separada:
(defun Sentencia ()
(append (Nombre-frase) (Verbo-frase)))
(defun Nombre-frase () (append
(Articulo) (Nombre)))
(defun Verbo-frase ()
(append (Verbo) (Nombre-frase)))
(defun Articulo ()
(uno-de '(the a)))
(defun Nombre ()
(uno-de '(man ball woman table)))
(defun Verbo ()
(uno-de '(hit took saw liked)))
Algunas de estas definiciones utilizan la función
uno-de que recibe como argumento posibles valores y devuelve uno elegido
aleatoriamente metido en una lista. De esta forma se puede aplicar append
a todas las categorías.
(defun uno-de (conjunto)
"Elige un elemento del conjunto
y lo mete en una lista."
(list (random-elt conjunto)))
(defun random-elt (posibilidades)
"Elige aleatoriamente un
elemento de una lista."
(elt posibilidades(random
(length posibilidades))))
Notar que la función elt no es más que la nth (enésimo elemento) con los parámetros intercambiados: (elt lista n) y (nth n lista).
Ahora podemos hacer una prueba generando unas pocas sentencias y partes de sentencia:
> (sentencia) ->(A BALL
LIKED A MAN)
> (sentencia) ->(A MAN
SAW A WOMAN)
> (sentencia) ->(THE
BALL HIT A MAN)
> (sentencia) ->(A BALL
HIT A WOMAN)
> (sentencia) ->(A WOMAN
TOOK A TABLE)
> (sentencia) ->(THE
BALL HIT A TABLE)
> (sentencia) ->(THE
BALL SAW A TABLE)
> (sentencia) ->(THE MAN TOOK
A TABLE)
> (sentencia) ->(THE
BALL SAW THE TABLE)
> (sentencia) ->(THE
BALL LIKED A TABLE)
> (sentencia) ->(THE
WOMAN SAW THE TABLE)
> (nombre-frase) ->(A TABLE)
> (verbo-frase) ->(HIT A WOMAN)
> (nombre-frase) ->(THE WOMAN)
> (verbo-frase) ->(TOOK THE
TABLE)
> (verbo-frase) ->(SAW A BALL)
> (verbo-frase) ->(HIT A BALL)
>
> (trace nombre-frase verbo-frase
articulo nombre verbo)
(NOMBRE-FRASE VERBO-FRASE ARTICULO
NOMBRE VERBO)
>
> (sentencia)
; 1> NOMBRE-FRASE called with no
args
; 2> ARTICULO called with no args
; 2< ARTICULO returns value:
;
(THE)
; 3> NOMBRE called with no args
; 3< NOMBRE returns value:
;
(BALL)
; 1< NOMBRE-FRASE returns value:
;
(THE BALL)
; 4> VERBO-FRASE called with no
args
; 5> VERBO called with no args
; 5< VERBO returns value:
;
(TOOK)
; 6> NOMBRE-FRASE called with no
args
; 7> ARTICULO called with no args
; 7< ARTICULO returns value:
;
(A)
; 8> NOMBRE called with no args
; 8< NOMBRE returns value:
;
(MAN)
; 6< NOMBRE-FRASE returns value:
;
(A MAN)
; 4< VERBO-FRASE returns value:
;
(TOOK A MAN)
(THE BALL TOOK A MAN)
>
Estas funciones trabajan bien y la traza tiene el estilo de la que derivamos anteriormente pero las definiciones de funciones son más duras de leer que las reglas gramaticales originales. Este problema surgirá con más intensidad al considerar reglas más complejas. Supongamos que queremos permitir que los nombres de frases puedan ser modificados por un número indefinido de adjetivos y un número indefinido de frases preposicionales. En notación gramatical, podemos considerar las siguientes nuevas reglas:
Nombre_de _frase ^ Artículo
+ Adj* + Nombre + PP*
Nombre_de _frase ^ Artículo
+ Adj* + Nombre + PP*
Adj* ^ 0, Adj + Adj*
PP* ^ 0, PP + PP*
PP ^ Prep + Nombre-frase
Adj ^ big, little, blue, green,
…
Prep ^ to, in, by, with, …
En esta notación ( ) indica la posibilidad de ningún elemento, una coma indica una elección de entre varias alternativas, los nombres terminados en un asterisco denotan cero o más repeticiones del nombre (esto es, PP* denota cero o más repeticiones de PP).
El problema aquí es que las reglas para Adj* y PP* contienen elecciones que deben programarse con algún mecanismo condicional. Por ejemplo:
(defun Adj* ()
(if (= (random 2) 0)
nil
(append
(Adj) (Adj*))))
(defun PP* ()
(if (random-elt '(t nil))
(append
(PP) (PP*))
nil))
(defun Nombre-frase () (append
(Articulo) (Adj*) (Nombre) (PP*)))
(defun PP () (append (Prep)
(Nombre-frase)))
(defun Adj () (uno-de '(big
little blue green adiabatic)))
(defun Prep () (uno-de '(to
in by with on)))
Para definir la gramática hemos empezado con funciones simples pero se están convirtiendo cada vez más complejas. Para comprenderlas, necesitamos conocer muchas convenciones de Lisp (defun, case, if, quote, reglas de evaluación, …) cuando idealmente la implementación de unas reglas gramaticales deberían utilizar sólo convenciones linguísticas. Si quisiéramos desarrollar una gramática más grande, el problema podría ser peor porque las reglas dependerían cada vez más de Lisp.
2.2.3 Una solución basada en reglas
Una implementación alternativa de este programa podría concentrarse en facilitar la escritura de las reglas gramaticales y dejar para más adelante la preocupación sobre como pueden ser procesadas. Hechemos un vistazo otra vez a las reglas gramaticales originales:
(defparameter *gramatica-simple*
'((Sentencia -> (Nombre-frase
Verbo-frase))
(Nombre-frase
-> (Articulo Nombre))
(Verbo-frase
-> (Verbo Nombre-frase))
(Articulo ->
the a)
(Nombre -> man
ball woman table)
(Verbo -> hit
took saw liked))
"Gramatica para un subconjunto
trivial de Ingles.")
(defvar *gramatica* *gramatica-simple*
"Gramatica utilizada para
generar. Inicialmente esta es la
*gramatica-simple*, pero
puede conmutarse por otras gramaticas.")
Las formas especiales defvar y defparameter introducen variables especiales y se les ligan un valor. La diferencia es que el valor ligado a una variable como *gramatica* puede ser cambiado durante la ejecución del programa. Mientras que un parámetro como *gramatica-simple* permanecerá constante.
Una vez se han definido la lista de reglas gramaticales,
pueden ser utilizadas para encontrar las posibles sustituciones de una
categoría. La función assoc está diseñada precisamente
para este tipo de tarea. Tiene dos argumentos, una "clave" y una lista
de listas, y devuelve el primer elemento de la lista de listas que comienza
por esa clave. Si no hay ninguno, devuelve nil. Aquí hay un ejemplo:
(defun regla-pir (regla)
"Parte izquierda de una regla."
(first regla))
(defun regla-pdr (regla)
"Parte derecha de una regla."
(rest (rest regla)))
(defun sustituciones (categoria)
"Devuelve una lista con todas
las posibles sustituciones de una
categoria."
(regla-pdr (assoc categoria
*gramatica*)))
Definiendo estas funciones facilitaremos hacer cambios en la representación de las reglas y la lectura de los programas que las usan.
Ahora estamos listos para abordar el problema principal: definir una función (que llamaremos genera) que ejecutará las sentencias (o nombres de frase o cualquier otra categoría). Esta función tendrá que abordar tres casos:
Aquí están algunos ejemplos del uso de genera:
> (genera 'sentencia)
->(LEE TOOK PAT TO THOSE IN THE BALL ...)
> (genera 'sentencia) ->(ROBIN
HIT SHE)
> (genera 'sentencia) ->(IT
TOOK A BIG GREEN TABLE)
> (genera 'sentencia) ->(LEE
HIT KIM)
> (genera 'sentencia) ->(A
MAN LIKED TERRY BY A BLUE MAN)
> (genera 'Nombre-frase) ->(HE)
> (genera 'Nombre-frase) ->(ROBIN)
> (genera 'Nombre-frase) ->(A
WOMAN)
> (genera 'Verbo-frase) ->(TOOK
THE BIG MAN BY IT WITH HE)
> (genera 'Verbo-frase) ->(LIKED
LEE)
> (genera 'Verbo-frase) ->(TOOK
THE WOMAN WITH TERRY WITH THE MAN
...)
> (genera 'Verbo-frase) ->(LIKED
LEE)
> (genera 'Verbo-frase) ->(LIKED
ROBIN BY A TABLE IN THE MAN)
>
Hay otras muchas formas posibles de escribir genera. La siguiente versión utiliza if en lugar de cond:
2.2.4 Cambiar la gramática sin cambiar el programa
Mostraremos la utilidad de este enfoque definiendo
una nueva gramática que incluye adjetivos, frases preposicionales,
nombres propios y pronombres. Podremos así aplicar la función
genera sin modificaciones para esta nueva gramática.
(defparameter *gramatica-mas-grande*
'((Sentencia
-> (Nombre-frase Verbo-frase))
(Nombre-frase
-> (Articulo Adj* Nombre PP*) (Nombre-propio)
(ProNombre))
(Verbo-frase
-> (Verbo Nombre-frase PP*))
(PP*
-> () (PP PP*))
(Adj*
-> () (Adj Adj*))
(PP
-> (Prep Nombre-frase))
(Prep
-> to in by with on)
(Adj
-> big little blue green adiabatic)
(Articulo
-> the a)
(Nombre-propio
-> Pat Kim Lee Terry Robin)
(Nombre
-> man ball woman table)
(Verbo
-> hit took saw liked)
(ProNombre
-> he she it these those that)))
(setf *gramatica* *gramatica-mas-grande*)
> (genera 'sentencia)
(A BLUE BIG BIG BALL
ON ROBIN SAW ...)
> (genera 'sentencia)
(THAT TOOK IT)
> (genera 'sentencia)
(LEE HIT THE BLUE BALL
TO TERRY WITH PAT ON A LITTLE WOMAN ON THESE)
> (genera 'sentencia)
(A WOMAN TOOK IT TO
PAT WITH HE)
> (genera 'sentencia)
(KIM LIKED THE BIG GREEN
BALL)
> (genera 'sentencia)
(THAT SAW ROBIN ON IT)
>
Notar el problema con los pronombre: el programa
generó with he,
aunque with him es
la forma gramatical apropiada. También, está claro que el
programa no distingue entre frases con o sin sentido.
Utilizando la versión basada en funciones
tendríamos que reescribir cada función para generar la estructura
adicional. Con la versión orientada a los datos podríamos
mantener la gramática tal como está y sólo necesitaríamos
escribir una función: una versión de genera que produzca
la lista anidada deseada. Los dos cambios a hacer son crear una lista con
la categoría con un cons
en la parte delantera de la sustitución y no hacer el append
de los resultados sino simplemente enlistarlos con un mapcar:
> (genera-todas 'articulo)
((THE) (A))
> (genera-todas 'nombre)
((MAN) (BALL) (WOMAN)
(TABLE))
> (genera-todas 'nombre-frase)
((THE MAN) (A MAN) (THE
BALL) (A BALL) (THE WOMAN) (A WOMAN) (THE TABLE) (A TABLE))
> (length (genera-todas
'sentencia))
256
>
.- Ejercicio 4 Crear una función denominada quita-palabra que quita una palabra de la gramática (probar los efectos de utilizar remove y delete). P.e.:
Prepara un único fichero con los resultados de todos los ejercicios pedidos. En el encabezamiento debe estar la documentación que presenta el fichero y el autor, y a continuación se encontrarán los resultados consecutivos de los ejercicios convenientemente distinguidos. El fichero será de Lisp por lo que debes colocar todo lo que no es código como comentarios. Deja en la parte correspondiente a cada ejercicio exclusivamente el código que tu has creado y que resulta imprescindible para probar tus soluciones.
Somete el fichero como se estableció en la
práctica primera.
2.4 Listado del código suministrado
(defun Sentencia ()
(append (Nombre-frase) (Verbo-frase)))
(defun Nombre-frase () (append (Articulo)
(Nombre)))
(defun Verbo-frase () (append
(Verbo) (Nombre-frase)))
(defun Articulo ()
(uno-de '(the a)))
(defun Nombre ()
(uno-de '(man ball woman table)))
(defun Verbo ()
(uno-de '(hit took saw liked)))
;;; ==============================
(defun uno-de (conjunto)
"Elige un elemento del conjunto
y lo mete en una lista."
(list (random-elt conjunto)))
(defun random-elt (choices)
"Elige aleatoriamente un
elemento de una lista."
(elt choices (random (length
choices))))
;;; ==============================
(defun Adj* ()
(if (= (random 2) 0)
nil
(append
(Adj) (Adj*))))
(defun PP* ()
(if (random-elt '(t nil))
(append
(PP) (PP*))
nil))
(defun Nombre-frase () (append (Articulo)
(Adj*) (Nombre) (PP*)))
(defun PP () (append (Prep) (Nombre-frase)))
(defun Adj () (uno-de '(big little
blue green adiabatic)))
(defun Prep () (uno-de '(to in by
with on)))
;;; ==============================
(defparameter *gramatica-simple*
'((Sentencia -> (Nombre-frase
Verbo-frase))
(Nombre-frase
-> (Articulo Nombre))
(Verbo-frase
-> (Verbo Nombre-frase))
(Articulo ->
the a)
(Nombre -> man
ball woman table)
(Verbo -> hit
took saw liked))
"Gramatica para un subconjunto
trivial de Ingles.")
(defvar *gramatica* *gramatica-simple*
"Gramatica utilizada para
generar. Inicialmente esta es la
*gramatica-simple*, pero
puede conmutarse por otras gramaticas.")
;;; ==============================
(defun regla-pir (regla)
"Parte izquierda de una regla."
(first regla))
(defun regla-pdr (regla)
"Parte derecha de una regla."
(rest (rest regla)))
(defun sustituciones (categoria)
"Devuelve una lista con todas
las posibles sustituciones de una categoria."
(regla-pdr (assoc categoria
*gramatica*)))
;;; ==============================
(defun genera (frase)
"Genera una sentencia o frase
aleatoria."
(cond ((listp frase)
;caso de lista
(mapcan #'genera frase))
((sustituciones frase) ;caso de simbolo
con sustituciones
(genera (random-elt (sustituciones frase))))
(t (list frase))))
;caso de palabra
;;; ==============================
(defparameter *gramatica-mas-grande*
'((Sentencia -> (Nombre-frase
Verbo-frase))
(Nombre-frase
-> (Articulo Adj* Nombre PP*) (Nombre-propio) (ProNombre))
(Verbo-frase
-> (Verbo Nombre-frase PP*))
(PP* -> () (PP
PP*))
(Adj* -> () (Adj
Adj*))
(PP -> (Prep
Nombre-frase))
(Prep -> to in
by with on)
(Adj -> big little
blue green adiabatic)
(Articulo ->
the a)
(Nombre-propio
-> Pat Kim Lee Terry Robin)
(Nombre -> man
ball woman table)
(Verbo -> hit
took saw liked)
(ProNombre ->
he she it these those that)))
(setf *gramatica* *gramatica-mas-grande*)
;;; ==============================
(defun genera-arbol (frase)
"Genera una sentencia o frase
aleatoria con su arbol completo."
(cond ((listp frase)
(mapcar #'genera-arbol frase))
((sustituciones frase)
(cons frase
(genera-arbol (random-elt (sustituciones frase)))))
(t (list frase))))
;;; ==============================
(defun genera-todas (frase)
"Genera una lista de
todas las posibles expansiones de esta frase."
(cond ((null frase)
(list nil))
((listp frase)
(combina-todas
(genera-todas (first frase))
(genera-todas (rest frase))))
((sustituciones frase)
(mapcan #'genera-todas (sustituciones frase)))
(t (list (list frase))))
)
(defun combina-todas (xlist ylist)
"Devuelve una lista de listas
formada añadiendo una y a una x.
P.e¡., (combina-todas
'((a) (b)) '((1) (2)))
-> ((A 1) (B 1) (A 2) (B
2))."
(mapcan #'(lambda (y)
(mapcar #'(lambda (x) (append x y)) xlist))
ylist))