如何在Clojure中的try catch块中创建占位符变量

时间:2016-02-04 22:32:21

标签: clojure

我有一些我会写的Java代码:

String var1;
String var2;
int var3;

try {
    String var1 = func1_that_might_throw();
    String var2 = func2_that_might_throw();
    int var3 = func3_that_might_throw();

    do_something_that_might_throw(var1, var2, var3);
} catch (Exception e) {
    cleanup_resources(var1, var2, var3);
}

把它变成Clojure是一场噩梦。

(try+
  (let [var1 (func1_that_might_throw)
        var2 (func2_that_might_throw)
        var3 (func3_that_might_throw)]
    (do_something_that_might_throw var1 var2 var3))
  (catch Exception e
    (cleanup_resources var1 var2 var3)))

问题是var1var2var3不存在于catch块中。 将let移到try之外是有问题的,因为函数1到3可能会抛出并需要捕获并清理资源。

我认为我可能需要的只是try之外的三个占位符变量,但a)我甚至不知道clojure是否允许这样做,以及b)使一切变得更复杂。我不认为这是老兵在clojure中这样做的方式。这里的解决方案是什么?

4 个答案:

答案 0 :(得分:3)

这是程序设计和结构的问题。这听起来像是关于资源状态管理的鸡与蛋问题。而不是解决这个问题,如果你可以将这个问题分解为:

,那么它可能会减少很多痛苦。
  • 创建状态并处理创建状态错误的函数
  • 另一个完成工作并处理工作中所引起的错误的函数。

这些是由一个功能解决的单独问题。如果你能把这些东西分开,事情会变得容易些。

现在,除了讲道之外,clojure有许多处理可变状态的方法,并且不假装整个世界可以说服“正确的方式”做事,而Cl​​ojure确实允许你简单地创建和设置变量只是在java中排队。你在实践中从未见过这些的原因是因为它们总是表示问题是以向后的方式解决或者与另一个不同的问题混在一起。

尝试一切,你可以像以下那样避免编写代码:

user> (def ^:dynamic var1)
#'user/var1
user> (def ^:dynamic var2)
#'user/var2
user> (def ^:dynamic var3)
#'user/var3

user> (defn do-stuff []
        (binding [var1 nil
                  var2 nil
                  var3 nil]
          (try
            (set! var1 42)
            (set! var2 43)
            (set! var3 44)
            (+ var1 var2 var3)
            (catch Exception e
              e))))
#'user/do-stuff
user> (do-stuff)
129

我从来没有见过像这样的代码用于任何实际问题(我一直在写Clojure作为我的日常工作,只要有人),这些事情基本上都没有出现过。我在这里打气,因为我不希望人们在Clojure中留下你“无法写”的东西。

重要的是要强调此代码具有clojure其余部分所具有的所有并发安全性。因为绑定会创建遵循此代码的线程本地值,同时隔离var1var2等其他用户不受影响。

一个更合理的方法是使用future(延迟创建这些)来定义要在try-catch外部运行的计算,而不实际运行它

然后代码进入try catch块并对延迟代码进行derefrence,使其实际运行。

user> (defn do-stuff []
        (let [var1 (delay (inc 41))
              var2 (delay (dec 43))
              var3 (delay (/ 42 0))]
          (try
            (+ @var1 @var2 @var3)
            (catch Exception e
              (println "cleaning up: " var1 var2 var3)))))
#'user/do-stuff
user> (do-stuff)
cleaning up:  #object[clojure.lang.Delay 0x3c55d284 {:status :ready, :val 42}]
              #object[clojure.lang.Delay 0x7bfa6dd1 {:status :ready, :val 42}]
              #object[clojure.lang.Delay     0x5e9e285b {:status :failed, :val #error {
              :cause Divide by zero

这假设您可以更改清理代码以引用可能包含要清除的值或导致错误的异常的内容。 delay具有有用的属性,可以在main函数中抛出异常,然后在错误处理程序中再次抛出 。思考这个例子:

user> (def x (delay (/ 2 0)))
#'user/x
user> @x
ArithmeticException Divide by zero  clojure.lang.Numbers.divide (Numbers.java:158)
user> @x
ArithmeticException Divide by zero  clojure.lang.Numbers.divide (Numbers.java:158)

你应该通过将你的功能分成两个来解决这个问题,而不是在dyanamic vars上使用set!。这是可能的。

答案 1 :(得分:2)

参考值/原子

不使用动态变量,您也可以将数据放入框中:

(let [v1 (ref nil)
      v2 (ref nil)
      v3 (ref nil)]
    (try
      (dosync
       (ref-set v1 (f1))
       (ref-set v2 (f2))
       (ref-set v3 (f3))
       (do-something @v1 @v2 @v3))
      (catch Exception e
        (cleanup-resources @v1 @v2 @v3))))

最后

如果您致力于采用纯粹不可改变的方式,那么您应该写下:

(let [v1 (func1)]
  (try
    (let [v2 (func2)]
      (try
        (let [v3 (func3)]
          (try
            (do
              (try
                (do-something v1 v2 v3)
                (catch Exception e <error-handling>)))
            (finally (cleanup-3 v3))))
        (finally (cleanup-2 v2))))
    (finally (cleanup-1 v1))))

你可以看到cleanup-resources分为三个函数,我认为这些函数在你的Java代码中也是有意义的。 以上是使用宏生成的:

(defmacro with-cleanups [bindings & body]
  (reduce
   (fn [form [v value cleanup]]
     `(let [~v ~value]
        (try ~form
             (finally ~cleanup))))
   `(do ~@body)
   (reverse (partition 3 bindings))))

以下是您需要编写的实际代码:

(with-cleanups [v1 (func1) (cleanup-1 v1)
                v2 (func2) (cleanup-2 v2)
                v3 (func3) (cleanup-3 v3)]
  (try
    (do-something v1 v2 v3)
    (catch Exception e <error-handling>)))

如果您愿意,可以让绑定的cleanup部分成为函数而不是表达式:

(with-cleanups [v1 (func1) cleanup-1
                v2 (func2) cleanup-2
                v3 (func3) cleanup-3]
  (try
    (do-something v1 v2 v3)
    (catch Exception e <error-handling>)))

答案 2 :(得分:2)

您可以用Java和Clojure以相同的方式编写这种代码。对于可能导致异常的每个函数都将该异常作为返回值的一部分。在Java中,您将返回一个类的实例,该类具有例如curl_setopt($ch, CURLOPT_COOKIEFILE, dirname(__FILE__).'/cookie.txt'); curl_setopt($ch, CURLOPT_COOKIEJAR, dirname(__FILE__).'/cookie.txt'); 方法。在Clojure中,您可以返回哈希映射或可能返回元组2向量。哈希映射可以包含例外:getErrors()您需要对所有4个函数执行此操作:{:exception nil}和其他三个函数。

在Clojure中,您可以对结果进行映射/过滤。例外情况可以在他们需要的时候处理(本地,或者在你的情况下集体(但仍然是本地))并且不会在整个代码库中扩散。

答案 3 :(得分:2)

更新:重构宏以获取代码块而不是函数

像@coredump一样,我也会为此提出一个宏,但它更接近于op的变体(他在一个块中清理所有资源):

(defmacro with-resources [[var expr & other :as resources]
                          body cleanup-block
                          [error-name error-block :as error-handler]]
  (if (empty? resources)
    `(try ~body
          (catch Throwable e#
            (let [~error-name e#] ~error-block))
          (finally ~cleanup-block))
    `(try
       (let ~[var expr]
         (with-resources ~other ~body ~cleanup-block ~error-handler))
       (catch Throwable e#
         (let ~(vec (interleave (take-nth 2 resources)
                                (repeat nil)))
           ~cleanup-block
           (let [~error-name e#] ~error-block))))))

这是它的工作原理: 它初始化资源,如果从初始化程序或正文中抛出某些内容,如果执行cleanup-block包含所有资源(或nil s表示未初始化的资源),error-block则绑定到{ {1}},并返回其值。它没有任何结果,只需调用error-name并返回cleanup-block的值。

的示例:

body