Skip to content
 
 

Latest commit

 

History

213 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

#+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 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 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!

About

A `define-class` macro for less boilerplate

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages