TODO: Finish me!
November 29, 2023 ยท View on GitHub
#+TITLE: NClasses
NClasses provides helper macros to help write classes and conditions with less boilerplate.
It's a fork of [[https://github.com/hu-dwim/hu.dwim.defclass-star][hu.dwim.defclass-star]].
** Motivation
-
=hu.dwim.defclass-star= has a symbol export bug which cannot be fixed upstream, see https://github.com/hu-dwim/hu.dwim.defclass-star/pull/7 and https://github.com/hu-dwim/hu.dwim.defclass-star/issues/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 not =defclass*=.
-
This library offers new features that wouldn't be accepted upstream, like [[https://github.com/hu-dwim/hu.dwim.defclass-star/pull/3][type inference]].
-
This library goes beyond class definitions in providing more utility macros e.g. =define-generic= and =make-instance*=.
** Examples
A basic session:
TODO: Finish me!
#+begin_src lisp (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) #+end_src
See the [[file:source/package.lisp][package]] documentation for a usage guide and more examples.
** Default class options
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=):
#+begin_src lisp
(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)))
#+end_src
** Helpers beyond =define-class= *** =define-generic=
=define-generic= is made to shorten the frequent pattern of generic with one method:
#+begin_src lisp (defgeneric foo (a b c) (:method ((a integer) (b symbol) c) (bar)) (:documentation "Some FOO documentation.")) #+end_src
Such a scary bloated code often makes one to use the neat =defmethod= instead:
#+begin_src lisp (defmethod foo ((a integer) (b symbol) c) "Some FOO documentation." (bar)) #+end_src
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=:
#+begin_src lisp (define-generic foo ((a integer) (b symbol) c) "Some FOO documentation." (bar)) #+end_src
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: #+begin_src lisp (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.")) #+end_src
See the =define-generic= documentation for more examples and details.
**** :export-generic-name-p (option) and *export-generic-name-p* (variable) These allow to export generic name after defining it: #+begin_src lisp (define-generic foo ((a integer)) (bar a) (:export-generic-name-p t)) #+end_src
*** make-instance* 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 via =apply=.
=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: #+begin_src lisp (make-instance* 'class (height width) :depth 3) ;; => ;; (make-instance 'class :height height :width width :depth 3) #+end_src
- Apply form allows passing the last =apply= argument without explicitly calling =apply=: #+begin_src lisp (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))) #+end_src
Both of these patterns can be used together, dramatically shortening the code: #+begin_src lisp (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))) #+end_src
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.
** Changes from =defclass-star=
- Renamed =defclass*= to =define-class= (although =defclass*= is still available as alias, alongside =define-class*=).
- Renamed =defcondition*= to =define-condition*= (=defcondition*= is still available as alias of =define-condition*=).
- Added convenience macros beyond class definition:
- =define-generic= for concise generic function definition (with =defgeneric*= and =define-generic*= aliases).
- =make-instance*= (with =make*= alias) to abstract eponymous keywords and arguments and inline the =apply #'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= and =question-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.)
** Change Log
*** 0.6.1
- Remove =NASDF= as a dependency.
*** 0.6.0
- 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.
*** 0.5.0
- Auto-generate documentation for class predicates.
- Auto-generate documentation for slot accessors.
- Add =:export-generic-name-p= option to =define-generic=.
*** 0.4.0
- Add =make-instance*= and =define-generic= convenience macros.
- Add alias macros, like =defclass*=, =defcondition*=, =defgeneric*=, and =make*=.
- Ensure documentation is always set for classes, generics, and conditions.
*** 0.3.0
- Default to nil when slot value is unspecified.
- Enable accessor generation in foreign package when it already exists.
- Bug fixes.
*** 0.2.1
- 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.
*** 0.2.0
- Fix =export-predicate-name-p= class option.
- Allow type inference to check for types in superclasses.
** Alternatives
=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).
** Implementation notes
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/.
** History
NClasses was originally developed for [[https://nyxt.atlas.engineer][Nyxt]], so the "N" may stand for it, or "New", or whatever poetic meaning you may find behind it!