在 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-validation
、calculate-stuff-using
和 calculate-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 process 叫 with-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 正是为了处理这种情况。
我有一个如下所示的 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-validation
、calculate-stuff-using
和 calculate-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 process 叫 with-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 正是为了处理这种情况。