How I Write Generics

By Artyom Bologov Soft

Generic functions are beautiful as an idea. But they are also somewhat ugly as a syntax.

My (ab)use of generics led to a certain programming style. This style, I conjecture, ends up in more readable and correct functions. Although it's up to you whether to believe me.

This post will be structured around what generics are. This overview will explain intuition behind my style of writing generics. With examples and reasoning, of course. In the end, I'll share a small macro that nicely abstracts away some of my conventions.

Don't expect some eye-opening experience, it's mostly about formatting and ordering of generic options. Still, helps in setting up style guides.

Structure of Generics

Generics are

Let's go through these one by one.

Generics Are Ugly Functions

That's the reason a lot of people prefer defmethod over defgeneric: Generics are ugly. They require a lot of keywords and don't even look like functions. Even though they are ones.

So my style aims to bring generics as close to regular functions as possible. Which means putting documentation and declarations first, and methods last (taken from CL-BLC):

(defgeneric compile (expr &optional stack)
  (:documentation "Compile Lispy EXPR into binary lambdas...")
  (:method ((expr list) &optional stack)
   #|...|#))
Making generics look a bit more like functions

Why not just use defmethod? Because defining a generic via defmethod doesn't attach source location info. So xref/edit-definition/swank:find-source-location on generics often gets confused by this. Define your generics with defgeneric. And enjoy the increased transparency it brings!

A note on arglists: it's useful listing all the keyword/optional arguments in the generic definition. Instead of splitting these into separate methods. This way, even the dumbest tooling (and most inattentive reader) knows what keywords/optionals to expect. Yes, SLIME/SLY infers the arglist somehow. But there's no such a thing as too much caring for the user.

Generics Have Methods

That's the main feature of generics: they have methods. Generics dispatch over argument types to call most relevant method(s).

While most methods are born equal, some have a generic-related role. Like termination/dispatch/default ones. These usually represent the empty state or some stub for the actual implementation to replace.

One can put all the methods as :method options in generic. One can also define them all as separate defmethod-s. The former keeps things together in one place. The latter gives methods more metadata and pins them to certain source locations.

My preference is to define termination/default methods in generics. And other methods on their own. This way, one can immediately see the default behavior of the generic. And only after that can they get to the actual implementation/extension with defmethod-s.

Here's a bit of (old and somewhat orthogonal to the style I'm suggesting, but nonetheless exemplary in default method case) code from NJSON:

(defgeneric decode-from-stream (stream)
  (:method (stream)
    (declare (ignore stream))
    (signal 'decode-from-stream-not-implemented))
  (:documentation "Decode JSON from STREAM..."))
Putting only the termination/base case method in generics

I also tend to write all the other methods in terms of transforming data to a proper format and calling next/default method on it. This way, most of the actual logic is concentrated in one method. While other methods provide nice wrappers and validators for it.

Generics Dispatch Over Types

This is somewhat tautologic with methods, but hear me out: Generics dispatch over almost all built-in and user-defined types. Including the eql types. And this type-exact nature makes them extremely composable. One can encode a lot in argument lists.

It's easy enough to re-implement documentation-like control args with eql specializers:

(defmethod coerce ((term list) (type (eql 'list)) &optional inner-type final-type)
  #|...|#)
(defmethod coerce ((term list) (type (eql t)) &optional inner-type final-type)
  #|...|#)
Using type dispatch (including eql types) with generic specializers

One can also dispatch over type hierarchies, like list/cons/null. This way, typed edge cases are all covered without extra branching in the actual methods.

So yes, I abuse this power of specializers for common good. And you should too!

But method dispatch is expensive!

No. One can use something like (declare (optimize speed)) in generic declaration. This should get one 90% there. And the rest is up to implementation magic (which is pretty good even by default, at least on SBCL) to figure out. Don't over-optimize, focus on designing a clear contract in your generics!

Meta(data): combinations and classes

Finally, a bit of cryptic stuff: method combinations and generic classes. You'll be surprized with how mendable the whole system is. For example, here's a simple method combination Trivial Inspect uses to reorder and index returned properties:

(defun reverse-append-index (&rest lists)
  (let ((fields (remove-duplicates (reduce #'append (nreverse lists))
                                   :key #'first
                                   :from-end t)))
    (mapcar #'cons (field-indices fields)
            fields)))

(define-method-combination reverse-append-index
  :identity-with-one-argument t)

(defgeneric fields (object &key &allow-other-keys)
  (:method-combination reverse-append-index)
  (:documentation "Return a list of OBJECT fields to inspect..."))
Simple method combination to reorder the results of method calls

The logic is:

I might've used an :around method to sort things. But you may not need that around method. And I prefer to not touch auxiliary methods. Downstream user might want to use :around to override generic behavior. So custom method combination it is!

Generic function and method classes are even more obscure. I didn't encounter a use-case for them yet (except for command class in Nyxt.) But once there's such a case, only these classes can save the situation, so better be prepared!

I tend to put this meta-data (pun intended) before or after the docstring, so that it resembles regular functions. Functions allow declarations after docstring, after all. But you can see (in the code listing above) that I'm somewhat inconsistent in that. Still, put metadata/docs at the top and implementation at the bottom—will be clearer this way!

A New Syntax, Maybe?

But hey, it's much harder to write out generics the way I'm suggesting, even if it's actually useful! Luckily, we have macros in CL. One can define a macro to shorten generics and typical use patterns like mine. define-generic from Nclasses does that (Do check out Nclasses version, it clocks at 100 lines of useful macro code. I've gone to great length to make it work nicely!) Or use this simplistic macro from my Graven Image project:

(defmacro define-generic (name (&rest method-args) &body (documentation . body))
  (check-type documentation string "Documentation string")
  `(let ((generic (defgeneric ,name (,@(mapcar #'first (mapcar #'uiop:ensure-list method-args)))
                    (:method (,@method-args)
                      ,@body)
                    (:documentation ,documentation))))
     (setf (documentation (fdefinition ',name) t) ,documentation)
     (ignore-errors
       (setf (documentation ',name 'function) ,documentation))
     generic))
define-generic, an opinionated (and simplistic) generic-writing macro inspired by Nclasses

Used as

(define-generic (setf function-lambda-expression*) ((value list) function &optional force)
  "Setter for `function-lambda-expression*'."
  (declare (ignorable force))
  (let ((name (if (symbolp function)
                  function
                  (function-name* function))))
    (function-lambda-expression* (compile name value))))
Example of define-generic use

Lots of niceties there:

Which all compounds to a good and readable generic definition. It also looks almost like defmethod without sacrificing any benefits of generics. It's not some generic-writing Swiss knife, but it gets one 90% there. Leaving the complex cases to Nclasses or whatever else one prefers.

Wrapping Up: Write Generics Like I Do

Just kidding! I'm not forcing you to any type of style. You might even ignore generics altogether. But, hopefully, this post has shown you some useful techniques of working with generics. Here's a full reference for how I suggest writing generics.

(defgeneric generic-name (arg1 arg2 &optional opt)
  (:documentation "Generic function docs
See https://nyxt.atlas.engineer/article/lisp-documentation-patterns.org for additional context")
  (:generic-function-class meta-data-meta-class)
  (:method-class meta-data-method-class)
  (:method-combination meta-combination)
  (declare (optimize (speed 3)))
  (:method ((arg1 list) (arg2 (eql 't)) &optional (opt 8))
    "Simple default/termination method."
    t))
(defmethod generic-name ((arg1 cons) (arg2 null) &optional opt)
  "Implementation method documentation.
Not strictly necessary, but nice for extra implementation details."
  #|...|#)
Reference for my style of generic writing

But this full form is unlikely to ever get useful, because you usually don't need that much meta. Here's a more realistic reference:

(defgeneric generic-name (arg1 arg2 &optional opt)
  (:documentation "Generic function docs
See https://nyxt.atlas.engineer/article/lisp-documentation-patterns.org")
  (:method ((arg1 list) (arg2 (eql 't)) &optional (opt 8))
    "Simple default/termination method."
    t))
(defmethod generic-name ((arg1 4) arg2 &optional opt)
  "A less simple method."
  (generic-name (list arg1) t opt))
A simplified reference for my style of generic writing

I hope that I also motivated you to try more of generics in your next project. And maybe even use them as an architectural device! Thanks for following me up until here!

Leave feedback! (via email)