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
- functions
- with multiple methods,
- type dispatch,
- and metadata (like documentation and method combinations.)
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)
#|...|#))
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..."))
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)
#|...|#)
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 &nu