Scheme – Lisp 中的卫生宏的仓库模式
Repository Pattern with Hygienic Macros in Scheme – Lisp

原始链接: https://jointhefreeworld.org/blog/articles/lisps/functional-repository-pattern-in-scheme-with-macros/index.html

本文详细介绍了一种在Scheme中实现仓库模式的新方法,旨在在MVC架构中实现更好的解耦和可测试性。作者在多种语言方面经验丰富,希望避免在Scheme项目中常见的控制器层与SQLite实现之间的紧密耦合。 该解决方案利用Scheme的卫生宏来创建一个特定领域语言(DSL)。引入了两个关键宏:`define-record-with-kw`用于符合人体工程学的关键字参数构造函数,以及`define-repo-method`用于简化具有灵活参数和可选关键字参数的仓库方法的定义。 这种DSL允许以简洁、函数式风格定义实体及其相关的仓库方法。然后,一个具体的SQLite实现,使用Artanis库,是*独立*构建的,从而展示了完全的解耦。作者提供了示例,展示了如何定义和使用这些宏,包括一个精简的示例,其中包含关键字参数以增加灵活性。 作者正在寻求对这种方法的反馈,认为它为Scheme项目的数据访问层提供了一个强大的解决方案,并计划将其集成到未来的工作中。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Scheme 中的卫生宏的仓库模式 – Lisp (jointhefreeworld.org) 6 分,由 jjba23 1 小时前发布 | 隐藏 | 过去 | 收藏 | 讨论 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

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.

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 repo here: lucidplan)

I am working on adding it to byggsteg too.

I plan to bring this pattern to all my projects to reap the benefits of the eDSL, better decoupling, and easier testing. Here is how I built it.

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-record-type type-name
                     (constructor-name field-name ...) pred
                     (field-name accessor-name) ...)

                   
                   (define* (kw-constructor-name #:key field-name ...)
                     (constructor-name field-name ...))

                   
                   (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))



(define-record-with-kw (<project-repository> %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.")

The SQLite Implementation #

Finally, here is the concrete SQLite implementation using 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))


(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:

(define-repo-method get-jobs repo-get-jobs
                  "Retrieves a list of active jobs from the given REPO.")


(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))

Using it can look like

(let*
  (job-repo (make-sqlite-job-repository rc))
  (jobs (get-jobs job-repo #:limit 50 #:offset 0))
.......)

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 :)

联系我们 contact @ memedata.com