Skip to content

A `define-class` macro for less boilerplate

License

Notifications You must be signed in to change notification settings

atlas-engineer/nclasses

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NClasses

NClasses provides helper macros to help write classes and conditions with less boilerplate.

It’s a fork of 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 type inference.
  • This library goes beyond class definitions in providing more utility macros e.g. define-generic and make-instance*.

Examples

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.

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):

(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)))

Helpers beyond define-class

define-generic

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.

:export-generic-name-p (option) and \*export-generic-name-p\* (variable)

These allow to export generic name after defining it:

(define-generic foo ((a integer))
  (bar a)
  (:export-generic-name-p t))

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:
(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 calling apply:
(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.

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

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Common Lisp 100.0%