Datatypes With Emacs Lisp Macros

Emacs Lisp doesn't really have a straightforward way to make user-defined data types. Instead, I can create records that bundle together other values and return a "type descriptor" of my choosing when fed to type-of. Records can be created with the built-in function record.

For example, if I want to make a value to represent the point (1, 3) in the XY plane I can run (defvar p (record (quote point) 1 3)). p satisfies the properties

I think it would be better to have a more readable and memorable way to make, inspect and modify points.

(defun point-make (x y) (record (quote point) x y))

(defun point-get-x (p) (aref p 1))

(defun point-get-y (p) (aref p 2))

(defun point-set-x (p x) (aset p 1 x))

(defun point-set-y (p y) (aset p 2 x))

Once the name of the type - in this case point - and the names of the fields x and y are known, writing the constructor, the get functions and the set functions is a mechanical task. I am going to automate the details away with a macro. In an Emacs Lisp file called datatype.el type the code that follows.

; datatype.el

(defun datatype-create-constructor (type fields)
  "TYPE should be a symbol, the datatype to be created. FIELDS should be a list
of symbols, the fields/member variables of the datatype. The return value is a
Lisp list that when evaluated defines a function that constructs the datatype."

  (let (; This is the name of the constructor function. If TYPE is the symbol
        ; apple then the value of name will be the symbol apple-make. intern
        ; converts a string to a symbol and symbol-name converts a symbol to a
        ; string.
        (name (intern (concat (symbol-name type) "-make")))
        
        ; This is the list of arguments that the constructor function will
        ; accept. Its only logical that the constructor function accepts initial
        ; values for each member variable.
        (arglist fields)

        ; This is the body of the constructor function.
        (body (let (; The value of quoted-type is Lisp code that quotes type.
                    (quoted-type (list 'quote type))

                    ; fields, the list of symbols, is passed to record as slots.
                    (slots fields))

                ; The function record has one initial argument TYPE and the rest
                ; of the arguments together called SLOTS. The value of record's
                ; TYPE argument is passed as quoted-type because there is
                ; already a variable called type. quoted-type and slots are both
                ; defined in the containing let block.
                (apply 'list 'record quoted-type slots))))
    
    ; The function defun takes three arguments - NAME, ARGLIST and BODY.
    ; All three have been bound above, in the same let block.
    (list 'defun name arglist body)))

(defun datatype-create-getter (type field index)
  "TYPE should be a symbol, the datatype for whose field a getter
  needs to be created. FIELD is the specific field for which we need
  a constructor. INDEX is the position at which you would put the
  value for FIELD when using the function record to create a value of
  type TYPE."

  (let ((name
         (intern
          (concat (symbol-name type)
                  "-get-"
                  (symbol-name field)))) 
        
        (arglist (list 'x))

        (body 
         (let ((array 'x)
               (idx index))
           (list 'aref array idx))))
    
    (list 'defun name arglist body)))

(defun datatype-create-setter (type field index)
  "TYPE should be a symbol, the datatype for whose field a setter
  needs to be created. FIELD is the specific field for which we need
  a setter. INDEX is the position at which you would put the value for
  FIELD when using the function record to create a value of type
  TYPE."
  
  (let ((name
         (intern
          (concat (symbol-name type)
                  "-set-"
                  (symbol-name field))))
        
        (arglist (list 'x 'new))

        (body
         (let ((array 'x)
               (idx index)
               (newelt 'new))
           (list 'aset array idx newelt))))
    
    (list 'defun name arglist body)))

(defmacro datatype-define-datatype (type &rest fields)
  "TYPE and FIELDS are to be passed as variables, not symbols. Calling this
macro defines the functions
    TYPE-make
    TYPE-get-FIELD0
    TYPE-get-FIELD1
    ...
    TYPE-get-FIELDN
    TYPE-set-FIELD0
    TYPE-set-FIELD1
    ...
    TYPE-set-FIELDN
where FIELD0, FIELD1, ..., FIELDN make up FIELDS."
  
  (let ((indices 
         (number-sequence 0 (1- (length fields)))))

    (let ((constructor
           (datatype-create-constructor type fields))
          
          (getters
           (seq-map
            (lambda (index)
              (datatype-create-getter type (nth index fields) (1+ index)))
            indices))
          
          (setters            
           (seq-map
            (lambda (index)
              (datatype-create-setter type (nth index fields) (1+ index)))
            indices)))

      (cons 'progn
            (cons constructor
                  (append getters
                          setters))))))

The macro can be tested by running the code below.

(datatype-define-datatype bus wheels decks)

(let ((bus
       (bus-make 2 10)))
  (let ((wheels-old 
         (bus-get-wheels bus))
        (decks-old 
         (bus-get-decks bus)))
    (bus-set-wheels bus 4)
    (bus-set-decks bus 20)
    (let ((wheels-new
           (bus-get-wheels bus))
          (decks-new
           (bus-get-decks bus)))
      (message
       (format 
        "wheels-old = %d
decks-old  = %d
wheels-new = %d
decks-new  = %d"
        wheels-old
        decks-old
        wheels-new
        decks-new)))))