Library Design Guidelines
This guide describes conventions for designing Sigil library APIs. Following these guidelines ensures consistency across the ecosystem and makes libraries intuitive to use.
Naming Conventions
Function Names
Use verb-noun or noun-verb order consistently within a module. Most Sigil libraries use noun-verb for operations on a specific type:
;; Noun-verb (preferred for type-specific operations)
(string-split str delim)
(json-encode value)
(path-join a b)
(dict-ref d key)
;; Verb-noun (for general operations)
(read-file path)
(write-file path content)
(find-module name)Predicates
Functions that return a boolean end with ?:
(string-empty? s) ; Is the string empty?
(file-exists? path) ; Does the file exist?
(json-null? value) ; Is this JSON null?Predicates should:
- Return
#tor#f, never other truthy/falsy values - Have no side effects
- Be safe to call multiple times
Mutators
Functions that modify state end with !:
(vector-set! vec idx val) ; Mutate vector in place
(hash-set! table key val) ; Mutate hash tableSigil prefers immutable data, so ! functions are rare. When an operation could be either:
- Use
!for the mutating version:sort! - Use plain name for the pure version:
sort
Constructors
Use make- prefix for constructors that take configuration:
(make-socket host port)
(make-request method: 'GET url: "/api")Use the type name directly for simple conversions or wrapping:
(point x y) ; Simple struct constructor
(string->number s) ; Conversion
(list->vector lst) ; Type conversionConversions
Use -> to indicate type conversion:
(string->number "42") ; => 42
(number->string 42) ; => "42"
(dict->alist d) ; => ((key . val) ...)
(keyword->string kw) ; => "name"The pattern is source-type->target-type.
Accessors
For struct fields, use the struct name as prefix:
(point-x p) ; Access x field of point
(point-y p) ; Access y field of point
(request-method req) ; Access method field of requestFor dict-like access, use -ref and -set:
(dict-ref d key) ; Get value for key
(dict-set d key val) ; Return new dict with key set
(array-ref arr idx) ; Get element at indexInternal Functions
Prefix internal/private functions with %:
(%parse-header line) ; Internal helper
(%validate-input x) ; Internal validationThese should not be exported. If you need to export something that's not part of the stable API, document it clearly.
Familiarity and Industry Conventions
When a concept has well-established naming across many programming languages, prefer the familiar term over strict Scheme conventions. Sigil aims to be approachable to programmers from diverse backgrounds.
Serialization: Use encode/decode for format serialization:
(json-encode value) ; Not value->json (JSON is a format, not a type)
(json-decode str) ; Not json->value
(base64-encode bytes) ; Familiar from most languages
(url-encode str) ; Standard web terminologyThe -> conversion pattern is best for type-to-type conversions where both sides are Sigil types (string->number, dict->alist). For serialization formats, encode/decode is clearer and more widely understood.
Parsing: Use parse for structured text parsing:
(parse-url str) ; Extract URL components
(parse-date str) ; Parse date stringOther common terms to prefer:
connect/disconnectfor network connections (notmake-connection/close-connection)open/closefor resources (files, streams)start/stopfor services and processesget/setfor property access patterns
This doesn't override Scheme conventions—it supplements them. When in doubt, ask: "What would a Python/JavaScript/Go developer expect this to be called?"
Function Signatures
Argument Order
Follow these conventions for argument order:
- Target object first: The thing being operated on comes first
(string-split str delim) ; str is the target
(dict-ref d key) ; d is the target
(filter pred lst) ; lst is the target (follows Scheme convention)- Required arguments before optional: Don't intermix
;; Good
(string-find str needle start:)
;; Avoid
(string-find str start: needle) ; required after optional- Configuration at the end: Options and flags come last
(json-encode value indent: #t)
(http-get url headers: timeout:)Keyword Arguments
Use keyword arguments for:
- Optional parameters
- Boolean flags
- Configuration options
- When there are more than 2-3 optional positional args
;;; Send an HTTP request.
;;;
;;; Required: method and url
;;; Optional: headers, body, timeout, follow-redirects
(define (http-request method url
(keys: (headers #{})
(body #f)
(timeout 30)
(follow-redirects #t)))
...)Benefits:
- Self-documenting at call site
- Order-independent
- Easy to add new options without breaking existing code
Default Values
Choose sensible defaults that work for the common case:
;; Good defaults
(define (read-file path (keys: (encoding "utf-8")))
...)
(define (json-encode value (keys: (indent #f))) ; compact by default
...)
(define (http-get url (keys: (timeout 30))) ; reasonable timeout
...)Document defaults in the docstring.
Return Values
Be consistent about what functions return:
- Queries return the value or
#fif not found - Predicates return
#tor#f - Transformations return the new value
- Side-effecting operations return a meaningful result (e.g., bytes written) or have an unspecified return value
(dict-ref d key) ; Returns value or errors
(dict-ref d key default) ; Returns value or default
(dict-get d key) ; Returns value or #f
(string-find str needle) ; Returns index or #f
(filter pred lst) ; Returns filtered list
(write-file path content) ; Returns bytes writtenError Handling
When to Signal Errors
Signal errors for:
- Invalid arguments (wrong type, out of range)
- Precondition violations
- Unrecoverable failures
(define (divide a b)
(when (zero? b)
(error "divide: division by zero"))
(/ a b))Error Messages
Write clear, actionable error messages:
;; Good: says what's wrong and what was expected
(error (format "string-ref: index ~a out of bounds (length ~a)" idx len))
;; Bad: vague
(error "invalid index")Include:
- The function name
- What went wrong
- The actual value (if helpful)
- What was expected
Recoverable Failures
For operations that commonly fail (file not found, network timeout), consider:
- Return
#for a default for simple cases:
(dict-get d key) ; Returns #f if not found
(dict-ref d key "foo") ; Returns "foo" if not found- Use
guardfor complex error handling:
(guard (e ((file-error? e) (handle-missing-file e)))
(read-file path))- Document the failure modes in the docstring.
Documentation
Module Documentation
Every module should start with a module-level docstring that explains the purpose of the module and gives brief examples for the most common use cases:
;;; (sigil json) - JSON Serialization and Parsing
;;;
;;; Brief description of what the module provides.
;;;
;;; ## Basic Usage
;;;
;;; ```scheme
;;; (import (sigil json))
;;; (json-decode "{\"name\": \"Alice\"}")
;;; ```Function Documentation
Every exported function needs a docstring:
;;; Brief one-line description of what the function does.
;;;
;;; Longer explanation if needed. Describe the behavior,
;;; edge cases, and relationship to other functions.
;;;
;;; ```scheme
;;; (function-name arg1 arg2)
;;; ; => expected-result
;;;
;;; (function-name edge-case)
;;; ; => edge-case-result
;;; ```
(define (function-name arg1 arg2)
...)Guidelines:
- First line is a complete sentence, imperative mood ("Return...", "Parse...", "Check...")
- Include examples for non-obvious behavior
- Document keyword arguments and their defaults
- Note any side effects
- Mention related functions
Section Comments
Use ;; for section headers (not ;;;):
;; ============================================================
;; String Searching
;; ============================================================
;;; Find the position of a substring...
(define (string-find str needle)
...)This prevents section headers from being parsed as docstrings.
Module Organization
Export List
List exports explicitly at the top of the module immediately following any (import ...) section:
(define-library (sigil example)
(export
;; Core operations
example-parse
example-encode
;; Predicates
example?
example-valid?
;; Utilities
example-merge)
(begin
...))Group related exports with comments. Order by importance or logical grouping, not alphabetically.
Internal Helpers
Keep internal helpers near the functions that use them, or group them in a dedicated section:
;; ============================================================
;; Internal Helpers
;; ============================================================
(define (%parse-token port)
...)
(define (%validate-structure data)
...)Dependencies
Import only what you need:
;; Good: explicit imports
(import (sigil http response)
(sigil io))
;; Avoid: pulling in root modules
(import (sigil http)) ; Only if you actually need most of itIdiomatic Style
Use Appropriate Data Structures
- Dicts for structured data with known keys:
#{ name: "Alice" age: 30 } - Alists for ordered key-value pairs or when building incrementally
- Lists for sequences of homogeneous items
- Arrays for indexed access and JSON interop:
#[1 2 3] - Structs for domain types with fixed fields
Pattern Matching
Use match when it clarifies intent:
;; Clear dispatch on structure
(define (process-message msg)
(match msg
(#{ type: "text" content: c } (handle-text c))
(#{ type: "image" url: u } (handle-image u))
(_ (error "Unknown message type"))))But don't overuse it for simple cases:
;; Overkill - just use if
(match x
(#t (do-something))
(#f (do-other)))
;; Better
(if x (do-something) (do-other))Control Flow
Use when/unless for single-branch conditionals:
(when (file-exists? path)
(delete-file path))
(unless (valid? input)
(error "Invalid input"))Use if when you have both branches:
(if (empty? lst)
default-value
(car lst))Use cond for multiple conditions:
(cond
((negative? n) 'negative)
((zero? n) 'zero)
(else 'positive))Threading
Use -> for data transformation pipelines:
(-> data
(filter valid? _)
(map transform _)
(take 10 _))Avoid These Patterns
- Deep nesting: Refactor into smaller functions
- Magic numbers: Use named constants
- Stringly-typed data: Use symbols, keywords, or structs
- Side effects in unexpected places: Document them clearly
API Evolution
Backwards Compatibility
When evolving an API:
- Add, don't modify: Add new keyword arguments instead of changing signatures
- Deprecate before removing: Warn users before breaking changes
- Version appropriately: Use semver (major.minor.patch)
Deprecation
Mark deprecated functions clearly:
;;; DEPRECATED: Use `new-function` instead.
;;;
;;; This function will be removed in version 2.0.
(define (old-function x)
(new-function x))Checklist
When reviewing a library API, check:
- [ ] Naming: Consistent verb-noun order, proper use of
?,!,-> - [ ] Signatures: Sensible argument order, keyword args for options
- [ ] Errors: Clear messages, appropriate failure modes
- [ ] Documentation: Module docs, function docs with examples
- [ ] Organization: Clear exports, logical grouping
- [ ] Style: Idiomatic patterns, appropriate data structures