我有一个类似于以下内容的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块,但感觉就像使用控件流的异常一样,感觉不正确。< / p>
我不能让异常逃避这个功能,因为我用它来映射一系列地图,我仍然想继续处理其余部分。
我想我之后的事情是这样的?
(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
谢谢!
答案 0 :(得分:6)
如果验证失败指示错误情况,则异常(和try-catch块)可能是处理错误的最佳方式。特别是如果它不是正常的&#34;出现(即无效的遗嘱等)。
更多&#34;正常&#34;但仍然无效&#34;在某些情况下,您可以使用some->
(发音为&#34; some-thread&#34;)来静静地压制&#34;坏&#34;案例。只需让验证器返回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
表示&#34;数据验证失败&#34;,因此您的数据中不能包含nil
。
使用nil
作为短路处理的特殊值可以起作用,但使用普通旧例外可能更容易,特别是对于明显&#34;错误数据的情况&#34;:< / p>
(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
或任何其他&#34; sentinal&#34;赋予特殊含义。值,并使用Exception
用于在出现错误时更改控制流的正常目的。
答案 1 :(得分:2)
这是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"))
答案 2 :(得分:1)
其他答案中的一个示例使用具有缺陷的some->
宏:每次失败都应该将消息打印到控制台并返回nil
。这并不好,因为nil
值也可能表示良好的结果,特别是对于空集合。毋庸置疑,您不仅需要打印错误,还要以某种方式处理错误或将其记录在某处。
重构代码的最简单方法就是分解代码。比方说,你可以将第一个if
的负分支中的所有内容放入一个单独的函数中,就是这样。这两个函数将变得更容易测试和调试。
至于我,这将是最好的选择,因为它会立即解决问题。
有例外情况也很好。不要发明自己的异常类,只需使用ex-info
抛出地图。一旦被捕获,这样的异常将返回与其一起抛出的所有数据:
(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的情况,但要注意过度设计问题。
答案 3 :(得分:0)
我遇到了同样的问题。我的解决方案是复制some-&gt;&gt;宏并调整一下:
(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))))))
答案 4 :(得分:0)
我创建了Promenade来处理这种情况。