函数式编程中的共享资源

Shared resource in functional programming

上下文

我正在使用 Clojure 并遵循函数式编程范式编写应用程序。在这个应用程序中,我有两个 HTTP 端点:/rank/invite。在 /rank 中,应用程序根据分数对客户列表进行排名。在 /invite 中,该应用程序收到一位客户对另一位客户的邀请,这应该会改变一些客户的分数。

问题

来自客户的数据保存在一个名为 record 的地图矢量中。

暂时搁置引用透明性,record 应该是端点之间的共享资源,一个读取它并在排名函数中使用它来响应 HTTP 请求,另一个读取它并更新分数在里面。

现在,考虑到函数式编程,record 无法更新,因此 /invite 端点应该读取它并且 return 一个新的 record',问题是, /rank 端点设置为使用 record,但是当生成新的 record' 时,它应该使用它而不是原来的。

我的解决方案

我理解,在这种情况下,整个应用程序不能完全从功能上讲是纯粹的。它从文件中读取初始输入并接收来自外部环境的请求,所有这些都使得处理这些部分的函数不是引用透明的。几乎每个程序都会有这些小部分非功能性代码,但为了尝试不向应用程序添加更多这些非功能性功能,而这个应用程序只是为了练习一些功能性编程,我并不坚持 record 到数据库或其他东西,因为如果是这种情况,问题就会解决,因为我可以调用一个函数来更新数据库上的记录。

到目前为止我最好的想法是:端点是用 Compojure 的 routes 函数创建的,所以在 /invite 端点我应该处理新的 record' 向量,然后重新创建/rank 端点为其提供 record'。重新创建 /rank 的这一部分是我正在努力的,我试图再次调用 routes 并再次定义所有端点,希望它将覆盖 routes 的原始调用,但正如我所料,我相信 Compojure 遵循函数式编程,一旦创建,路由是不可变的并且新的路由调用不会覆盖任何东西,它只会创建新的路由,这些路由将留在空白中,而不是附加到 HTTP请求。

那么,是否可以用纯函数代码做我想做的事情?或者不可避免地会破坏引用透明性,我应该坚持 record 到文件或数据库以更新它?

PS.: 我不知道这是否相关,但我是 Clojure 和任何类型的网络交互的新手。

Clojure(与 Haskell 相反)并不纯粹,它有自己的结构来管理对共享状态的更改。它不使用类型系统(如 Haskell 中的 IO monad)来隔离不纯性,而是促进使用纯函数并使用不同类型的引用(atom, agent, ref)来管理状态,定义清晰的语义如何以及何时状态已更改。

对于您的方案,Clojure 的 atom 将是最简单的解决方案。它提供了关于如何管理其状态的明确契约。

我会创建一个 var 将您的记录保存在一个原子中:

(def record (atom [])) ;; initial record is empty

然后在您的 rank 端点中,您可以使用 deref@ 作为语法糖来使用它的值:

(GET "/rank" []
  (calculate-rank @record))

而您的 invite 端点可以使用 swap!:

自动更新您的记录值
(POST "/invite/:id" [id]
  (invite id)
  (swap! record calculate-new-rank id)
  (create-response))

您的 calculate-new-rank 函数将如下所示:

(defn calculate-new-rank [current-record id]
  ;; do some calculations
  ;; create a new record value and return it
  (let [new-record ...]
    new-record))

您的函数将使用存储在 atom 中的当前版本数据和其他可选参数来调用,您的函数的结果将作为您的 atom 的新值安装。