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.
Generics are
Let's go through these one by one.
That's the reason a lot of people prefer
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):
Why not just use CD(defmethod)?
Because defining a generic via CD(defmethod) doesn't attach source location info.
So
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.
HYPERTEXTONLY()
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
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 CD(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:
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.
HYPERTEXTONLY()
This is somewhat tautologic with methods, but hear me out:
Generics dispatch over almost all built-in and user-defined types.
Including the
It's easy enough to re-implement
One can also dispatch over type hierarchies, like
So yes, I abuse this power of specializers for common good.
And you should too!
No.
One can use something like
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:
The logic is:
I might've used an
Generic function and method classes are even more obscure.
I didn't encounter a use-case for them yet
(except for
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!
HYPERTEXTONLY()
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:
Used as
Lots of niceties there:
Which all compounds to a good and readable generic definition.
It also looks almost like
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.
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:
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!
(defgeneric compile (expr AMP()optional stack)
(:documentation "Compile Lispy EXPR into binary lambdas...")
(:method ((expr list) AMP()optional stack)
HASH()|...|HASH()))
Generics Have Methods
(defgeneric decode-from-stream (stream)
(:method (stream)
(declare (ignore stream))
(signal 'decode-from-stream-not-implemented))
(:documentation "Decode JSON from STREAM..."))
Generics Dispatch Over Types
(defmethod coerce ((term list) (type (eql SQUOTE()list)) AMP()optional inner-type final-type)
HASH()|...|HASH())
(defmethod coerce ((term list) (type (eql t)) AMP()optional inner-type final-type)
HASH()|...|HASH())
Meta(data): combinations and classes
(defun reverse-append-index (AMP()rest lists)
(let ((fields (remove-duplicates (reduce NUM()'append (nreverse lists))
:key NUM()'first
:from-end t)))
(mapcar NUM()'cons (field-indices fields)
fields)))
(define-method-combination reverse-append-index
:identity-with-one-argument t)
(defgeneric fields (object AMP()key AMP()allow-other-keys)
(:method-combination reverse-append-index)
(:documentation "Return a list of OBJECT fields to inspect..."))
Get the list of properties returned by other methods.
A New Syntax, Maybe?
(defmacro define-generic (name (AMP()rest method-args) AMP()body (documentation . body))
(check-type documentation string "Documentation string")
`(let ((generic (defgeneric ,name (,@(mapcar NUM()SQUOTE()first (mapcar NUM()'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 (setf function-lambda-expression*) ((value list) function AMP()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))))
Mandatory docstring.
Wrapping Up: Write Generics Like I Do
(defgeneric generic-name (arg1 arg2 AMP()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 SQUOTE()t)) AMP()optional (opt 8))
"Simple default/termination method."
t))
(defmethod generic-name ((arg1 cons) (arg2 null) AMP()optional opt)
"Implementation method documentation.
Not strictly necessary, but nice for extra implementation details."
NUM()|...|NUM())
(defgeneric generic-name (arg1 arg2 AMP()optional opt)
(:documentation "Generic function docs
See https://nyxt.atlas.engineer/article/lisp-documentation-patterns.org")
(:method ((arg1 list) (arg2 (eql SQUOTE()t)) AMP()optional (opt 8))
"Simple default/termination method."
t))
(defmethod generic-name ((arg1 4) arg2 AMP()optional opt)
"A less simple method."
(generic-name (list arg1) t opt))