Have a look at CLOS, the Common Lisp Object System, described notably in Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS in 1988. CLOS itself was influenced by existing object systems like Flavors and CommonLoops.
The following defines or redefines a class when called:
(defclass my-class (multiple parent classes)
((x :initarg :x)
(y :initarg :y)))
This is a macro, but the functional equivalent exist and is called
ensure-class, which is easier to use when arguments are computed.
The value returned by this expression, or by
(find-class 'my-class), is a value of type
standard-class. Classes have a metaclass, which is an object which describes how to represent classes and how to access slot's values. You can change CLOS to make it behave like another object system thanks to the meta-object protocol, described in The Art of the Metaobject protocol (a.k.a. AMOP). For example, you could provide a
:metaclass argument which makes your objects serialize and deserialize from a database.
Once you have a class, you can instanciate it:
(make-instance 'my-class :x 10 :y 20)
Note that the class argument to
make-instance can be given at runtime.
All values in Common Lisp have a class:
=> #<built-in-class fixnum>
(class-of '(a b c))
=> #<built-in-class cons>
But not everything is necessarily an object.
CLOS allows you to define multimethods and hence generic functions are not tied to a single class. For example:
(defgeneric attack (attacker target)
(:method ((w wizard) (v vampire))
;; Vampires do not glitter when exposed to light, they burn.
(cast-spell w 'sunbath :on v))
(:method ((w wizard) (d dragon))
;; Fall, you fool
(cast-spell w 'paralysis :on d))
;; Dispatch on human here, a superclass of wizard
(:method ((v vampire) (h human)) (bite v h))
(:method ((d dragon) (h human)) (ignite d h))
;; Dragon duel
(:method ((d1 dragon) (d2 dragon) (bite d1 d2))
;; Default case
(:method (x y) (punch x y)))
You can add or remove methods at runtime and specify also other kind of methods, such as
(defmethod attack :around (attacker target)
(when (>= (roll-dice (attack-points attacker))
;; attack is successful, proceed to actual attack
The above is executed around each attack and checks if the attacker is successful, based on chance and both objects's characteristics. The
(call-next-method) expression is used to call the next less-specific
:around method or the next most-specific primary method if no other
:around method exists.
Objects can change class at runtime:
(defmethod bite ((v vampire) (h human))
(take-hit h (- (bite-attack v) (armor h)))
(change-class h (infected (class-of h))
Here, a vampire inflicts damage to a wizard which then changes into an
infected-wizard class, thanks to
(infected (class-of h)) which makes sure such a class exist by creating it if necessary (requires alexandria and closer-mop):
(defun infected (class)
(symbolicate 'infected "-" (class-name class))
:direct-superclasses (list 'infected class)))
The instance of human is updated with new slots: here, the wizard is supposed to be changed into a generic vampire in three turns.
how would you give this functionality while keeping language consistent?
Changing classes and methods at runtime might be problematic but the system is built around a robust protocol for dealing with runtime changes, such as calling the appropriate initialization methods when changing an object's class, or recomputing class precedence lists before generic functions are called.
... but then you loose type-checking
Type-checking is enforced at runtime.