在 Clojure 函数中编写多个 if 检查的更好方法?

Better way of writing multiple if checks in a Clojure function?

我有一个如下所示的 Clojure 函数。

(defn calculate-stuff [data]
  (if (some-simple-validation data)
    (create-error data)
    (let [foo (calculate-stuff-using data)]
      (if (failed? foo)
        (create-error foo)
        (let [bar (calculate-more-stuff-using foo)]
          (if (failed? bar)
            (create-error bar)
            (calculate-response bar)))))))

哪个工作正常但有点难读,所以我想知道是否有更惯用的 Clojure 写法?

我考虑过让 some-simple-validationcalculate-stuff-usingcalculate-more-stuff-using 抛出异常并使用 try/catch 块,但感觉就像使用异常来控制流程,但并没有感觉正确。

我不能让异常逃脱这个函数,因为我正在使用它来映射一系列地图,我仍然想继续处理剩余部分。

我猜我想要的是这样的东西?

(defn calculate-stuff [data]
  (let-with-checking-function
    [valid-data (some-simple-validation data)
     foo (calculate-stuff-using valid-data)
     bar (calculate-more-stuff-using foo)]
    failed?)                    ; this function is used to check each variable
      (create-error %)          ; % is the variable that failed
      (calculate-response bar)) ; all variables are OK

谢谢!

如果验证失败表示错误情况,则异常(和 try-catch 块)可能是处理它的最佳方式。特别是如果它不是 "normal" 事件(即无效的客户 ID 等)。

对于更多 "normal" 但仍有 "invalid" 个案例,您可以使用 some->(发音为 "some-thread")来安静地压制 "bad" 个案例。只需让您的验证器 return nil 处理错误数据,some-> 将中止处理链:

(defn proc-num [n]
  (when (number? n)
    (println :proc-num n)
    n))

(defn proc-int [n]
  (when (int? n)
    (println :proc-int n)
    n))

(defn proc-odd [n]
  (when (odd? n)
    (println :proc-odd n)
    n))

(defn proc-ten [n]
  (when (< 10 n)
    (println :proc-10 n)
    n))

(defn process [arg]
  (when (nil? arg)
    (throw (ex-info "Cannot have nil data" {:arg arg})))
  (some-> arg
    proc-num
    proc-int
    proc-odd
    proc-ten))

结果:

(process :a) => nil

(process "foo") => nil

:proc-num 12
:proc-int 12
(process 12) => nil

:proc-num 13
:proc-int 13
:proc-odd 13
:proc-10 13
(process 13) => 13

(throws? (process nil)) => true

话虽如此,您现在使用 nil 表示 "data validation failure",因此您的数据中不能包含 nil


对无效数据使用异常

使用 nil 作为短路处理的特殊值可以工作,但使用普通旧异常可能更容易,特别是对于明显 "bad data":[=24 的情况=]

(defn parse-with-default [str-val default-val]
  (try
    (Long/parseLong str-val)
    (catch Exception e
      default-val))) ; default value

(parse-with-default "66-Six" 42) => 42

我有 a little macro to automate this processwith-exception-default:

(defn proc-num [n]
  (when-not (number? n)
    (throw (IllegalArgumentException. "Not a number")))
  n)

(defn proc-int [n]
  (when-not (int? n)
    (throw (IllegalArgumentException. "Not int")))
  n)

(defn proc-odd [n]
  (when-not (odd? n)
    (throw (IllegalArgumentException. "Not odd")))
  n)

(defn proc-ten [n]
  (when-not (< 10 n)
    (throw (IllegalArgumentException. "Not big enough")))
  n)

(defn process [arg]
  (with-exception-default 42  ; <= default value to return if anything fails
    (-> arg
      proc-num
      proc-int
      proc-odd
      proc-ten)))

(process nil)    => 42
(process :a)     => 42
(process "foo")  => 42
(process 12)     => 42

(process 13)     => 13

这避免了赋予 nil 或任何其他 "sentinal" 值特殊含义,并使用 Exception 用于在出现错误时更改控制流的正常目的。

这是 Clojure 代码库中的常见问题。一种方法是将您的数据包装成提供更多信息的内容,即操作是否成功。有几个库可以帮助您。

例如猫 (http://funcool.github.io/cats/latest/):

(m/mlet [a (maybe/just 1)
         b (maybe/just (inc a))]
  (m/return (* a b)))

或有结果 - 我在这方面提供了帮助 (https://github.com/clanhr/result):

(result/enforce-let [r1 notgood
                     r2 foo])
    (println "notgoof will be returned"))

我遇到了同样的问题。我的解决方案是复制 some->> 宏并稍微调整一下:

(defmacro run-until->> [stop? expr & forms]
     (let [g (gensym)
           steps (map (fn [step] `(if (~stop? ~g) ~g (->> ~g ~step)))
               forms)]
        `(let [~g ~expr
               ~@(interleave (repeat g) (butlast steps))]
             ~(if (empty? steps)
                g
                (last steps)))))

此宏将检查您预定义的条件,而不是检查 nils。例如:

(defn validate-data [[status data]]
    (if (< (:a data) 10)
       [:validated data]
       [:failed data]))

(defn calculate-1 [[status data]]
     [:calculate-1 (assoc data :b 2)])

(defn calculate-2 [[status data]]
    (if (:b data)
       [:calculate-2 (update data :b inc)]
       [:failed data]))

(deftest test
    (let [initial-data [:init {:a 1}]]
       (is (= [:calculate-2 {:a 1, :b 3}] 
              (run-until->> #(= :failed (first %))
                            initial-data
                            (validate-data)
                            (calculate-1)
                            (calculate-2))))

       (is (= [:failed {:a 1}] 
              (run-until->> #(= :failed (first %))
                            initial-data
                            (validate-data)
                            (calculate-2))))))

其他答案中的一个例子使用了有缺陷的 some-> 宏:每次失败都应该在控制台和 return nil 中打印一条消息。这不好,因为 nil 值也可能表示好的结果,尤其是对于空集合。不用说,您不仅需要打印错误,还需要以某种方式处理它或将其记录在某处。

重构代码的最简单方法就是分解它。比如说,您可以将第一个 if 的负分支中的所有内容放入一个单独的函数中,仅此而已。这两个功能将变得更容易测试和调试。

对于我来说,这将是最好的选择,因为它会立即解决问题。

有例外的案例也不错。不要发明自己的 Exception 类,只需使用 ex-info 抛出一个 map。一旦被捕捉到,这样的异常returns 随之抛出的所有数据:

(if (some-checks data)
  (some-positive-code data)
  (throw (ex-into "Some useful message" {:type :error 
                                         :data data})))

抓住它:

(try
  (some-validation data)
(catch Exception e
  (let [err-data (ex-data e)]
    ; ...)))

最后,可能会有使用 monad 的情况,但要注意过度设计问题。

我创建 Promenade 正是为了处理这种情况。