在Clojure中处理带前置条件的用户输入验证

在Clojure中处理带前置条件的用户输入验证,clojure,Clojure,我编写了一个游戏服务器,必须检查来自用户的消息是否正确有效。这意味着它们必须具有正确的语法,符合参数格式,并且语义正确,即符合游戏规则 我的目标是拥有一种表达性、功能性的方式,而不抛出异常,从而尽可能好地实现可组合性 我知道还有其他类似的问题,但它们要么是指我不喜欢的{:pre…,:post…},因为一旦抛出异常,只能处理字符串化的信息,要么是指我不喜欢的异常处理,因为Clojure应该能够完成这类任务,或者他们引用哈斯凯尔的一元风格,例如一个可能的单子a(>[err such]),这也是我不喜

我编写了一个游戏服务器,必须检查来自用户的消息是否正确有效。这意味着它们必须具有正确的语法,符合参数格式,并且语义正确,即符合游戏规则

我的目标是拥有一种表达性、功能性的方式,而不抛出异常,从而尽可能好地实现可组合性

我知道还有其他类似的问题,但它们要么是指我不喜欢的
{:pre…,:post…}
,因为一旦抛出异常,只能处理字符串化的信息,要么是指我不喜欢的异常处理,因为Clojure应该能够完成这类任务,或者他们引用哈斯凯尔的一元风格,例如一个可能的单子a
(>[err such])
,这也是我不喜欢的,因为Clojure应该能够在不需要单子的情况下处理这类任务

到目前为止,我使用cond作为先决条件检查程序和错误代码,然后将其发送回发送请求的客户机:

(defn msg-handler [client {:keys [version step game args] :as msg}]
  (cond
    (nil? msg)                     :4001
    (not (valid-message? msg))     :4002
    (not (valid-version? version)) :5050
    (not (valid-step?    step))    :4003
    (not (valid-game-id? game))    :4004
    (not (valid-args?    args))    :4007
    :else (step-handler client step game args)))
类似的

(defn start-game [game-id client]
  (let [games   @*games*
        game    (get games game-id)
        state   (:state game)
        players (:players game)]
    (cond
      (< (count players) 2) :4120
      (= state :started)    :4093
      (= state :finished)   :4100
      :else ...)))
(defn开始游戏[游戏id客户端]
(让"游戏")*
游戏(获取游戏id)
状态(:状态游戏)
玩家(:玩家游戏)]
(续)
(<(计数玩家)2):4120
(=状态:已启动):4093
(=状态:已完成):4100
:其他…)

另一种方法是编写一个宏,类似于
defn
{:pre}
,但不是抛出一个
AssertionError
抛出一个带有映射的
ex-info
,而是:反对抛出异常。

你问题的核心似乎是你的评论:


是的,也许我应该解释一下这是什么“丑陋”:这是仅仅返回一个实际结果的不可组合性,它几乎可以任意表示,并且一个编号的关键字作为错误。我的调用堆栈如下:
(>msg msg handler step handler step-N sub function)
,它们都可以返回这一个错误类型,但没有一个返回相同的成功类型,另一件事是,我必须手动设计en error返回类型是否短路,或者是否需要执行一些中间工作(撤消数据更改或同时通知其他客户端)

Clojure 1.5+具有
some->
线程宏以摆脱
nil?
检查样板。我们只需轻轻调整代码以替换
nil?
检查,以进行我们选择的检查

(defmacro pred->
  "When predicate is not satisfied, threads expression into the first form
  (via ->), and when that result does not satisfy the predicate, through the
  next, etc. If an expression satisfies the predicate, that expression is
  returned without evaluating additional forms."
  [expr pred & forms]
  (let [g (gensym)
        pstep (fn [step] `(if (~pred ~g) ~g (-> ~g ~step)))]
    `(let [~g ~expr
           ~@(interleave (repeat g) (map pstep forms))]
       ~g)))
注意,在这个定义中,
(pred->expr nil?form1 form2…
(some->expr form1 form2…
),但现在我们可以使用其他谓词


例子
您的用例 一个灵活的选择是为验证错误制作一个包装器

(deftype ValidationError [msg])
然后,您可以将错误代码/消息包装为
(>validationError4002)
,并将线程设置为

(pred-> msg #(instance? ValidationError %) 
  msg-handler step-handler step-N sub-function)

那么,这对你来说最理想的情况是什么?或者,为什么cond很难看?是的,也许我应该解释一下这是什么“难看”:只是返回一个实际结果的可组合性不好,这个结果几乎可以任意表示,一个编号的关键字作为错误。我的调用堆栈看起来像这样:
(->msg msg handler step handler step-N sub function)
,它们都可以返回这一个错误类型,但没有一个返回相同的成功类型,另一件事是,我必须手动设计en error返回类型是否短路,或者是否需要执行一些中间工作(撤消数据更改或同时通知其他客户端)。我想你说得一针见血。旁注:在我开始使用
cond
之前,我使用了
let?
宏,该宏功能非常强大,但看起来仍然像是代码中的黑客。我可能还需要重新考虑f
deftype
以使代码更具表现力。这是开源好处的一个很好的说明。虽然重写我实现了另一种模拟
pred->
+
deftype
行为的方法:一个人可以使用
some->
,并在失败时让每一步返回
(meta{:error 5000}nil)
。@kreisquadraur你有向后的
meta
参数。你给出了映射
{:error 5000} NIL元数据。交换它,你会得到一个异常。你不能将元数据分配给<代码> NIL。即使这样做,我也会认为它是滥用元数据。
(pred-> msg #(instance? ValidationError %) 
  msg-handler step-handler step-N sub-function)