Should I use a function or a macro to validate arguments in Clojure?
Asked Answered
R

3

12

I have a group of numeric functions in Clojure that I want to validate the arguments for. There are numerous types of arguments expected by the functions, such as positive integers, percentages, sequences of numbers, sequences of non-zero numbers, and so on. I can validate the arguments to any individual function by:

  1. Writing validation code right into the function.
  2. Writing a general purpose function, passing it the arguments and expected types.
  3. Writing a general purpose macro, passing it the arguments and expected types.
  4. Others I haven't thought of.

Some Lisp code by Larry Hunter is a nice example of #3. (Look for the test-variables macro.)

My intuition is that a macro is more appropriate because of the control over evaluation and the potential to do compile-time computation rather than doing it all at run time. But, I haven't run into a use case for the code I'm writing that seems to require it. I'm wondering if it is worth the effort to write such a macro.

Any suggestions?

Rainarainah answered 28/10, 2009 at 21:42 Comment(0)
D
13

Clojure already has (undocumented, maybe subject-to-change) support for pre- and post-conditions on fns.

user> (defn divide [x y]
        {:pre [(not= y 0)]}
        (/ x y))
user> (divide 1 0)
Assert failed: (not= y 0)
   [Thrown class java.lang.Exception]

Kind of ugly though.

I'd probably write a macro just so I could report which tests failed in a succinct way (quote and print the test literally). The CL code you linked to looks pretty nasty with that enormous case statement. Multimethods would be better here in my opinion. You can throw something like this together pretty easily yourself.

(defmacro assert* [val test]
  `(let [result# ~test]              ;; SO`s syntax-highlighting is terrible
     (when (not result#)
       (throw (Exception.
               (str "Test failed: " (quote ~test)
                    " for " (quote ~val) " = " ~val))))))

(defmulti validate* (fn [val test] test))

(defmethod validate* :non-zero [x _]
  (assert* x (not= x 0)))

(defmethod validate* :even [x _]
  (assert* x (even? x)))

(defn validate [& tests]
  (doseq [test tests] (apply validate* test)))

(defn divide [x y]
  (validate [y :non-zero] [x :even])
  (/ x y))

Then:

user> (divide 1 0)
; Evaluation aborted.
; Test failed: (not= x 0) for x = 0
;   [Thrown class java.lang.Exception]

user> (divide 5 1)
; Evaluation aborted.
; Test failed: (even? x) for x = 5
;   [Thrown class java.lang.Exception]

user> (divide 6 2)
3
Dinger answered 28/10, 2009 at 23:15 Comment(2)
Thanks for another great answer. This does pretty much exactly what I wanted to do. It also illustrates one of the reasons that a macro is a better solution. Thinking about how to disambiguate the problem argument if a function required two or more with the same characteristics, say two positive integers, the macro route lets you put the name of the problem argument in the error message, not just the value. I don't think you could do that with a function.Rainarainah
Preconditions and postconditions are documented right here clojure.org/special_forms Search for condition map.Digitiform
R
3

Just a few thoughts.

I have a feeling it depends on the complexity and number of validations, and the nature of the functions.

If you are doing very complex validations, you should break your validators out of your functions. The reasoning is that you can use simpler ones to build up more complex ones.

For example, you write:

  1. a validator to make sure a list is not empty,
  2. a validator to make sure a value is greater than zero,
  3. use 1 and 2 to make sure a value is a non empty list of values greater than zero.

If you're just doing a huge amount of simple validations, and your issue is verbosity, (e.g. you have 50 functions that all require non-zero integers), then a macro probably makes more sense.

Another thing to consider is that function evaluation is Clojure is eager. You could get a performance boost in some cases by not evaluating some parameters if you know the function will fail, or if some parameters are not needed based on values of other parameters. E.g. the every? predicate doesn't need to evaluate every value in a collection.

Finally, to address "others you haven't thought of". Clojure supports a generic dispatching pbased on a dispatch functon. That function could dispatch to appropriate code, or error messages based on any number of factors.

Rout answered 28/10, 2009 at 22:56 Comment(0)
A
1

A case where you need a macro would be if you wanted to modify the language to automatically add the tests to any function defined within a block, like this:

(with-function-validators [test1 test2 test4]  
    (defn fun1 [arg1 arg2] 
        (do-stuff))
    (defn fun2 [arg1 arg2] 
        (do-stuff))
    (defn fun3 [arg1 arg2] 
        (do-stuff)))  
Anamorphism answered 28/10, 2009 at 23:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.