Programación Orientada a Objetos en Emacs
A raíz del último artículo, donde hablaba de algunos lenguajes de la
familia de LISP, me han preguntado por la POOProgramación Orientada a Objetos que se puede hacer dentro de elisp
y su
compatibilidad con CLOSCommon Lisp Object System. En
elisp
encontramos el paquete con el acrónimo EIEIO, que significa
Enhanced Implementation of Emacs Interpreted Objects. No hay que
instalar nada, es uno de los paquetes built-in y lo encuentras
instalado con tu instalación de Emacs. En este artículo voy a hacer
una breve introducción a eieio
para todos aquellos que no pueden, o
sí, vivir sin objetos en su código y quieran usarlos programando para
Emacs en elisp
.
Siempre es recomendable leer la documentación, como he hecho yo para
escribir este artículo. No empleo POO cuando trabajo en el elisp
,
no porque le tenga manía a dicho estilo de programación, sino porque
mis neuronas a estas alturas se alienan en modo funcional cuando
pienso en hacer algo con elisp
o, en general, con cualquiera de los
herederos de LISP. También es necesario advertir que eieio
no es
una implementación completa de CLOS y hay diferencias entre ambos
sistemas. Esto es debido a que elisp
tiene algunas restricciones en
su diseño y por tanto no puede soportar la especificación CLOS al
completo. Si estás decidido a utilizarlo y has utilizado antes CLOS,
sería conveniente que le echaras un vistazo antes a dichas
limitaciones.
Entre las características que proporciona eieio
están:
- Una manera estructurada de crear clases básicas con atributos y métodos, con herencia múltiple y muy similar a CLOS.
- Comprobación de tipos.
- Navegadores de clases: simple y complejo, con comandos como
eieio-browse
. - Soporte
edebug
para los métodos. - Soporte de compilación a
bytecode
de los métodos. - Ayuda para la extensión del sistema de clases y métodos.
- Muchas clases básicas para tareas interesantes.
- Sistema simple de tests para TDD.
- Soporte para entradas públicas y privadas en las clases.
Definiendo clases
Para definir una clase, se utiliza el macro defclass
con la
siguiente estructura:
(defclass nombre-clase (lista-superclases) (lista-slots) &rest opciones-y-doc)
Al crear una clase, automágicamente eieio
nos crea una función con
la que podemos saber si un objeto pertenece a dicha clase, la función
nombre-clase-p
:
(nombre-clase-p objeto)
Devolverá t
si es de esa clase y nil
en caso contrario. También
encontraremos otras funciones creadas automágicamente como
nombre-clase-child-p
para comprobar si es de una clase derivada y
algunos más. Como siempre, hay que remitirnos a la documentación. Sin
embargo, esto me recuerda que hay que hablar un poco de la herencia.
Herencia
Hablar en abstracto es complicado. Creo que será mejor si pongo un ejemplo sencillo para ilustrar cómo funciona. Tampoco voy a ser exhaustivo, pero sí quiero ser los más amplio posible sin complicar mucho las explicaciones.
(defclass mi-clase-base () ((slot-a :initarg :slot-a) (slot-b :initarg :slot-b)) "Mi clase base para el ejemplo.")
Como vemos, en la macro, la lista de clases para la herencia está
vacía: ()
. Vamos a derivar otra clase de esta:
(defclass mi-clase-derivada (mi-clase-base) ((su-propio-slot-a :initarg :su-propio-slot-a)) "Una subclase de mi-clase-base.")
En este caso, mi-clase-derivada
tendrá slot-a
y slot-b
heredados
de la clase base y contará también con su-propio-slot-a
.
Si en la definición de la clase se pide una lista de clases de las que
deriva es porque en eieio
se puede utilizar la herencia múltiple.
Por seguir con el mismo ejemplo, podríamos haber definido también una
interface:
(defclass mi-interface () ((interface-slot :initarg :interface-slot)) "Una interfaz para tener comportamientos especiales" :abstract t)
Como se puede observar, una interface es una clase más, pero
declarada como :abstract t
. Nuestra clase derivada, si añadimos la
interface se podría haber hecho así:
(defclass mi-clase-derivada (mi-clase-base mi-interface) ((su-propio-slot-a :initarg :su-propio-slot-a)) "Una subclase de mi-clase-base.")
Así, además de su slot propio y de los heredados de mi-clase-base
,
tendría también el heredado de mi-interface
.
Como se puede ver, cada definición de campo utiliza :initarg
para
dar un valor inicial al mismo. Al definirlos podemos realizar muchos
tipos de operaciones sobre ellos:
:initarg
- Valor inicial del campo.
:initform
- Una expresión cuyo resultado tras su evaluación determinará el valor inicial del campo.
:print
- una función que imprimirá el contenido del campo.
:writer
- una función que escribirá el contenido del campo.
:reader
- una función que leerá el contenido del campo.
:type
- especificación del tipo.
:protection
- especificación de la protección del campo. Puede
tomar los valores
:public
,:private
y:protected
.
Como hemos visto al definir mi-interface
, las clases también admiten
varias opciones. En el ejemplo he utilizado :abstract t
que no permite
crear objetos directamente de esa clase, sino que obliga a que se
utilice, tan sólo, a efectos de herencia. Hay muchos más y es
recomendable que mires esas opciones en la documentación.
Crear instancias
Para crear un objeto de una determinada clase se utiliza
make-instance
:
(make-instance 'nombre-clase &rest argumentos-de-inicio)
Para seguir nuestro ejemplo podríamos hacer algo como:
(setq objeto (make-instance 'mi-clase-derivada :slot-a 3 :slot-b 7 :interface-slot 42 :su-propio-slot-a 13)) ;; => #s(mi-clase-derivada 3 7 42 13) (mi-clase-deriva-p objeto) ;; => t
A partir de este momento, objeto
será una instancia de
mi-clase-base
, con los valores especificados en su creación.
Por otro lado, hay muchas formas de acceder a los slots o campos de un objeto. Los más habituales (pero no lo únicos, mira en la documentación) son:
slot-value
Esta función nos permite acceder al valor de un slot. Por ejemplo:
(slot-value objeto :slot-a) ;; => 3
set-slot-value
Esta función permite establecer valores en un slot. Por ejemplo:
(set-slot-value objeto :slot-a 25) (slot-value objeto :slot-a) ;; => 25
Definir métodos
Para definir métodos se tira de un macro que pertenece a la librería
CL (Common LISP). Siguiendo con el sencillo ejemplo que vamos
llevando, podríamos definir un método que sumara un determinado
valor entero al slot-a
. Mira el ejemplo:
(cl-defmethod suma-a ((obj mi-clase-derivada) (valor integer)) "Suma VALOR al :slot-a de un objeto de mi-clase-derivada." (set-slot-value obj :slot-a (+ valor (slot-value obj :slot-a))))
Por explicarlo un poco más detalladamente, vemos que el método
suma-a
toma dos argumentos. El primero, obj
es de tipo
mi-clase-derivada
. Lo he llamado obj
, pero si utilizas otros
lenguajes orientados a objetos lo mismo te es más fácil llamarlo
this
o self
. También admite un valor
de tipo integer
. Después
encontramos una cadena que será la documentación del método y en
último lugar se encuentra el cuerpo del método. El cuerpo puede ser
cualquier forma válida de elisp
. En el ejemplo, utilizo el acceso a
los campos para establecer un nuevo valor de :slot-a
con la suma de
dicho campo y el argumento pasado en la llamada. Evaluando el código:
(suma-a objeto 4) (slot-value objeto :slot-a) ;; => 29
La definición de métodos además tiene muchas más opciones y detalles que deben consultarse en la documentación, pero sirva el ejemplo como una sencilla introducción.
Conclusiones
No voy a profundizar mucho más. Si alguien está interesado en la
Programación Orientada a Objetos para Emacs, puede mirar la
documentación del paquete eieio
. Seguramente encontrará también
otros documentos a través de Internet que le sirvan de orientación.
También podemos ver el árbol de clases o la lista de métodos con los
comandos eieio-browse
o eieio-display-method-list
.
Comentarios