Notxor tiene un blog

Defenestrando la vida

Programación Orientada a Objetos en Emacs

Notxor
2023-01-29

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:

  1. Una manera estructurada de crear clases básicas con atributos y métodos, con herencia múltiple y muy similar a CLOS.
  2. Comprobación de tipos.
  3. Navegadores de clases: simple y complejo, con comandos como eieio-browse.
  4. Soporte edebug para los métodos.
  5. Soporte de compilación a bytecode de los métodos.
  6. Ayuda para la extensión del sistema de clases y métodos.
  7. Muchas clases básicas para tareas interesantes.
  8. Sistema simple de tests para TDD.
  9. 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.

Categoría: emacs elisp poo

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en esta cuenta de Mastodon, también en esta otra cuenta de Mastodon y en Diaspora con el nick de Notxor.

Si usas habitualmente XMPP (si no, te recomiendo que lo hagas), puedes encontrar también un pequeño grupo en el siguiente enlace: notxor-tiene-un-blog@salas.suchat.org

Disculpen las molestias.