Manejadores de Mensajes
Los manejadores son
esenciales en OOP porque proporcionan el soporte para la encapsulación de
objetos. La única forma con la que los objetos pueden responder a los mensajes
es teniendo los adecuados manejadores para recibirlos y llevar a cabo las
acciones apropiadas.
Además de los
manejadores predefinidos como print, clips
también permite al usuario definir nuevos manejadores.
La clase NUMBER
tiene las subclases INTEGER y FLOAT, son clases predefinidas como se vio en la
práctica anterior, en la siguiente descripción de la clase INTEGER se
puede
ver que no dispone de ningún manejador de
mensajes. Si se desea enviarle un mensaje print, se deberá crear un manejador
para recibir y realizar esta acción.
El manejador es
definido para la clase NUMBER en lugar de
para INTEGER porque de esta manera también se pueden manejar los
mensajes de los objetos de la clase FLOAT. En lugar de definir el mismo
manejador para FLOAT e INTEGER, es más
útil definir un único manejador para la superclase NUMBER. Si un manejador para
un mensaje dado no esta definido en la clase de un objeto, clips busca en todos
los manejadores en la lista de precedencia. Por lo tanto si “+” no esta
definido para INTEGER, CLIPS busca en NUMBER, donde encuentra el manejador a
aplicar y devuelve el resultado de la acción.
Al pedir la
descripción de la clase INTEGER en CLIPS, aparece el manejador “ +” definido
por el usuario.
Si como se ha visto
INTEGER es una clase abstracta, ¿cómo es posible que pueda tener instancias
como 1, 2,3, etc.? La respuesta es que no se pueden crear directamente
instancias de una clase abstracta, pero si se puede hacer uso de las instancias
existentes. En el caso de la clase predefinida INTEGER, todos los enteros hasta
el máximo permitido son objetos disponibles en clips. Análogamente, cadenas y
símbolos son instancias disponibles de las clases abstractas STRING y SYMBOL.
La variable ?self es una variable especial en la
que clips almacena la instancia activa, que es la instancia a la que se envía
el mensaje. ?self es una palabra reservada que no puede ser incluida
explícitamente como argumento de un manejador, ni tampoco se le puede asignar
un valor diferente mediante bind a la variable
?self.
Ejercicio:
Define un manejador que concatene
cadenas, símbolos o ambos.
Los manejadores
normalmente se definen para las subclases de USER donde el usuario puede
definir instancias de estas clases. Como norma y para mejorar la eficiencia se
debe definir el manejador lo más cerca posible de la clase o clases para las
que ha sido concebido. De esta manera se mejora la eficiencia ya que clips no
tiene que ir buscando a través de las clases el manejador a aplicar.
COOL permite al
usuario declarar por anticipado los gestores de mensajes de una clase dentro de
la construcción defclass. El
propósito de estas declaraciones es sólo para documentación y son ignoradas por
CLIPS. No es necesario declarar previamente los gestores de una clase para
poder definirlos y asociarlos a una clase.
En cualquier caso, para definir un gestor se
debe utilizar el constructor defmessagehandler.
Ejemplo:
(defclass rectángulo (is-a USER)
(role concrete)
(slot base (create-accessor read-write) (default 1))
(slot altura (create-accessor read-write) (default 1))
(message-handler calcular-area))
; declaración, no definición
Sintaxis de
defmessage-handler
Como ya se ha visto,
la construcción defmessage-handler se
utiliza para especificar la conducta de los objetos de una clase en respuesta a
un mensaje particular.
(defmessage-handler <nombre-clase>
<nombre-mensaje>
[<tipo-handler>] [<comentario>]
(<parámetro>* [<parámetro-comodín>])
<acción>*)
La definición de un
gestor de mensajes consta de 7 partes:
1. Un
nombre de clase (ya definida) a la que el gestor estará asociado.
2. El
nombre del mensaje al que el gestor responderá.
3. El
tipo de gestor (por defecto se supondrá primary si no se especifica).
4. Una
cadena de comentario (opcional).
5. Una
lista de parámetros que se pasará al gestor durante la ejecución.
6. Un
parámetro comodín (para gestionar múltiples parámetros).
7. Una
secuencia de acciones o expresiones que serán ejecutadas por el gestor.
El valor devuelto por un gestor de mensajes
es el resultado de la evaluación de la última acción.
Dentro de una clase,
los gestores para un mensaje particular pueden ser de cuatro tipos diferentes: [<tipo-handler>]
primary: Realiza la tarea
principal del mensaje. before:
Realiza trabajo auxiliar para un mensaje antes de que se ejecute el gestor
primario (usado sólo para efectos laterales; se ignora el valor producido).
after: Realiza trabajo
auxiliar para un mensaje después de que se ejecute el gestor primario (usado
sólo para efectos laterales; se ignora el valor producido).
around: Inicializa un entorno para la ejecución
del resto de los gestores.
El gestor primario proporciona la parte de la implementación del
mensaje que es más específica al objeto (el definido en la clase más cercana
anula los de las clases superiores).
Un mensaje utiliza todos los gestores before o after de todas
las clases de un objeto. Esto quiere decir que si se envía un determinado
mensaje a una clase C, se ejecuta el gestor primario de C, y todos los gestores
before y after de sus superclases.
Partiendo de la clase rectángulo definida anteriormente:
CLIPS>(defmessage-handler
rectangulo calcular-area () (* ?self:base ?self:altura)) ;el área (base por
altura).
CLIPS>(defmessage-handler rectangulo calcular-area
before ()
(printout t “****Empezando a calcular el área***”))
CLIPS>(defmessage-handler rectangulo calcular-area after
()
(printout t “****Área calculada***”))
CLIPS >(defmessage-handler rectángulo imprimir-área ()
(printout t (send ?self
calcular-área) crlf))
El número de
parámetros de un gestor de mensajes puede ser fijo o variable, dependiendo de
que se proporcione un parámetro comodín,$?<nombre-variable>.
El cuerpo de un
gestor de mensajes es una secuencia de expresiones que se ejecutan en el orden
definido. Las acciones de los gestores tienen acceso directo a los atributos,
mediante : ,son considerados como
parte de la encapsulación del objeto.
?self:<nombre-slot> Ejemplo:
(defclass auto (is-a USER)
(role concrete)
(slot puertas (default 4))
(multislot seguridad (default cinturones)))
(defmessage-handler auto imprimir-slots ()
(printout t
?self:puertas " " ?self:seguridad crlf))
La función bind
también se puede utilizar para modificar el valor de un atributo: (bind ?self:<nombre-slot> <valor>*)
Ejemplo:
CLIPS >(defmessage-handler auto actualizar-seguridad
($?valores)
(bind ?self:seguridad $?valores))
CLIPS >(make-instance Renault-19 of auto)
[Renault-19]
CLIPS >(send [Renault-19] actualizar-seguridad Airbag
ABS)
(Airbag ABS)
Cuando un objeto
recibe un mensaje mediante la función send, CLIPS examina la lista de
precedencia de clases para determinar el conjunto de gestores de mensajes
aplicables al mensaje.
Un gestor de
mensajes es aplicable a un mensaje, si su nombre coincide con el mensaje y el
gestor está ligado (asociado) a alguna clase de la lista de precedencia a la
cual pertenezca la instancia que recibe el mensaje.
El conjunto de gestores de mensajes
aplicables se ordena de acuerdo a su tipo o role
(el orden es: around, before, primary, after), y a
continuación se ordena por especificidad de clase:
los gestores around, before y primary se ordenan de más especifico a
más general.
los gestores after .se
ordenan de más general a más específico.
El orden de ejecución es como sigue:
1) los
gestores around comienzan a
ejecutarse desde el más específico al más general (cada gestor around debe permitir explícitamente la
ejecución a los otros gestores).
2) Se
ejecutan los gestores before (uno
tras otro) de más específico a más general.
3) Se
ejecutan los gestores primary, desde
el más específico al más general.
4) Los
gestores primary finalizan su ejecución, desde el más general al
más específico.
5) los gestores after se ejecutan (uno tras otro) de más general a más específico.
6) Finaliza la ejecución de los gestores
around del más general al más
específico.
Emparejamiento de patrones con objetos
Para que pueda
producirse la correspondencia o emparejamiento de patrones en objetos es
necesario indicar en la declaración de la clase a partir de la que se crean las
instancias la cláusula pattern-match
reactive. El comportamiento por defecto es no permitir el emparejamiento,
lo cual también se indicaría explícitamente mediante la cláusula “patternmatch
non-reactive”. La posibilidad de emparejamiento se puede indicar a nivel de
slot indicando las mismas cláusulas.
Ejemplo:
(defclass PERSONA (is-a USER) (role abstract))
(defclass HOMBRE (is-a PERSONA) (role concrete)
(pattern-match reactive)
(slot
nombre (create-accessor read-write))
(slot
edad (create-accessor read-write) (default 0)
))
(definstances seres_iniciales
(hombre1 of HOMBRE (nombre juan))
(hombre2 of HOMBRE (nombre luis))
(hombre3 of HOMBRE (nombre alfonso))
(hombre4 of HOMBRE (nombre juan))
)
(defrule nombres_personas
?clase <- (object (is-a HOMBRE)
(nombre ?cual))
=>
(printout t "La instancia
" (instance-name ?clase) " tiene de nombre " ?cual crlf)
)
El emparejamiento
del objeto se hace especificando la clase y los valores de los slot que se
quieran, se usa el elemento condicional para emparejamiento de objetos object para indicar que se está
haciendo emparejamiento con un objeto. El nombre de la instancia no tiene
importancia a la hora de realizar el emparejamiento en este ejemplo. Para
acceder al nombre de la instancia se debe usar la función instance-name que toma como argumento el índice o dirección de la
instancia y devuelve el nombre de la instancia.
Se puede hacer el
emparejamiento en objetos de forma que también se empareje el nombre de la
instancia, para esto, se usa en el patrón una restricción de nombre, que se
indica mediante la palabra name
seguida por el nombre de la instancia (entre corchetes) que se quiere hacer
corresponder:
(defrule nombres_personas
?clase
<- (object (is-a HOMBRE) (nombre ?cual) (name [hombre2])) =>
(printout t "La instancia
" (instance-name ?clase) " tiene de nombre " ?cual crlf)
)
La palabra name es una palabra reservada, no puede
haber slots con ese nombre en ninguna clase.
Ejercicio: Modificar
la regla en el siguiente programa para que se imprima el mismo resultado pero
sin necesidad de obtener el índice de la instancia ni usar la función
“instance-name”
(defclass PERSONA (is-a USER) (role abstract))
(defclass HOMBRE (is-a PERSONA) (role concrete)
(pattern-match reactive)
(slot
nombre (create-accessor read-write))
(slot
edad (create-accessor read-write) (default 0)
))
(definstances seres_iniciales
(hombre1 of HOMBRE (nombre juan))
(hombre2 of HOMBRE (nombre luis))
(hombre3 of HOMBRE (nombre alfonso))
(hombre4 of HOMBRE (nombre juan))
)
(defrule nombres_personas
?clase <- (object (is-a HOMBRE)
(nombre ?cual))
=>
(printout t "La instancia
" (instance-name ?clase) " tiene de nombre " ?cual crlf)
)
El sistema de consultas de COOL
Además de las
reglas, se puede producir correspondencia de patrones en instancias mediante el
denominado sistema de consultas de
COOL. Este sistema consta de seis funciones que pueden ser usadas para efectuar
la correspondencia de patrones en un conjunto
de instancias (instance-set) y
realizar las acciones pertinentes.
Conjuntos de instancias
Un conjunto de instancias es una colección ordenada de
instancias, por ejemplo:
[hombre1] [hombre2] [hombre3]
es un conjunto de instancias,
[hombre4] [hombre1] [hombre2]
es otro conjunto de instancias,
[hombre4] [hombre4] [hombre4]
es otro más.
En los conjuntos de
instancias, las posiciones de los elementos o miembros del conjunto de instancias importa, por lo que los
conjuntos de instancias:
[hombre1] [hombre2] [hombre3]
y
[hombre3] [hombre1] [hombre2]
son diferentes.
La creación de los
conjuntos de instancias es automática, haciéndose en base a las
especificaciones del usuario que se reflejan en plantillas de conjuntos de instancias.
Las instancias que
aparecen en el conjunto de instancias corresponden a las que el usuario ha
indicado en las restricciones de clase
que aparecen en las plantillas de conjuntos de instancias.
Por ejemplo, la plantilla:
((?ins1 HOMBRE) (?ins2 MUJER))
define un conjunto
de instancias formado por dos instancias, la primera de las cuales debe ser de
la clase HOMBRE y la segunda de la clase MUJER.
Las variables ?ins1
e ?ins2 se denominan variables de
miembros de conjuntos de instancias y se asocian a nombres de instancias en
el proceso de creación de los conjuntos. Por todo esto se puede concluir que
una plantilla de conjunto de instancias está formada por pares variable –
restricciones de clase. Las restricciones de clase pueden constar de una o más nombres
de clases, también sería válido:
((?ins1 HOMBRE MUJER) (?ins2 MUJER))
Si se tiene la siguiente descripción de clases e
instancias:
(defclass PERSONA (is-a USER)
(role abstract)
(slot sexo (access read-only)
(storage
shared))
(slot age (type NUMBER)
(visibility
public)))
(defmessage-handler PERSONA put-edad (?valor)
(dynamic-put edad ?valor))
(defclass HEMBRA (is-a PERSONA)
(role abstract)
(slot sexo (source composite)
(default
hembra)))
(defclass VARON (is-a PERSONA)
(role abstract)
(slot sexo (source composite)
(default
varon)))
(defclass CHICA (is-a HEMBRA)
(role concrete)
(slot edad (source composite)
(default
4)
(range
0 17)))
(defclass MUJER (is-a HEMBRA)
(role concrete)
(slot edad (source composite)
(default
25)
(range
18 100)))
(defclass CHICO (is-a VARON)
(role concrete)
(slot edad (source composite)
(default
4)
(range
0 17)))
(defclass HOMBRE (is-a VARON)
(role concrete)
(slot edad (source composite)
(default
25)
(range
18 100)))
(definstances GENTE
(hombre1 of HOMBRE (edad 18))
(hombre2 of HOMBRE (edad 60))
(mujer1 of MUJER (edad 18))
(mujer2 of MUJER (edad 60))
(mujer3 of MUJER)
(chico1 of CHICO (edad 8))
(chico2 of CHICO)
(chico3 of CHICO)
(chico4 of CHICO)
(chica1 of CHICA (edad 8))
(chica2 of CHICA))
En este caso, la definición de una plantilla de conjunto de
instancias como la que sigue:
((?hombre_chico CHICO HOMBRE) (?mujer_chica CHICA MUJER))
que también podría haberse escrito:
((?hombre_chico VARON) (?mujer_chica HEMBRA))
daría lugar a la generación de treinta conjuntos de
instancias:
1. [chico1]
[chica1]
2. [chico1]
[chica2]
3. [chico1]
[mujer1] 4. [chico1] [mujer2]
5. [chico1]
[mujer3]
6. [chico2]
[chica1]
7. [chico2]
[chica2]
8. [chico2]
[mujer1]
9. [chico2]
[mujer2]
10. [chico2]
[mujer3]
11. [chico3]
[chica1]
12. [chico3]
[chica2]
13. [chico3]
[mujer1] 14. [chico3] [mujer2]
15. [chico3]
[mujer3]
16. [chico4]
[chica1]
17. [chico4]
[chica2]
18. [chico4]
[mujer1] 19. [chico4] [mujer2]
20. [chico4]
[mujer3]
21. [hombre1]
[chica1]
22. [hombre1]
[chica2]
23. [hombre1]
[mujer1] 24. [hombre1] [mujer2]
25. [hombre1]
[mujer3]
26. [hombre2]
[chica1]
27. [hombre2]
[chica2]
28. [hombre2]
[mujer1] 29. [hombre2] [mujer2]
30. [hombre2] [mujer3]
Definición de consultas
Una consulta es una expresión booleana
definida por el usuario que se aplica a un conjunto de instancias para
determinar si este conjunto de instancias cumple las restricciones
establecidas. Si la evaluación de esta expresión para un conjunto de instancias
es cualquier cosa que no sea FALSE, se dice que el conjunto de instancias
satisface la consulta.
Dentro de una
consulta, los slot de los elementos de un conjunto de instancias pueden ser
accedidos sin necesidad de pasar un mensaje al objeto correspondiente, sino separando
simplemente el nombre de la variable de instancia del nombre de slot:
<nombre-variable-de-conjunto-de-instancias>:<nombre-slot>
Acciones distribuidas
Una acción distribuida es una expresión
definida por el usuario que se evalúa para cada conjunto de instancias que
satisface una consulta. Para ejecutar más de una acción, se debe usar la
función progn para agruparlas.
Las variables de
miembro de conjunto de instancias sólo son válidas dentro de la función de
consulta.
Funciones de consulta
Existen seis
funciones. Para un conjunto de instancias dado, las seis funciones iterarán
sobre estas instancias en el mismo orden.
1.- Función any-instancep
Esta función aplica
una consulta a cada conjunto de instancias que empareja con la plantilla. Si un
conjunto de instancias satisface la consulta, la función termina (se busca sólo
que haya algún conjunto de instancias que cumpla la consulta) y se devuelve el
valor TRUE. De otra forma, se devuelve FALSE.
La sintaxis es:
(any-instancep <plantilla-de-conjunto-de-instancias>
<consulta>)
Ejemplo:
(any-instancep ((?hombre HOMBRE)) (> ?hombre:edad 30))
2.- Función find-instance
Esta función aplica
una consulta a cada conjunto de instancias que cumple con la especificación de
la plantilla. Si un conjunto de instancias satisface la consulta, entonces la
función termina y se devuelve el conjunto de instancias en un campo multivalor. Si no se satisface la
consulta, se devuelve un campo multivalor de longitud cero.
Ejemplo: Para encontrar el primer par de hombre y mujer con
la misma edad:
(find-instance ((?h HOMBRE) (?m MUJER)) (= ?h:edad
?m:edad))
3.- Función find-all-instances
Esta función aplica
una consulta a cada conjunto de instancias que empareja con la plantilla. Cada
conjunto de instancias que satisface la consulta se guarda en un campo
multivalor, por lo que al final se devuelve un campo multivalor en el que
aparecen todos los nombres de todas las instancias de todos los conjuntos de
instancias que satisfacen la consulta.
Por ejemplo, para encontrar todos los pares hombre-mujer
que tienen la misma edad:
(find-all-instances ((?h HOMBRE) (?m MUJER)) (= ?h:edad
?m:edad))
4.- Función do-for-instance
La función aplica
una consulta a cada conjunto de instancias que emparejan con la plantilla. Si
un conjunto de instancias satisface la consulta, la acción distribuida
especificada se ejecuta y la función termina. El valor de retorno es la
evaluación de la acción. Si ningún conjunto de instancias satisface la
consulta, el valor de retorno es FALSE.
Por ejemplo, para imprimir por pantalla la tripleta de
gente que tiene la misma edad.
(do-for-instance ((?p1 PERSONA) (?p2 PERSONA))
(and (= ?p1:edad
?p2:edad ?p3:edad)
(neq
?p1 ?p2)
(neq
?p1 ?p3)
(neq
?p2 ?p3))
(printout t ?p1 “ “ ?p2 “ “ ?p3
crlf))
¿Por qué se usan las llamadas a “neq”?
5.- Función do-for-all-instances
En este caso, la
acción se ejecuta tantas veces como conjuntos de instancias satisfagan la
consulta. El valor devuelto es la evaluación de la acción para el último
conjunto de instancias que satisface la consulta. Si ningún conjunto de
instancias satisface la consulta, el valor devuelto es FALSE.
6.- Función delayed-do-for-all-instances
En este caso se
emplea un almacenamiento intermedio de los conjuntos de instancias, empezando a
ejecutarse las acciones cuando todos los conjuntos que satisfacen la consulta
están localizados.