NClasses provides helper macros to help write classes and conditions with less boilerplate.
It’s a fork of hu.dwim.defclass-star.
hu.dwim.defclass-star
has a symbol export bug which cannot be fixed upstream, see hu-dwim#7 and hu-dwim#12 for a discussion.- The macro and package names of hu.dwim.defclass-star prove to be rather
unwieldy.
Emacs can automatically highlight
define-class
as a macro, but notdefclass*
. - This library offers new features that wouldn’t be accepted upstream, like type inference.
- This library goes beyond class definitions in providing more utility
macros e.g.
define-generic
andmake-instance*
.
A basic session:
(define-class foo ()
((slot1 :initarg nil)
(slot2 \"hello!\")
(unexported-slot :export nil))
(:export-class-name-p t)
(:export-accessor-names-p t)
(:accessor-name-transformer #'nclasses:default-accessor-name-transformer))
(make-instance 'foo :my-slot1 17)
See the package documentation for a usage guide and more examples.
If you want to change the default class options, say, for a package, you can
simply define a wrapping macro (without importing nclasses:define-start
):
(defmacro define-class (name supers slots &rest options)
"`nclasses:define-star' with automatic types and always-dashed predicates."
`(nclasses:define-class ,name ,supers ,slots
,@(append
'((:automatic-types-p t)
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer))
options)))
define-generic
is made to shorten the frequent pattern of generic with one method:
(defgeneric foo (a b c)
(:method ((a integer) (b symbol) c)
(bar))
(:documentation "Some FOO documentation."))
Such a scary bloated code often makes one to use the neat defmethod
instead:
(defmethod foo ((a integer) (b symbol) c)
"Some FOO documentation."
(bar))
While convenient and short, standalone method definition
auto-generates a generic function that’s neither documented nor
inspectable. define-generic
solves this problem by making defgeneric
form shorter and more defmethod
-like, without any loss of
semantics. The previous form looks like this with define-generic
:
(define-generic foo ((a integer) (b symbol) c)
"Some FOO documentation."
(bar))
This form expand to exactly the same generic definition as the one above, while being as concise as the defmethod version.
The body or define-generic
is automatically wrapped into a :method
option, so there could be several body forms. If any of these body
forms is a defgeneric
option, it’s safely put as defgeneric option
outside the implied method:
(define-generic foo ((a integer) (b symbol) c)
"Some FOO documentation." ; Docstring should always go first.
(:method-combination progn)
(bar)
(:generic-function-class foo-class))
;; =>
;; (defgeneric foo (a b c)
;; (:method ((a integer) (b symbol) c)
;; (bar))
;; (:method-combination progn)
;; (:generic-function-class foo-class)
;; (:documentation "Some FOO documentation."))
See the define-generic
documentation for more examples and details.
These allow to export generic name after defining it:
(define-generic foo ((a integer))
(bar a)
(:export-generic-name-p t))
There are several idioms that heavily object-oriented CL code converges to:
(make-instance 'class :width width :height height)
- repetitive arguments.
(apply #'make-instance 'class :key val :key2 val2 (when something (list :key3 val3)))
- appending
args to the
make-instance
form viaapply
.
make-instance*
abstracts these two patterns with shortcut arguments and apply forms respectively:
- Shortcut arguments are a list of symbols that will be expanded into a list of eponymous keywords and args:
(make-instance* 'class (height width) :depth 3)
;; =>
;; (make-instance 'class :height height :width width :depth 3)
- Apply form allows passing the last
apply
argument without explicitly callingapply
:
(make-instance* 'class :width 3 :height 5 (when three-dimentions (list :depth 3)))
;; =>
;; (apply #'make-instance 'class :width 3 :height 5 (when three-dimentions (list :depth 3)))
Both of these patterns can be used together, dramatically shortening the code:
(make-instance* 'class (width height) (when three-dimentions (list :depth 3)))
;; =>
;; (apply #'make-instance 'class :width width :height height (when three-dimentions (list :depth 3)))
Note that using either of these conveniences as the sole
make-instance*
argument is an ambiguous case that should be avoided by
providing either shortcuts or apply form as an explicit NIL/().
See the make-instance*
documentation for more examples and details.
- Renamed
defclass*
todefine-class
(althoughdefclass*
is still available as alias, alongsidedefine-class*
). - Renamed
defcondition*
todefine-condition*
(defcondition*
is still available as alias ofdefine-condition*
). - Added convenience macros beyond class definition:
define-generic
for concise generic function definition (withdefgeneric*
anddefine-generic*
aliases).make-instance*
(withmake*
alias) to abstract eponymous keywords and arguments and inline theapply #'make-instance
idiom.
- Default slot value when initform is omitted is
nil
. To leave slot unbound, specify:unbound
as initform value. - Only the core system has been kept, the ContextL, hu.dwim.def and Swank optional features have been removed.
- New predicate name transformers
always-dashed-predicate-name-transformer
andquestion-mark-predicate-name-transformer
. - New type inference options:
:automatic-types-p
and:type-inference
. - Default accessor transformer now follows the slot name.
hu.dwim.defclass-star default accessor is available as
dwim-accessor-name-transformer
. - Bug fixes:
- No longer try to export
NIL
. - Always return the class.
- Avoid unneeded
progn
. - Do not generate generic functions and accessors in foreign packages when
:accessor-name-package
is:slot-name
and:accessor
is not provided. (If accessor already exists in foreign package, then the new one is generated.)
- No longer try to export
- Remove
NASDF
as a dependency.
- Make
define-generic
declaration parsing smarter. - Ensure more correct
define-generic
body parsing.- Interpret a single-string body as method body and signal warnings due to the ambiguity of it.
- Auto-generate documentation for class predicates.
- Auto-generate documentation for slot accessors.
- Add
:export-generic-name-p
option todefine-generic
.
- Add
make-instance*
anddefine-generic
convenience macros. - Add alias macros, like
defclass*
,defcondition*
,defgeneric*
, andmake*
. - Ensure documentation is always set for classes, generics, and conditions.
- Default to nil when slot value is unspecified.
- Enable accessor generation in foreign package when it already exists.
- Bug fixes.
- Fix
default-accessor-name-transformer
to follow:accessor-name-package
. - Do not generate accessors in foreign packages when
:accessor-name-package
is:slot-name
and:accessor
is not provided.
- Fix
export-predicate-name-p
class option. - Allow type inference to check for types in superclasses.
defclass/std
is another popular library with a similar goal, but with more
insistance on conciseness, maybe at the expanse of readability. In particular,
it implements a way to specify slots by properties which may seem unnatural (we
read slots by their name, not by their properties).
Metaclasses would not be very useful here since most of our features need to be enacted at compile-time, while metaclasses are mostly useful on classe instances.
NClasses was originally developed for Nyxt, so the “N” may stand for it, or “New”, or whatever poetic meaning you may find behind it!