Implementing the Repository Pattern with Hygienic Macros in Scheme
Hi everyone!
I’ve been working on a new approach for the data layer of my projects lately, and I’d love to poke your brains and get some feedback.
What is the repository pattern? #
Repository Pattern is an architectural software design pattern that decouples the data layer of a program with the core logic and domain.
It brings separation of concerns as your business logic doesn’t know where the data lives and asks the repository/store for it.
It also makes testing your software much easier and your query read/write logic is kept in one place. If you need to optimize a slow database query, you change it in the repository, not sprinkled across fifty different files.
Coming from a background in Scala, Java and other OOP languages and a fascination for FP languages and Lisps (as well as Rust and Haskell), I’ve seen a lot of patterns come and go.
Recently, I noticed a common anti-pattern in my own Scheme projects: a tight coupling between my controller layer and the SQLite implementation. It wasn’t ideal, and I really missed the clean separation of the Repository Pattern.
So, I set out to decouple my data layer from my controller layer in the MVC architecture I love. I wanted to do this using pure functional programming, and I ended up building something really fun using Scheme’s hygienic macros.
(If you want to see this implemented in a real project, check out my example repos here: lucidplan, and byggsteg
I plan to bring this pattern to many of my projects to reap the benefits of the eDSL, better decoupling, and easier testing.
Using it can look like this:
(let* ((job-repo (make-sqlite-job-repository rc)) (jobs (get-jobs job-repo #:limit 50 #:offset 0))) (map (lambda(job) (format #t "\njob: ~a" job )) jobs))
So if you wanted to change tomorrow to from SQLite to PostgreSQL, you just replace one function call and done ✨
The Macros #
I created two main macros. define-record-with-kw magically defines a
keyword-argument constructor, bypassing the need for strict parameter
ordering. It’s highly ergonomic.
define-repo-method is the real
superpower. It accepts any arity, plus optional or #:keyword
arguments. This saves a ton of work, reduces tedious parameter passing,
and gives you a very clean eDSL definition.
(define-module (lucidplan domain repo) #:declarative? #t #:use-module (srfi srfi-9) #:export (define-repo-method define-record-with-kw)) (define-syntax define-repo-method (syntax-rules () ((_ method-name accessor docstring) (define* (method-name repo . args) docstring (apply (accessor repo) args))))) (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 ...)))))
Defining the Domain eDSL #
Here is how I use those macros to define my DSL for a “projects” entity:
(define-module (lucidplan domain project) #:declarative? #t #:use-module (srfi srfi-9) #:use-module (lucidplan domain repo) #:export (get-projects)) ;; -- Record definition --- (define-record-with-kw (<project-repository> %make-project-repository project-repository?) mk-project-repository (get-projects-proc repo-get-projects)) ;; --- eDSL: Embedded Domain Specific Language --- (define-repo-method get-projects repo-get-projects "Retrieves a list of all active projects from the given REPO.")
The SQLite Implementation #
Here is an example SQLite implementation using GNU Artanis. This is completely decoupled from the rest of the application logic.
(define-module (lucidplan sqlite project) #:declarative? #t #:use-module (srfi srfi-9) #:use-module (kracht prelude) #:use-module (artanis db) #:use-module (lucidplan sqlite util) #:use-module (lucidplan domain project) #:export (make-sqlite-project-repository)) ;; --- Artanis + SQLite implementation --- (define (make-sqlite-project-repository rc) (define columns '(id human-id title url vcs-url description created-at updated-at deleted-at)) (define (get-projects) (let* ((query (format #f "SELECT ~a FROM project WHERE deleted_at IS NULL ORDER BY human_id ASC" (symbols->sql-columns-list columns))) (_ (log-info "get-projects query:\n\t~a\n" query)) (rows (map sql-row->scheme-alist (DB-get-all-rows (DB-query (DB-open rc) query)))) (_ (log-info "get-projects rows: ~a\n" (length rows)))) rows)) (mk-project-repository #:get-projects-proc get-projects))
A condensed example with keyword arguments:
;; The DSL (notice how arity is clean) (define-repo-method get-jobs repo-get-jobs "Retrieves a list of active jobs from the given REPO.") ;; SQLite implementation (define* (get-jobs #:key limit offset) (let* ((query (format #f "SELECT ~a FROM job ORDER BY created_at DESC LIMIT ~a OFFSET ~a" (symbols->sql-columns-list columns) limit offset)) (_ (log-info "get-jobs query:\n\t~a\n" query)) (rows (map sql-row->scheme-alist (DB-get-all-rows (DB-query (DB-open rc) query)))) (_ (log-info "get-jobs rows: ~a\n" (length rows)))) rows))
I believe I have something really powerful cooking here, but I know there is always room for improvement.
What do you all think? How would you go about improving this? I’m entirely open to criticism, feedback, and brainstorming!
Thanks for reading this :)