2.Práctica: Generación automática de frases en Common Lisp

                  2.1 Objetivo de la práctica

      El objetivo de esta práctica es incrementar la experiencia en el desarrollo de programas en Common Lisp tanto escribiendo código como depurando programas. Para ello mostraremos como combinar las funciones básicas y formas especiales de Lisp en un programa completo. Adicionalmente, podrás adquirir conocimiento sobre vocabulario y comportamiento de nuevas funciones.

      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:
         

            Para obtener una Sentencia, añadimos un Nombre-de-frase y un Verbo-de-frase
              Para obtener un Nombre-de-frase, añadir un Artículo y un Nombre
                Elegir “the” para el Artículo
                Elegir “man” para el Nombre
              El Nombre-de-frase resultante es “the man”
              Para obtener un Verbo-de-frase, añadir un Verbo y un Nombre-de-frase
                Elegir “hit” para el Verbo
                Para obtener un Nombre-de-frase, añadir un Artículo y un Nombre
                  Elegir “the” para el Artículo
                  Elegir “ball” para el Nombre
                El Nombre-de-frase resultante es “the ball”
              El Verbo-de-frase resultante es “hit the ball”
            La Sentencia resultante es “the man hit the ball”
         
         
               2.2.2 Una solución elemental

        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:

          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
        Cada regla consiste en una flecha con un símbolo en la parte izquierda y algo en la parte derecha. La complicación es que hay dos posibilidades para la parte derecha: una lista concatenada de símbolos, como "Nombre_de _frase ^ Artículo + Nombre", o una lista de palabras alternativas como en "Nombre ^  man, ball, woman, table …". Podríamos representar la lista concatenada de símbolos como una lista p.e. "(Artículo Nombre)", mientras que las palabras alternativas podrían ser representados directamente como símbolos "man, ball, woman, table". Según este estilo de representación las reglas quedarían de la siguiente forma:

         (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.")
         

           
        Notar que la versión de Lisp basada en reglas imita muy fielmente la versión original (se ha incluido incluso el símbolo -> sólo con propósitos decorativos pero sin ningún papel real).

        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:
         

          > (assoc 'Nombre *gramatica*)
          (NOMBRE -> MAN BALL WOMAN TABLE)
           
        Aunque las reglas resultan bastante simples implementadas como reglas, es una buena idea imponer un nivel de abstracción definiendo funciones que operen con las reglas. Necesitaremos tres funciones:

         (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:

        1. En el caso más simple, le llegará un símbolo que tiene un conjunto de reglas de sustitución ligadas a el. Se elige una aleatoriamente y se genera de él.
        2. Si el símbolo no tiene reglas de sustitución, debe ser un símbolo terminal (una palabra más que una categoría gramatical). Se deja como está (en realidad se devuelve metida en una lista porque se quiere que todos los resultados sean listas de palabras).
        3. En algunos casos, cuando el símbolo tiene sustituciones, se cogerá una que es una lista de símbolos, y se tratará de generar de ahí. Así, genera debe aceptar también una lista como entrada, en cuyo caso debe generar cada elemento de la lista y añadirlos todos juntos.
         
          (defun genera (frase)
            "Genera una sentencia o frase aleatoria."
            (cond ((listp frase)
                   (mapcan #'genera frase))
                  ((sustituciones frase)   ;devuelve p.e. ((nombre verbo))
                   (genera (random-elt (sustituciones frase))))
                  (t (list frase))))
           

           

        Esta es una función corta pero densa de información. Este estilo de programación se denomina programación dirigida por los datos, debido a que los datos (la lista de sustituciones asociada con cada categoría) dirige lo que el programa debe hacer a continuación. Esto es un estilo natural y fácil-de-utilizar en Lisp y conduce a programas concisos y extensibles pues siempre es posible añadir una nueva pieza de datos con una nueva asociación sin tener que modificar el programa original.

        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:

          (defun genera (frase)
            "Genera una sentencia o frase aleatoria."
            (if (listp frase)
               (mapcan #'genera frase)
               (let ((posibilidades (sustituciones frase)))
                  (if (null posibilidades)
                     (list frase)
                     (genera (random-elt (sustituciones frase)))))))
           
        Esta versión utiliza la forma especial let, que introduce una nueva variable (en este caso, posibilidades) y liga un valor a dicha variable. En este caso, la introducción de la variable nos evita llamar dos veces a la función sustituciones, como se hizo en la anterior versión.
      .- Ejercicio 1 Escribe una versión de genera que utiliza cond pero evita llamar dos veces a sustituciones.

               2.2.4 Cambiar la gramática sin cambiar el programa

        La aproximación utilizada para codificar genera, orientada a los datos, implica la creación de un paso extra con respecto a la primera versión y por tanto representa más trabajo para problemas pequeños. Sin emabrgo, los programas que utilizan esta aproximación resultan a menudo más sencillos de modificar y expandir. Esto es especialmente verdad en dominios donde hay una gran cantidad de datos a tener en cuenta. La gramática del lenguaje natural es uno de estos dominios (de hecho, la mayoría de los programas de IA encajan en este enfoque). La idea detrás de esta aproximación es trabajar con el problema lo máximo posible en nuestros propios términos y minimizar la parte de la solución que está escrita en Lisp.

        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.
         
         

                2.2.5 Utilizando los mismos datos para varios programas
        Otra ventaja de la representación de la información en forma declarativa (como reglas o hechos en lugar de como funciones) es que pueden facilitar el uso de información para múltiples propósitos. Supongamos que queremos una función que pueda generar no sólo la lista de palabras de una sentencia sino una representación de la sintaxis completa de la sentencia. Por ejemplo, en lugar de (a woman took a ball), queremos obtener la lista anidada:

         

          (SENTENCIA (NOMBRE-FRASE (ARTICULO A) (NOMBRE WOMAN))
                     (VERBO-FRASE (VERBO TOOK)
                                  (NOMBRE-FRASE (ARTICULO A) (NOMBRE BALL))))

           

        Esto corresponde al árbol que los linguistas dibujan según la siguiente figura:

        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:
         

          (defun genera-arbol (frase)
            "Genera una sentencia o frase aleatoria con un su arbol completo."
            (cond ((listp frase)
                   (mapcar #'genera-arbol frase))
                  ((sustituciones frase)
                   (cons frase
                         (genera-arbol (random-elt (sustituciones frase)))))
                  (t (list frase))))
           
        Aquí están algunos ejemplos (El símbolo # indica ninguna palabra (nil)):
         
          > (genera-arbol 'sentencia)
          (SENTENCIA (NOMBRE-FRASE (ARTICULO A)
                                   (ADJ* (ADJ LITTLE)
                                         (ADJ*))
                                   (NOMBRE WOMAN)
                                   (PP* (PP # #)
                                        (PP* # #)))
                     (VERBO-FRASE (VERBO TOOK)
                                  (NOMBRE-FRASE (ARTICULO THE)
                                                (ADJ*)
                                                (NOMBRE MAN)
                                                (PP*))
                                  (PP*)))
          >
          > (genera-arbol 'sentencia)
          (SENTENCIA (NOMBRE-FRASE (PRONOMBRE THESE))
                     (VERBO-FRASE (VERBO SAW)
                                  (NOMBRE-FRASE (ARTICULO A)
                                                (ADJ* # #)
                                                (NOMBRE BALL)
                                                (PP*))
                                  (PP*)))
           
        Para mostrar otro ejemplo para demostrar las ventajas de esta solución orientada a los datos vamos a generar todas las posibles sustituciones de una frase (genera-todas). Para su diseño vamos a utilizar también una función auxiliar, combina-todas, para gestionar la combinación de resultados. También necesitaremos cuatro casos en lugar de tres pues necesitamos chequear el nil explícitamente. Todavía, el programa completo queda bastante simple:

         

          (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))
           
        Ahora puedes utilizar genera-todas para comprobar las reglas de *gramatica-simple*:
         
          > (setq *gramatica* *gramatica-simple*)
          ……

          > (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 2 Justifica porqué resultan 256 sentencias.
            .- Ejercicio 3 Si llamaras a la función genera-todas con la gramática *gramatica-mas-compleja* verías que ocurre un serio problema (no lo pruebes sin haber guardado tus cambios). Indica porqué.

            .- 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.:

             

              > *gramatica*
              ((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))
              > (quita-palabra 'table)
              NIL
              > *gramatica*
              ((SENTENCIA -> (NOMBRE-FRASE VERBO-FRASE))
               (NOMBRE-FRASE -> (ARTICULO NOMBRE))
               (VERBO-FRASE -> (VERBO NOMBRE-FRASE))
               (ARTICULO -> THE A)
               (NOMBRE -> MAN BALL WOMAN)
               (VERBO -> HIT TOOK SAW LIKED))
              >.
    .- Ejercicio 5 Escribe una pequeña gramática para el lenguaje español y comprueba su buen funcionamiento poniendo ejemplos de todas las categorías.

     

           2.3 Información a entregar

      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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;   iaaa  iaaa  iaaa  iaaa  iaaa  iaaa  iaaa  iaaa  iaaa  iaaa   ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Idea original
;;;     1991        Code from Paradigms of Artificial Intelligence Programming
;;;                 Copyright (c) 1991 Peter Norvig
;;;
;;; Modificaciones:
;;;     17-10-97    muro    Modificaciones para practicas.
;;;                         Paso a castellano, cambio en
;;;                         mappend
;;;
 

(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))