3
votes

Problem

I am trying to define a macro using Clojure that works similarly to try in java It should be able to have a binding form, for example [variable value], that can bind to an instance of a closable.

Example in Java

try (Socket s = new Socket()) {
s.accept();
} catch(Exception e) {}

In this code, the socket 's' is automatically closed as if there is explicitly a finally clause

finally {
if (s != null) s.close();
}

Attempted solution

;I am defining my macro. It can either take one argument(expression) or can take two arguments(expression and a vector with two elements(a variable and value)
;expression is a form expression and should be able to be evaluated
(defmacro safe 
    ; if a vector and also an expression is passed into the macro
    [s NewS expression]
    ;I am defining my try block, and unquoting it with a ' so that the try block is not executed within the macro
    `(try   
        ;I am letting the variable(NewS) be equal to the value(s)
        (let [s NewS]
        ;I am trying the expression to see if it is valid and printing the exception if there is one
        (try 
            ~expression (catch Exception e (str "caught exception: " (.getMessage e))))
        )
        ;I am checking if my value is an instance of a java closeable
        (instance? java.util.Closeable s)
        ;I am catching the exception from the let statement if there is one
        (catch Exception e (str "caught exception: " (.getMessage e)))
    )
    ;if only an expression is passed into the macro
    [expression]
    `(try
        ~expression (catch Exception e (str "caught exception: " (.getMessage e)))
    )
)

Example input and output

user> (def v (safe (/ 1 0)))
user> v
#<ArithmeticException java.lang.ArithmeticException: Divide by zero>
user> (def v (safe (/ 10 2)))
user> v
5
user> (def v (safe [s (FileReader. (File. "file.txt"))] (.read s)))
user> v
105 ; first byte of file file.txt
user> (def v (safe [s (FileReader. (File. "missing-file"))] (. s read)))
user> v
#<FileNotFoundException java.io.FileNotFoundException:
missing-file (No such file or directory)>

Error message

When I put these example inputs into my main function I get a compiler excpetion that I don't understand.

CompilerException clojure.lang.ArityException: Wrong number of args (1) passed to: core/safe, compiling:(/private/var/folders/6f/q7lhngtn45q_xpzd_24gjp2h0000gn/T/form-init2350735096437822603.clj:1:8) 

I don't know what I can adjust in this macro but I cannot get it to not return errors.

Update

This solution almost works

(defmacro safe
    ([[s NewS] expression]
    `(try  
        (let [~s ~NewS] ~expression) (catch Exception e# (str "caught exception: " (.getMessage e#)))
    ))
    ([expression]
    `(try
        ~expression (catch Exception e# (str "caught exception: " (.getMessage e#)))
    ))
)

But the following test fails

(defn -main "I don't do a whole lot ... yet." [& args] (import java.io.FileReader java.io.File) (def v (safe [s (FileReader. (File. "file.txt"))] (.read s))) (println v) )

user$ lein run
caught exception: file.txt (No such file or directory)
user$ cat file.txt 
teast
1
I think you could use with-open for this.Taylor Wood
I don't really understand what my main flaws are. Even when I use with open I get similar errors (defmacro safe [& [s newS] expression] (if s (let [~s ~newS])) (with-open ~newS & ~expression) ) (defn -main [& args] (def v (safe (/ 1 0))) ) Sam
Have yoy checked what macroexpand gives for your macro? You have the code formatted poorly here, so it's hard to read.Carcigenicate
Also, Clojure uses semicolons for comments, not plain hashes.Carcigenicate
Also, think about what s and newS are for your divide by 0 example.Carcigenicate

1 Answers

2
votes

The auto-closing behavior is already solved by with-open macro. See the implementation here. Here's a formulation that (I think) does what you ask.

(defmacro safe
  ([body]
   `(try ~body
      (catch Exception e#
        (str "caught exception: " e#))))
  ([bindings & body]
   `(try
      (with-open ~bindings
        ~@body)
      (catch Exception e#
        (str "caught exception: " e#)))))

Example usage:

(safe (/ 1 nil))
;;=> "caught exception: java.lang.NullPointerException"

(safe [s (clojure.java.io/reader "file.txt")]
  (prn (.read s)))
;;=> "caught exception: java.io.FileNotFoundException: file.txt (No such file or directory)"

(spit "file.txt" "contents here")
(safe [s (clojure.java.io/reader "file.txt")]
  (.read s))
;;=> 99

However

  1. Returning a string on failure is confusing because what if the expected value is also a string? Then how do you know if your evaluation succeeded or failed? Maybe you just want to print/log the exception and return nil? (In that case consider replacing str with println in your catch block.)
  2. Having two arities of safe (one which takes only one body form and another that takes binding(s) to be closed and body forms) entangles concerns that should arguably be separated. with-open already exists and we shouldn't reinvent it when we can reuse it.

This version of safe takes any number of forms instead of only one, so it's now more flexible.

(defmacro safe
  [& body]
  `(try ~@body
     (catch Exception e#
       (println "caught exception:" (.getMessage e#)))))

And we can easily use with-open within safe to get your desired behavior:

(safe
 (prn (/ 1 2)) ;; prints 1/2
 (with-open [s (clojure.java.io/reader "not_a_file.txt")]
   (.read s))) ;; fails, prints exception

(safe
 (with-open [s (clojure.java.io/reader "file.txt")]
   (char (.read s)))) ;; returns the first char from file we `spit` above
;;=> \c