Playing around with EIEIO (a.k.a. CEDET internals, part 1)

Tagged:  

We're on the brink of the Emacs 23.1 release, so it's about time to look at the new stuff 23.2 will have… One major thing that is planned is the merging of CEDET into Emacs. While CEDET in itself is great stuff already, it will also include some general new packages it heavily depends on and which will bring some interesting new concepts to programming in Emacs Lisp. Also, understanding these packages is important if you'd like to start hacking CEDET yourself.

CEDET extensively uses two packages which I guess most people are unfamiliar with: mode-local (which is in the 'common' directory, written by David Ponce) and EIEIO (written by Eric Ludlam). The first one can be roughly described as "buffer-local variables, but for major-modes instead of buffers". That means, you can generally set variables depending on the major mode of a buffer - CEDET uses these mode-local variables to setup the language parsers for the different buffers.

In this post however, I'd like to take a look at the other package, EIEIO, which is way bigger and much more intricate than mode-local. It's a framework to write object-oriented code in Emacs Lisp, trying to remain as close to CLOS as it is possible in Emacs Lisp. For those who have never heard of CLOS: it stands for "Common Lisp Object System" and is the standard for doing OOP in Common Lisp. You probably know OOP from C++ or Java, but you'll find that CLOS is pretty different from those in several aspects. While there are classes and methods (of course), most of the other stuff is completely different, and often times it is best to just forget what you learned in C++ or Java. There are several nice tutorials for CLOS, and I give some links at the end of this post. Most of that stuff you read there can be applied to EIEIO, but there are some differences, which are described in the EIEIO manual (section "CLOS compatibility"). For a quick overview of CLOS, its Wikipedia article is a good starting point.

EIEIO is a part of CEDET, so if you want to try out the following examples, just download and compile it. If you just want to use EIEIO, you don't have to load the entire CEDET suite in your init file; just put (require 'eieio) in your .emacs.

Let's take a look at the most basic thing: Defining a class.

   (defclass record () ; No superclasses
       ((name :initarg :name
              :initform ""
              :type string
              :custom string
              :documentation "The name of a person.")
        (birthday :initarg :birthday
                  :initform "Jan 1, 1970"
                  :custom string
                  :type string
                  :documentation "The person's birthday.")
        (phone :initarg :phone
               :initform ""
               :documentation "Phone number."))
       "A single record for tracking people I know.")

(This example is straight from the EIEIO docs).

So, 'defclass' obviously defines a class, and it has the following syntax:

(defclass name superclass slots &rest options-and-doc)

If you're familiar with OOP in general, you probably figured out yourself that 'superclass' is CLOS-speak for the class this one inherits from (the "parent"), and 'slots' are like 'member variables' in C++/Java - in the above example we have 'name', 'birthday', and 'phone'. You also see that you can define additional attributes for these slots, like the type, default values and documentation.

If you evaluate this example and do "C-h f record RET", you'll get the following:

Object Constructor Function: record
Creates an object of class record.

Class record

Documentation:
A single record for tracking people I know.

Instance Allocated Slots:

Slot: name    type = string    default = ""
  The name of a person.

Slot: birthday    type = string    default = "Jan 1, 1970"
  The person's birthday.

Slot: phone    default = ""
  Phone number.

You see that 'defclass' automatically created a constructor function 'record' which creates an object of the class. It also created a proper documentation string, describing the slots of the class with their types and default values (if specified).

So, lets create an instance of this class:

(setq rec (record "rand" :name "Random Sample" :birthday "01/01/2000" :phone "555-5555"))

Each instance is given a name as the first argument, so different instances can be easily distinguished during debugging. The further arguments are the slots and their values (by the way: in CLOS you create an instance using 'make-instance', and this is also possible in EIEIO).

You can check if 'rec' is really of class 'record' using the automatically generated function 'record-p'. For retrieving and setting its slot values, use 'oref' and 'oset':

(record-p rec)
  => t
(oref rec :birthday)
  => "01/01/2000"
(oset rec :phone "555-5566")
(oref rec :phone)
  => "555-5566"

Now lets define a method:

(defmethod call-record ((rec record) &optional scriptname)
  "Dial the phone for the record REC.
   Execute the program SCRIPTNAME as to dial the phone."
   (message "Dialing the phone for %s"  (oref rec name))
   ;; to be implemented... 
  )

You see that there is no notation like "OBJECT.method", since in CLOS methods do not really "belong" to classes. Instead, the code which is executed is derived from the arguments given to the function. Therefore, you call such a method like you would call any other function:

(call-record rec)
  => "Dialing the phone for Random Sample"

If you have several 'call-record' methods for different classes, it will know by its first argument which one to call. Let's take a look at the doc-string of 'call-record':

call-record is a generic function with only one primary method.

Documentation:
Dial the phone for the record REC.
   Execute the program SCRIPTNAME as to dial the phone.

Implementations:

`record' :PRIMARY (rec &optional scriptname)
Dial the phone for the record REC.
   Execute the program SCRIPTNAME as to dial the phone.

You see that it lists all implementation of 'call-record', i.e. for all classes it is defined for. If you'd create another class with a 'call-record' method, it would also be listed under "Implementations" (try it out!). But what does "with only primary methods" mean?

Well, lets create a class which inherits from 'record':

   (defclass abroad-record (record)
      ((country :initarg :country
        :initform "DE"
        :documentation "Country this person is living in."))
       "Special class for people living abroad.")

Looking at the doc-string of 'abroad-record', you'll see that it mentions that it inherits from 'record', and you'll also see that it inherited all slots from 'record', but now also contains the slot 'country'.

Lets define 'call-record' for this class, but with a twist.

(defmethod call-record :before ((rec abroad-record) &optional scriptname)
  "Prepend country code to phone number, then dial the phone for REC.
   Execute the program SCRIPTNAME as to dial the phone"
   (message "Prepending country code to phone number.")
   (unless (string-match "^00" (oref rec :phone))
    (let ((country (oref rec :country)))
     (cond 
       ;; just an example...
      ((string= country "IT")
       (oset rec :phone (concat "0043" (oref rec :phone)))))))
  )

This function just prepends the country code for Italy if necessary. If you think all this 'oset' and 'oref' stuff is tedious - you are right. But you can define so called "accessor" functions: in the slot definition you can write

  :accessor get-phone

and the you can just use (get-phone rec) to obtain the phone number.

Now, you see that there is this argument ":before", and this is where it gets interesting. It means that if you do

(setq abroadrec (abroad-record "friend" :name "Good Friend" :birthday "01/01/2000" :phone "555-5555" :country "IT"))
(call-record abroadrec)

it will first call the 'call-record' from 'abroad-record', and then it automatically calls 'call-record' from 'record'. Therefore, the Message buffer shows:


Prepending country code to phone number.
Dialing the phone for Good Friend

You'll also see that the 'phone' slot now also has the country-code "0043" before the actual number. (BTW, I know this example doesn't make much sense and you would usually solve this problem differently… it's just an example, after all.)

Generally speaking, if more than one class defines a method with the same name (which is called a generic function), the applicable methods are combined into one single effective method. How these single methods are combined is where it gets really interesting, and in CLOS you can even define this yourself. However, the default is the so called standard method combination, and this is also what EIEIO does.

Next to ":before", you might guess that there's also ":after". If you omit this specification, it is ":primary" by default. These primary methods form the the main body of the effective method; in our case, it's the method which does the actual calling. This also answers the above question what EIEIO means with "generic function with only one primary method. A generic function can have several primary methods, and you can manually invoke the next most specific method via

(call-next-method)

All this is just scratching on the surface on how methods are combined to the effective method. I just mention that there's also ":around" and ":static", and that you can easily do multiple inheritance, which is when method invocation gets really powerful (and complicated). And if you were wondering: yes, there's also a standard method 'initialize-instance' which is similar to a constructor in C++. If you'd like to know more, take a look at the following tutorials:

A brief guide to CLOS

Fundamentals of CLOS

Tiny CLOS tutorial

At the end, let's take a short look how CEDET uses EIEIO. The part of CEDET which makes extensive use of EIEIO is EDE, the package for managing projects. For an example, take a look at the class 'ede-compiler', which defines a general compiler, and classes like 'ede-object-compiler' inherit from this class. They include other objects in their slots, for example 'ede-sourcecode', which defines which kind of sourcecode the compiler can compile. Therefore, if you'd like to include new compilers/linkers into EDE, you simply define new sourcetypes, compiler and linker classes and fill the slots appropriately.

Another thing which makes EIEIO very powerful is that you can easily build complex customization buffers for manually editing object slots. For an example, simply evaluate

(eieio-customize-object rec)

I should also mention that EIEIO is not the only object system for Emacs Lisp. For example, the shimbun library from emacs-w3m uses 'luna', which is part of FLIM and much smaller than EIEIO, but also with less features (e.g. multiple inheritance is not really working with it).