git clone 'git://github.com/skeeto/at-el.git'

@, another object system for Emacs Lisp

@ is a library providing a domain-specific language for multiple-inheritance prototype-based objects in Emacs Lisp. The goal is to provide a platform for elegant object-oriented Emacs Lisp.

The root object of the @ object system is @. New objects are created with the @extend function, by extending existing objects. Given no objects to extend, new objects will implicitly extend @. Keyword arguments provided to @extend are assigned as properties on the new object.

Properties are looked up using eq, so stick to keywords, symbols, and integers as property keys. The parent prototypes of an object, used in property lookups, are listed in the :proto property of the object. This can be modified at any time to change the prototype chain.

See also: Prototype-based Elisp Objects with @

Prototype Library

Practical example @ prototypes can be found under lib/. When prototypes are stored in global variables following the @ naming convention, as demonstrated in the examples, their methods can be looked up with describe-@ (C-h @), similar to describe-function (C-h f).

Feature Demonstration

Here's a hands-on example of @'s features.

Property Access

(require '@)

;; Create a rectangle prototype, extending the root object @.
;; Convention: prefix "class" variable names with @.
(defvar @rectangle (@extend :width nil :height nil))

;; The @ function is used to access properties of an object, following
;; the prototype chain breadth-first as necessary. An error is thrown
;; if the property has not been defined.
(@ @rectangle :width) ; => nil

;; The @ function is setf-able. Assignment *always* happens on the
;; immediate object, never on a parent prototype.
(setf (@ @rectangle :width) 0)
(setf (@ @rectangle :height) 0)

;; Define the method :area on @rectangle.
;; The first argument is this/self. Convention: call it @@.
(setf (@ @rectangle :area) (lambda (@@) (* (@ @@ :width) (@ @@ :height))))

;; Convenience macro def@ for writing methods. Symbols like @: will be
;; replaced by lookups on @@. The following is equivalent to the above
;; definition.
(def@ @rectangle :area ()
  (* @:width @:height))

Multiple Inheritance

;; Create a color mix-in prototype
(defvar @colored (@extend :color (list)))

;; The @: variables are setf-able, too.
(def@ @colored :mix (color)
  (push color @:color))

;; Create a colored rectangle from the prototypes.
(defvar foo (@extend @colored @rectangle :width 10 :height 4))

;; @! is used to call methods. The object itself is passed as the
;; first argument to the function stored on that prototype's property.
(@! foo :area)  ; => 40
(@! foo :mix :red)
(@! foo :mix :blue)
(@ foo :color)  ; => (:blue :red)

;; @: variables are turned into method calls when in function position.
(def@ foo :describe ()
  (format "{color: %s, area: %d}" @:color (@:area)))

(@! foo :describe)  ; => "{color: (:blue :red), area: 40}"

Constructors and Super Methods

;; By convention, constructors are the :init method. The @^:
;; "variables" are used to access super methods, including :init. Use
;; this to chain constructors and methods up the prototype chain (like
;; CLOS's `call-next-method').
(def@ @rectangle :init (width height)
  (setf @:width width @:height height))

;; The :new method on @ extends @@ with a new object and calls :init
;; on it with the provided arguments.
(@! (@! @rectangle :new 13.2 2.1) :area) ; => 27.72

Dynamic Property Getters and Setters

;; If a property is not found in the prototype chain, the :get method
;; is used to determine the value.
(let ((obj (@extend)))
  (def@ obj :get (property)
    (format "got %s" property))
  (@ obj :foo))
; => "got :foo"

;; The :get method on @, the default getter, produces an error if a
;; property is unbound. If you would rather unbound properties return
;; nil mix in @soft-get, which provides an alternate default :get
;; method.
(@ (@extend @rectangle @soft-get) :foo) ; => nil

;; Properties are assigned using the :set method. The :set method on @
;; does standard property assignment as would be expected. This can be
;; overridden for custom assignment behavior.
(let ((foo-only (@extend)))
  (def@ foo-only :set (property value)
    (if (string-match-p "^:foo" (prin1-to-string property))
        (@^:set property value)  ; supermethod
      (error "Only :foo* properties allowed!")))
  (setf (@ foo-only :foo-bar) 'a)  ; ok
  (setf (@ foo-only :bar) 'b))     ; ERROR

;; Using this, an @immutable prototype mixin is provided that
;; disallows all property assignments.
(setf (@ (@extend @immutable) :foo) 'a)  ; ERROR

The @vector prototype under lib/ shows how these can be useful for providing pseudo-properties.

;; Built on :set, a @watchable mixin is provided for observing all of
;; the changes to any object.
(let ((history ())
      (obj (@extend @watchable)))
  (@! obj :watch (lambda (obj prop new) (push (list property new) history)))
  (setf (@ obj :foo) 0)
  (setf (@ obj :foo) 1)
  (setf (@ obj :bar) 'a)
  (setf (@ obj :bar) 'b)
; => ((:bar b) (:bar a) (:foo 1) (:foo 0))


;; @is is the classical "instanceof" operator. It works on any type of
;; object in both positions.
(@is foo @colored)   ; => t
(@is foo @rectangle) ; => t
(@is foo foo)        ; => t
(@is foo @)          ; => t
(@is foo (@extend))  ; => nil
(@is [1 2 3] @)      ; => nil

;; The :is method on @ can also be used for this.
(@! @colored :is @)    ; => t
(@! foo :is @colored)  ; => t

;; The :keys method on @ can be used to list the keys on an object.
(@! foo :keys)  ; => (:proto :width :height :color)

Syntax Highlighting

The library provides syntax highlighting for ‘def@’ and ‘@:’ variables in emacs-lisp-mode, so the above @ uses will look more official in an Emacs buffer.