safsaf/STYLE.md
Christopher Baines 5b0e6397dc
All checks were successful
/ test (push) Successful in 9s
Initial commit
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.
2026-04-13 14:24:19 +03:00

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-foo for public constructors, %make-foo for raw constructors.
  • foo? for predicates, foo-field for 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:

  1. Standard library(ice-9 ...), (web ...)
  2. SRFIs(srfi srfi-1), (srfi srfi-9), etc.
  3. External dependencies(knots ...), (webutils ...), (json ...)
  4. 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. Reserve compose for cases where composition is genuinely the idea being expressed.

Data Types

  • Prefer records (define-record-type from (srfi srfi-9)) over ad-hoc lists. Do not browse data with car/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/.