函数式编程中的共享资源
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 的新值安装。
上下文
我正在使用 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 的新值安装。