1

I'm working through Modern ClojureScript tutorial, and trying to write some macros to automate some things.

For instance, in one of validate functions:

(defn validate-shopping-form [quantity price tax discount]
  (validate {:quantity quantity :price price :tax tax :discount discount}

            ;; validate presence

            [:quantity present? "Quantity can't be empty"]
            [:price present? "Price can't be empty"]
            [:tax present? "Tax can't be empty"]
            [:discount present? "Discount can't be empty"]

            ;; validate type

            [:quantity integer-string? "Quantity has to be an integer number"]
            [:price decimal-string? "Price has to be a number"]
            [:tax decimal-string? "Tax has to be a number"]
            [:discount decimal-string? "Discount has to be a number"]

            ;; validate range

            [:quantity (gt 0) "Quantity can't be negative"]

            ;; other specific platform validations (not at the moment)

            ))  

I've wrote some macros, but my function does not compile. And i don't understand why

(ns try-cljs.shopping.validators
  (:require [valip.core :refer [validate]]
            [valip.predicates :refer [present?
                                      integer-string?
                                      decimal-string?
                                      gt]]))

(defmacro empty-checks []
  `(list ~@(map (fn [x] [(keyword x) 'present? (str x " can't be empty")])
                ["quantity" "price" "tax" "discount"])))

(defmacro number-checks []
  `(list ~@(mapv (fn [x] [(keyword x) 'decimal-string? (str x " should be a number")])
                 ["price" "tax" "discount"])))

(defmacro quantity-checks []
 `(list [:quantity integer-string? "quantity should be an integer"]
        [:quantity (gt 0) "quantity should be positive"]))

(defmacro checks [] `(list ~@(empty-checks)
                           ~@(number-checks)
                           ~@(quantity-checks)))

(defn validate-shopping-form [quantity price tax discount]
  (apply validate {:quantity quantity :price price :tax tax :discount discount}
                  (checks)))

The error is: java.lang.IllegalArgumentException: No matching ctor found for class valip.predicates$gt$fn__2332

And it is about (gt 0) form. But what does this mean and how can i fix the macros?

By the way. In the REPL expression

(apply validate {:quantity "10"} (concat (quantity-checks)))

works fine

leeor
  • 17,041
  • 6
  • 34
  • 60
  • what version of valip are you using? is this a .cljc file? – nberger Jan 20 '16 at 13:54
  • @nberger the version is 0.4.0-SNAPSHOT, ant the file is .cljc – M.Bakhterev Jan 20 '16 at 13:57
  • 1
    Your checks macro evals the other macros in macro-expansion time. That's what the `~` in `checks` does. That's causing the error, because evaling `quantity-checks` makes `(gt 0)` to be evaled and that's your valip.predicates$gt$fn__2332. But you don't want that, you want to output `(gt 0)` to be eval'ed in runtime. I think you can fix it with something like `(concat (empty-checks) (number-checks) (quantity-checks))` instead of `(list ~@(empty-checks) ...)` – nberger Jan 20 '16 at 14:23
  • Or maybe it's just removing the three `~` in the checks macro – nberger Jan 20 '16 at 14:25
  • I've found some solution [gist](https://gist.github.com/mbakhterev/239ed1d89541025ca155). But it's quite ugly because of runtime `concat` call. But i still don't understand why the original code doesn't work. – M.Bakhterev Jan 20 '16 at 20:16

1 Answers1

1

First thing to note is that you don't really need any macros for this. I assume that you want to use macros to learn about macros.

To try to explain the issue I am going to use a simplified version of your check function:

(defmacro checks [] `(list ~@(quantity-checks)))

When having issues with a macro, you need to compare what is the code that you want to generate vs what is the code that your macro is actually generating.

What you want to generate:

(clojure.core/list
 [:quantity valip.predicates/integer-string? "quantity should be an integer"]
 [:quantity (valip.predicates/gt 0) "quantity should be positive"])

To see what you are actually generating use (clojure.pprint/pprint (macroexpand '(checks))):

(clojure.core/list
 [:quantity #object[valip.predicates$integer_string_QMARK_ 0x13272c8f "valip.predicates$integer_string_QMARK_@13272c8f"] "quantity should be an integer"]
 [:quantity #object[valip.predicates$gt$fn__25100 0x17fe6151 "valip.predicates$gt$fn__25100@17fe6151"] "quantity should be positive"])

That #object[valip.predicates$gt$fn__25100... gibberish is the toString representation of fn returned by (gt 0).

So your macro is executing (gt 0) instead of returning the code that represents the call to (gt 0). This is because quantity-check is a macro, so it is evaluated before compiling the checks macro. See this question for a longer explanation.

To avoid the macro executing the function, you just need to quote the expression:

(defmacro quantity-checks []
  `(list [:quantity 'integer-string? "quantity should be an integer"]
         [:quantity '(gt 0) "quantity should be positive"]))

If you run macroexpand with this version, you will see that produces the same code as the one that we want.

Note that quantity-checks could be a function like:

(defn quantity-checks []
 `([:quantity integer-string? "quantity should be an integer"]
   [:quantity (gt 0) "quantity should be positive"]))

Back-quoting is not exclusive of macros.

Community
  • 1
  • 1
DanLebrero
  • 8,545
  • 1
  • 29
  • 30
  • could you clarify one more missunderstanding of mine, please? I've rewrote my validation code as here [gist](https://gist.github.com/mbakhterev/86c4ac21ced119a5e90c#file-my-validate-cljc). And it works as expected in Clojure, but ClojureScript gives error: `Don't know how to create ISeq from: clojure.lang.Symbol at file src/cljc/try_cljs/shopping/validators.cljc, line 30, column 32`. What is the problem? – M.Bakhterev Jan 23 '16 at 21:44
  • what does Clojure println? – DanLebrero Jan 23 '16 at 22:14
  • it just prints stuff with newline at the end. These lines with `println` can be commented out, but the error reemerges at line 31. – M.Bakhterev Jan 24 '16 at 10:09