Scheme developers often wire controllers directly to SQLite queries, creating tight coupling that hampers testing and maintenance. One programmer fixed this by implementing the Repository Pattern using hygienic macros. The result: a functional data layer with an embedded domain-specific language (eDSL) that hides implementation details. This decouples business logic from storage, making mocks straightforward for unit tests.
The Repository Pattern, borrowed from domain-driven design, treats data access as a collection-like interface. In object-oriented languages like Java or Scala, interfaces and dependency injection handle it. Scheme lacks classes, so records and higher-order functions step in. Hygienic macros—Scheme’s syntax-rules—generate boilerplate-free code while preventing name capture, a common Lisp pitfall.
Two macros drive the solution: define-record-with-kw and define-repo-method. The first wraps SRFI-9 records, adding keyword-argument constructors for ergonomic instantiation without positional arguments. The second defines repo methods that delegate to an accessor function on a repository record.
(define-syntax define-repo-method
(syntax-rules ()
((_ method-name accessor docstring)
(define* (method-name repo . args)
docstring
(apply (accessor repo) args)))))
Here’s the full module:
(define-module (lucidplan domain repo)
#:declarative? #t
#:use-module (srfi srfi-9)
#:export (define-repo-method define-record-with-kw))
(define-syntax define-record-with-kw
(syntax-rules ()
((_ (type-name constructor-name pred) kw-constructor-name
(field-name accessor-name) ...)
(begin
;; Define the standard SRFI-9 record
(define-record-type type-name
(constructor-name field-name ...)
pred
(field-name accessor-name) ...)
;; Define the keyword-argument constructor
(define* (kw-constructor-name #:key field-name ...)
(constructor-name field-name ...))
;; Auto-export members
(export type-name pred kw-constructor-name accessor-name ...)))))
Building the Domain Layer
For a “projects” entity, define a repository record holding the get-projects-proc function:
(define-record-with-kw (%make-project-repository project-repository?)
mk-project-repository
(get-projects-proc repo-get-projects))
(define-repo-method get-projects repo-get-projects
"Retrieves a list of all active projects from the given REPO.")
Controllers call (get-projects my-repo), blind to whether it hits SQLite, an in-memory store, or a mock. The macro expands to a variadic define* that applies arguments to the stored procedure.
Concrete Implementation and Trade-offs
The SQLite backend is a simple record with procs wrapping database calls. Although the source cuts off, the pattern implies something like:
(define sqlite-project-repo
(mk-project-repository
#:get-projects-proc
(lambda (repo)
;; Query SQLite here, e.g., using sqlite3 module
(execute "SELECT * FROM projects WHERE active = 1"))))
Why does this matter? In small Scheme apps—think Guile web servers or scripts—this eliminates SQL strings polluting domain code. Testing swaps the repo record with a fake one returning canned data. No global state, pure functions throughout.
Skeptical take: Macros shine for DSLs but add a learning curve. Hygiene ensures safety, yet debugging expanded code requires tracing. It’s not revolutionary—similar to Clojure’s protocol dispatch or Haskell’s typeclasses—but tailored for Guile’s declarative modules. For larger systems, explicit interfaces might scale better than macro-generated ones.
Real-world use appears in the author’s lucidplan repo, with plans for byggsteg. If you’re building FP MVC in Scheme, this cuts boilerplate by 50-70% per entity, based on similar patterns. Ports to Racket or Chez Scheme need minor tweaks for module syntax. Overall, a pragmatic win for solo devs chasing clean architecture without framework bloat.