【问题标题】:Handling user input validation with preconditions functionally in Clojure在 Clojure 中使用先决条件功能处理用户输入验证
【发布时间】:2014-06-12 21:51:07
【问题描述】:

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

我的目标是拥有一种表达性强、功能性强且不抛出异常的方法,从而尽可能提高可组合性。

我知道其他类似的问题,但它们要么引用我不喜欢的{:pre ..., :post ...},因为一旦抛出异常,只能处理字符串化信息,或者引用我不喜欢的一般异常处理,因为 Clojure 应该能够做到这种任务,或者他们指的是 Haskell 的单子风格,例如一个可能是 Monad à la (-> [err succ]) 我也不喜欢,因为 Clojure 应该能够在不需要 Monad 的情况下处理这种任务。

到目前为止,我使用 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{:pre},但不是抛出一个AssertionError,而是抛出一个带有映射的ex-info,但同样:反对抛出异常。

【问题讨论】:

  • 那么,最适合您的是什么?或者,为什么 cond 很难看?
  • 是的,也许我应该解释一下这是什么“丑陋”:这是只返回一个实际结果的不好的可组合性,它几乎可以任意表示,一个带编号的关键字作为错误。我的调用堆栈看起来像这样:(-&gt; msg msg-handler step-handler step-N sub-function),它们都可以返回一个错误类型,但是它们都没有返回相同的成功类型,另一件事是,如果错误返回类型应该短路,我必须手动设计或者必须完成一些中间工作(撤消数据更改或并行通知其他客户端)。

标签: clojure


【解决方案1】:

您的问题的核心似乎在您的评论中:

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

Clojure 1.5+ 有 some-&gt; 线程宏来摆脱 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-&gt; expr nil? form1 form2 ...)(some-&gt; expr form1 form2...)。但现在我们可以使用其他谓词了。


示例

(defn foo [x] (if (even? x) :error-even (inc x)))
(defn bar [x] (if (zero? (mod x 3)) :error-multiple-of-three (inc x)))

(pred-> 1 keyword? foo) ;=> 2
(pred-> 1 keyword? foo foo) ;=> :error-even
(pred-> 1 keyword? foo foo foo) ;=> :error-even

(pred-> 1 keyword? foo bar foo bar) ;=> 5
(pred-> 1 keyword? foo bar foo bar foo bar foo bar) ;=> :error-multiple-of-three

您的用例

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

(deftype ValidationError [msg])

然后您可以将错误代码/消息包装为(-&gt;ValidationError 4002) 并将您的线程更改为

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

【讨论】:

  • 我认为你一针见血。旁注:在我开始使用cond 之前,我使用了来自github.com/egamble/let-elselet? 宏,它非常强大,但看起来仍然像代码中的黑客。我可能还需要重新考虑 fdeftype 在代码中更具表现力。
  • 很好地说明了开源的好处。
  • 在重写时,我意识到另一种模拟 pred-&gt; + deftype 行为的方法:可以使用 some-&gt; 并让每个步骤在失败时返回 (with-meta {:error 5000} nil)
  • @Kreisquadratur 你有 with-meta 的论据倒在那里。您正在给地图{:error 5000} nil 元数据。交换它,你会得到一个例外。您不能将元数据分配给nil。即使这确实有效,我也会认为这是对元数据的滥用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-02
  • 2015-05-25
  • 1970-01-01
  • 2015-04-14
相关资源
最近更新 更多