Safsaf is a Guile web framework, written using Claude Code running Claude Opus 4.6, based off of the Guix Data Service, Nar Herder and Guix Build Coordinator codebases.
5.3 KiB
Guile Style Guide for Safsaf
This guide draws on Riastradh's Lisp Style Rules, the Guix Coding Style.
Formatting
Indentation
Use 2-space indentation, no tabs. When a subform follows the
operator on the same line, align subsequent subforms to that column.
When the first subform is on the next line, align it with the operator.
Special forms (define, let, lambda, if, cond, match, etc.)
follow standard Scheme indentation.
Line Length
Do not exceed 80 columns.
Parentheses
Never place closing parentheses on their own line. Do not put spaces after opening parentheses or before closing ones.
Blank Lines
Separate top-level forms with a single blank line. Do not place blank lines in procedure bodies except to separate internal definitions from the body.
Square Brackets
Do not use square brackets. They are non-standard and non-portable.
Naming
Write names with English words separated by hyphens. No underscores, camelCase, or abbreviations.
?(predicates): Boolean-returning questions. E.g.,route?,logged-in?.!(mutation): Procedures whose primary purpose is destructive update. E.g.,set-route-handler!. Do not append to every procedure with side effects.%(private): Module-private bindings. E.g.,%make-route,%email-rx.*(variants): Variations on a theme (let*,define*).
Records
(define-record-type <route>
(%make-route method pattern handler name) ; internal constructor
route? ; predicate
(method route-method) ; accessor
(handler route-handler set-route-handler!)) ; accessor + setter
make-foofor public constructors,%make-foofor raw constructors.foo?for predicates,foo-fieldfor accessors,set-foo-field!for setters.
Parameters and Dynamic State
- Guile parameters use the
current-prefix:current-session,current-csrf-token. with-establishes dynamic state and calls a thunk.call-with-calls a procedure with arguments, managing resources or continuations.
Local Variables
Use meaningful names. Single-letter names only for unambiguous index variables in tight loops.
Comments
;;;; file-heading.scm — File Heading
;;;
;;; Section Heading
;;;
;;; Top-level explanatory comment.
(define (fnord zarquon)
;; Fragment comment before code.
(quux zot mumble ;margin note
frotz))
;;;;— File headings.;;;— Section headings (use the sandwich:;;;/;;; Title/;;;) and top-level explanations.;;— Fragment comments, before the code they describe.;— Margin comments, after code on the same line.
Write comments only where the code cannot explain itself.
Docstrings
All public procedures must carry a docstring. Place it as the first expression after the parameter list. Describe what the procedure does, its parameters, and return values:
(define* (route method pattern handler #:key (name #f))
"Create a route. METHOD is a symbol, list of symbols, or '* for any.
HANDLER is a procedure (request body-port) -> (values response body).
NAME is an optional symbol used for reverse routing with path-for."
...)
For parameters, use set-procedure-property! with 'documentation.
Module Definitions
Use define-module with #:use-module and #:export. Group imports:
- Standard library —
(ice-9 ...),(web ...) - SRFIs —
(srfi srfi-1),(srfi srfi-9), etc. - External dependencies —
(knots ...),(webutils ...),(json ...) - Internal modules —
(safsaf ...)
Prefer #:use-module; use #:autoload for heavy or circular deps.
Standalone scripts may use use-modules instead of define-module.
Procedures
- No more than four positional parameters. Use keyword arguments
(via
define*) beyond that. - Keep procedures under roughly 21 lines (excluding docstring). Break long procedures into meaningfully named helpers.
- Prefer purely functional code. Use mutation only for I/O, performance, and low-level utilities.
- Avoid point-free style and functional combinators. Use explicit
lambda. Reservecomposefor cases where composition is genuinely the idea being expressed.
Data Types
- Prefer records (
define-record-typefrom(srfi srfi-9)) over ad-hoc lists. Do not browse data withcar/cdr/cadr. - Do not export record type descriptors (e.g.,
<route>). Export only predicates, constructors, and accessors. - Use
(ice-9 match)for pattern matching. - Use alists for lightweight key-value data (route params, headers, config).
Multiple Return Values
Use values for multiple return values. Prefer (srfi srfi-71) extended
let over (srfi srfi-11) let-values:
(let ((response body (handler request body-port)))
(values response body))
Error Handling
Use (ice-9 exceptions) functionality.
File Organization
- Keep files under 512 lines. Do not exceed 1024.
- Minimize module dependencies.
- Structure files with
;;;section headings.
Testing
Tests use define-suite, suite, test, and is. Keep test files
under tests/.