mark-watson/loving-common-lisp/common-lisp-skill

Claude Code and agent skill by mark-watson/loving-common-lisp

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Write Common Lisp in Mark Watson's style for the loving-common-lisp project. Quicklisp, ASDF, UIOP / Dexador / QURI / cl-json, file IO, and a deliberately plain no-LOOP style.

Install

npx vskill@latest install mark-watson/loving-common-lisp/common-lisp-skill

SKILL.md

Skill: Common Lisp in Mark Watson's Style

Writing Common Lisp for Mark Watson's loving-common-lisp project. The audience is a model strong in Python and TypeScript and weak in Lisp, so treat the rules below as absolute.

0. How you work here — you do not run code

The developer runs everything. You never execute Lisp, and you never claim you did.

  1. Forms to evaluate — the smallest set that proves the point, in one lisp

block, in evaluation order, starting with any ql:quickload. Say what you expect back.

  1. A file to load — write it, then ask the developer to run

sbcl --script path/to/file.lisp and paste what it prints.

Ask for the specific thing you need ("paste the value of the last form"), not "the output". Verify the risky parts only; read the real error before guessing. Until output comes back, say the code is unverified. sbcl --script skips ~/.sbclrc, so it works whatever the developer's Quicklisp setup looks like; if the file uses uiop: or asdf:, put (require :asdf) on its first line.


1. The ten rules

  1. Never write `loop` (see 2.5). Leave old loop forms in src/ alone unless asked.
  2. Every `(` must be closed — an unbalanced paren is the most common failure.
  3. Locals must be declared by let/let*/dolist/dotimes/defun args; there is no "assign and it exists".
  4. Use `format`, not `print`, for anything a human reads.
  5. A function returns its last form; use return/return-from to exit early.
  6. Only `nil` is false. 0, "", and #() are all true; there is no separate False.
  7. Strings compare with `string=` or `equal`; numbers with =.
  8. Hash tables with string keys need `:test #'equal`.
  9. `if` takes both branches; with one branch use when/unless.
  10. Match the surrounding file — its package, indentation, and naming.

2. Mark's style

2.1 Layout and naming
lisp
;;; Copyright (C) 2026 Mark Watson <markw@markwatson.com>
;;; MIT License
;;; One-line description of this file.
(in-package #:my-package)
(defun my-function (argument)
"Docstring in the imperative mood, one sentence."
(format nil "value is ~a" (* argument 2)))

Two spaces per indent; closing parens stack on the last line. ;;; headers, ;; inside functions, ; at end of line. A docstring is the first form of defun/defclass/defstruct/defvar/defparameter. Names: fetch-url; predicates valid-uri-p; globals *earmuffs*; constants +plus-signs+; accessors type-slot.

2.2 Packages and globals

Use #:name (uninterned symbol) for the package name, and export everything the outside world calls — otherwise callers get The symbol X is not external.

lisp
(defpackage #:my-system
(:use #:cl)
(:export #:run #:response #:response-status))
(in-package #:my-system)
(defparameter *base-url* "https://api.example.com" ; resets on reload: config
"Base URL for API requests.")
(defvar *cache* (make-hash-table :test #'equal) ; set when unbound: state
"Memoisation cache. Keeps its value when the file is reloaded.")

Inside the package call your own functions bare; outside use my-system:run. cl-user is the wrong place for real code.

2.3 Functions

Use either &optional or &key, never both.

lisp
(defun greet (name &key (greeting "Hello") (excited-p nil))
"Return a greeting string for NAME."
(format nil "~a, ~a~a" greeting name (if excited-p "!" ".")))
lisp
(greet "Mark") ; => "Hello, Mark."
(greet "Mark" :excited-p t) ; => "Hello, Mark!"
(greet "Mark" :nope 1) ; ERROR: Unknown &KEY argument: :NOPE
Never write `(defun f (x &optional y &key z) ...)`. The optional parameter
swallows the first keyword, so (f 1 :z 2) binds :Z to y and then signals
odd number of &KEY arguments.

Order is &optional, then &rest, then &key. Never name a function after a CL symbol — report, count, remove, find, sort, length are locked, and redefining one signals Lock on package COMMON-LISP violated. Add a suffix.

2.4 Conditionals
lisp
(if test then else) ; exactly 2 or 3 parts
(when test body...) ; no else branch
(unless test body...) ; negated when
(cond (test1 body1...) (test2 body2...) (t default...))
(case value (1 :one) (2 :two) (t :other))
2.5 No loop — the translation table

Mark, in manuscript/loop_macros.md: "a complex looping macro that I almost never use in my own code because it does not look Lisp like."

Do not writeWrite instead
(loop for x in xs do (f x))(dolist (x xs) (f x))
(loop for i from 0 below n do ...) / (loop repeat n do ...)(dotimes (i n) ...)
(loop while test do ...)(do () ((not test)) ...)
(loop for x in xs collect (f x))(mapcar #'f xs)
(loop for x in xs when (p x) collect x)(remove-if-not #'p xs)
(loop for x in xs unless (p x) collect x)(remove-if #'p xs)
(loop for x in xs sum x)(reduce #'+ xs) or an accumulator
(loop for x in xs count (p x))(count-if #'p xs)
(loop for x in xs always (p x))(every #'p xs)
(loop for k being the hash-keys of h ...)maphash + collect (see 2.6)

The accumulator idiom you will use constantly — push builds the list backwards, nreverse at the end, and the third argument of dolist/dotimes is the return value:

lisp
(defun squares-of-evens (numbers)
"Return the squares of the even members of NUMBERS."
(let ((result nil))
(dolist (n numbers (nreverse result))
(when (evenp n) (push (* n n) result)))))
2.6 Iteration, hash tables, structs
lisp
(dolist (item items) (format t "~a~%" item))
(let ((sum 0)) (dolist (n '(1 2 3) sum) (incf sum n))) ; returns SUM
(dotimes (i 5) (format t "~d~%" i)) ; 0 .. 4
(dolist (x '(1 2 3 4)) (when (= x 3) (return))) ; early exit
;; do: (do ((var init step)...) ((test result...) body...)
(do ((i 0 (1+ i)) (acc nil (cons i acc)))
((= i 3) (nreverse acc))) ; => (0 1 2)
lisp
(defun hash-table-keys (table)
"Return a list of the keys in TABLE."
(let ((keys nil))
(maphash (lambda (k v) (declare (ignore v)) (push k keys)) table)
(nreverse keys)))
(defstruct point "A 2D point." x y)
;; => (make-point :x 1 :y 2) (point-x p) (setf (point-x p) 7) (point-p p)

The (declare (ignore v)) is needed or SBCL warns about the unused variable. defstruct defines make-<name>, <name>-<slot>, and <name>-p; use it instead of a class when there is no behaviour.


3. Language core

3.1 Syntax
lisp
(fn arg1 arg2) ; call FN with two arguments
; comment to end of line #| block comment |#
"a string" #\a ; the character a
:keyword ; self-evaluating keyword symbol
'symbol ; the symbol, not its value
'(1 2 3) ; a literal list #'function-name ; function object
#(1 2 3) ; a vector #p"/a/path" ; a pathname
42 3.14 1.0d0 1/3 ; integer, single-float, double-float, ratio
nil t ; false / true (also '() for nil)

The reader upcases symbols, so Foo and FOO are the same symbol. 1.0 is a single-float; write 1.0d0 for a double.

3.2 Truth and equality — read this twice

Only nil is false. 0, "", #(), and :false are all true.

TestUse it forExample
=numbers only(= 1 1.0) → T
char= / char-equalcharacters, case-sensitive / not(char-equal #\a #\A) → T
string= / string-equalstrings(string= "a" "a") → T
eqidentical object, same symbol(eq 'a 'a) → T
eqlnumbers and characters of the same type(eql 1 1.0) → NIL
equallists, strings, structures — the default choice(equal '(1 2) '(1 2)) → T
equalplike equal, case/type-insensitive, compares vectors(equalp #(1 2) #(1 2)) → T
nullis it nil(null nil) → T
memberlist membership(member 2 '(1 2 3)) → (2 3)

Two traps: (equal #(1 2) #(1 2)) is NIL — equal does not descend into vectors, so use equalp. And boolean functions return a generalised truth value, not necessarily t: (member 3 '(1 2 3)) → (3), (search "zz" "abc") → NIL.

3.3 Locals, recursion, multiple values
lisp
(let ((a 1) (b 2)) (+ a b)) ; bindings computed in parallel
(let* ((a 1) (b (+ a 1))) b) ; later bindings see earlier ones
(let ((total 0)) (incf total 5) (decf total 2) (setf total 0)) ; => 5, 3, 0
(defun count-items (list)
"Count LIST without a loop."
(labels ((walk (rest count)
(if (null rest) count (walk (cdr rest) (1+ count)))))
(walk list 0)))
(multiple-value-bind (q r) (floor 7 2) ; => 3 rem 1
(format nil "~d rem ~d" q r))

let for independent bindings, let* when one depends on another. setf assigns to anything: variables, (aref v i), (gethash k h), (point-x p). labels defines local recursive functions, flet non-recursive ones, (lambda (x) ...) anonymous ones. gethash, read-from-string, dex:get, and uiop:run-program return extra values — capture them with multiple-value-bind, not let.

3.4 Lists, vectors, sequences
lisp
(cons 1 '(2 3)) ; => (1 2 3) (list 1 2 3) ; => (1 2 3)
(car '(1 2 3)) ; => 1 (also FIRST) (cdr xs) ; => (2 3) (also REST)
(length '(1 2 3)) ; => 3 (nth 1 '(1 2 3)) ; => 2
(append '(1 2) '(3 4)) ; => (1 2 3 4) (reverse '(1 2 3)) ; => (3 2 1)
(subseq '(1 2 3 4) 1 3) ; => (2 3) (last '(1 2 3)) ; => (3)
(push 1 xs) ; (setf xs (cons 1 xs)) (pop xs) ; returns (car xs)
(sort (copy-list xs) #'<) ; destructive: copy first
(assoc "b" alist :test #'string=) ; => ("b" . 2)
(aref v 1) ; read element 1 (setf (aref v 1) 9)
(elt v 0) ; works on vectors and lists

mapcar, remove-if, remove-if-not, reduce, find-if, position, count-if, some, and every are the sequence functions you reach for instead of loop — see the table in 2.5. sort and nreverse destroy their argument; copy first when the list is shared: (sort (copy-list xs) #'<).

lisp
(let ((table (make-hash-table :test #'equal))) ; EQUAL for string keys
(setf (gethash "a" table) 1) ; write
(gethash "a" table) ; => 1, T
(gethash "missing" table :none) ; => :NONE
(remhash "a" table))

The default hash-table :test is #'eql, which will not find string keys built at runtime — pass :test #'equal for strings, #'eq for keywords.

3.5 CLOS, and alists
lisp
(defclass animal ()
((name :initarg :name :accessor animal-name)
(sound :initarg :sound :initform "..." :accessor animal-sound)))
(defmethod speak ((a animal))
(format nil "~a says ~a" (animal-name a) (animal-sound a)))
(defclass dog (animal) ())
(defmethod speak ((d dog)) (format nil "~a barks" (animal-name d)))
;; (speak (make-instance 'dog :name "Rex")) => "Rex barks"

Slots need :initarg to be settable from make-instance and :accessor (or :reader) to be readable; :initform gives a default. defmethod dispatches on the class of its arguments, and a subclass method overrides the parent.

lisp
;; alist: a list of (key . value) conses — the shape decoded JSON takes
(defparameter *config* '(("host" . "localhost") ("port" . 8080)))
(cdr (assoc "host" *config* :test #'string=)) ; => "localhost"
(getf '(:name "Mark" :age 60) :name) ; plist => "Mark"

4. Strings and FORMAT

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/mark-watson/loving-common-lisp/common-lisp-skill/badge)](https://verified-skill.com/skills/mark-watson/loving-common-lisp/common-lisp-skill)

Something wrong with this skill or its page? Tell us.