188 lines
5.3 KiB
Markdown
188 lines
5.3 KiB
Markdown
|
|
# Guile Style Guide for Safsaf
|
||
|
|
|
||
|
|
This guide draws on
|
||
|
|
[Riastradh's Lisp Style Rules](https://mumble.net/~campbell/scheme/style.txt),
|
||
|
|
the [Guix Coding Style](https://guix.gnu.org/manual/1.5.0/en/html_node/Coding-Style.html).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 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
|
||
|
|
|
||
|
|
```scheme
|
||
|
|
(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
|
||
|
|
|
||
|
|
```scheme
|
||
|
|
;;;; 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:
|
||
|
|
|
||
|
|
```scheme
|
||
|
|
(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`:
|
||
|
|
|
||
|
|
```scheme
|
||
|
|
(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/`.
|