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
(aref p 0)
yields 1(aref p 1)
yields 3(aset p 0 8)
changes the x-coordinate of p
from 1 to 8 and returns 8(aset p 0 9)
changes the x-coordinate of p
from 1 to 9 and returns 9(type-of p)
yields the symbol point
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)))))