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

** 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!