LE-LISP permite definir objetos estructurados del tipo de los
record en Pascal, los struct en C o los structure en
Fortran. Para ello se usa la primitiva defstruct. Estos objetos
estructurados están formados por un cierto número de campos no tipados.
Se puede definir una estructura como sub-estructura de una precedente, lo
cual permite crear una jerarquía de tipos con herencia. Sin embargo,
no se soportan jerarquías múltiples, aunque ello no
representa una limitación en la práctica, pues este tipo de
herencia plantea numerosos problemas cuando se utiliza. A las
estructuras se les pueden asociar métodos, con lo cual se puede conseguir
encapsulamiento de funciones y datos, al estilo de los object del
Pascal o de las class del C. También se pueden conseguir
polimorfismo definiendo métodos con el mismo nombre en objetos distintos
para realizar acciones similares. La primitiva send permite enviar un mensaje
a un objeto (en el sentido de O.O..).
El sistema se ocupa automáticamente de determinar cual es el método adecuado
que debe aplicar al objeto para satisfacer el mensaje.
Al definir los objetos estructura, además de definir los campos que lo forman, se puede determinar un valor incial para ellos, que será asignado en ese campo en el momento de creación de cada nueva instancia.
Como ejemplo, a continuación definimos el tipo person como una estructura con los campos name, DNI y birthday. A este último campo se le asigna un valor inicial cada vez que se crea una instancia de person. Dicho valor es el resultado de la evaluación de la función get-date que supongamos devuelve la fecha actual.
(defstruct person name DNI (birthday (get-date)))
Para asignar a Manolo una instancia de person, podemos utilizar las dos opciones siguientes:
(setq Manolo (new 'person)) (setq Manolo (#:person:make))
Esto se debe a que cuando se crea una estructura, automáticamente se crea el método make (que devuelve una instancia de la estructura) y un método para acceder a cada campo cuyo nombre coincide con el del campo.
Resumiendo, diremos que defstruct representa la parte declarativa de la programación con tipos estructurados, es decir define las clases, mientras que make y new generan instancias concretas de esas clases (que muchas veces reciben el nombre de objetos en el ámbito de la O.O.)
Mediante la sintaxis (#:estructura:campo objeto) podemos recuperar el valor del campo campo en el objeto objeto, que debe ser una instancia de estructura. Si lo que queremos es asignar un valor a un campo, usaremos la sintaxis (#:estructura:campo objeto valor).
Podemos definir un nuevo método llamado age que dado un objeto person
devuelva como resultado su edad de la siguiente manera:
(defun #:person:age (pers) (- (year (get-date) (year (#:person:age pers)))
En general, la forma en que se define un método coincide con la forma usual de definir funciones con la salvedad de que el primer parámetro estará ocupado por el objeto receptor del método y que se debe especificar la estructura a la pertenece el método mediante la sintaxis #:estructura:método o una forma abreviada equivalente.
La utilización de defstruct permite la definición de jerarquías de tipos, es decir, un árbol de tipos en donde las estructuras se corresponden con los nodos y donde los descendientes de un nodo heredan las características de dicho nodo, esto es, sus campos y métodos. El nodo raíz del árbol se denota por #.
Como ejemplo, a continuación se define los tipos lecturer y student
que son subtipos de person
y se crea una instacia para cada uno de ellos:
(defstruct #:person:lecturer category) (defstruct #:person:student degree) (setq Manolo (#:person:lecturer:make)) (setq Pablo (#:person:student:make))
Para obtener la edad de Manolo y Pablo podemos hacer:
(setq manolo-age (#:person:age Manolo)) (setq pablo-age (#:person:age Pablo))
o bien:
(setq manolo-age (send 'age Manolo)) (setq pablo-age (send 'age Pablo))
El uso de send es más ineficiente ya que el sistema LE-LISP debe recorrer ascendentemente la jerarquía hasta encontrar el método cuyo nombre coincide con el mensaje, aunque evita al programador el tener que preocuparse del lugar en la jerarquía en que están definidos los métodos. La función send-super es similar a send excepto que comienza a buscar por el método en el nodo padre del objeto pasado como argumento. Otras variantes son send-error, csend y send2.
Por otra parte, al ser posible redefinir los métodos a lo largo de la jerarquía, send aplica siempre el método más cercano ascendentemente al objeto de aplicación, mientras que indicando el nombre completo podemos especificar qué método exacto queremos ejecutar.
Para evitar tener que escribir la jerarquía completa como en #:person:lecturer existen las abreviaturas, que se definen usando defabbrev. Por ejemplo, si definimos defabbrev profesor #:person:lecturer, a partir de este momento podremos usar {lecturer} como sustituto de #:person:lecturer en cualquier lugar donde esto último era válido. Las funciones put-abbrev, get-abbrev, abbrevp, has-an-abbrev y rem-abbrev también están relacionadas con las abreviaturas.