"Wrong number of args" ArityExceptions are misleading
camsaul opened this issue · 6 comments
If you call a defmethod
with the wrong number of args you'll get a misleading arity exception that reports you've passed one more are than you actually have:
(m/defmulti my-multimethod
(fn [x y]
[(keyword x) (keyword y)]))
(m/defmethod my-multimethod [:x :y]
[_])
(my-multimethod :x :y)
;; ->
;; Execution error (ArityException)
;; Wrong number of args (3) passed to: my-multimethod-primary-method-x-y
This is of course because the defmethod
actually macroexpands to
(do
(defn my-multimethod-primary-method-x-y [next-method _])
(methodical.util/add-primary-method!
#'my-multimethod
[:x :y]
my-multimethod-primary-method-x-y))
and gets the extra next-method
parameter...
instead of macroexpanding to
(defn my-multimethod-primary-method-x-y
[next-method _]
...)
we could do something like
(defn my-multimethod-primary-method-x-y
[next-method a#]
((fn [_]
...) a#))
so we get correct error messages. I don't think this would make a noticeable performance difference but probably worth benching just to be sure.
Alternatively just write reify a version of IFn
that gives meaningful error messages. Maybe it could even include stuff like the dispatch value
Failing test (methodical.macros-test
):
(m/defmulti mf5
{:arglists '([x y])}
(fn [x & _]
(keyword x)))
(m/defmethod mf5 :default
[x y]
{x y})
(m/defmethod mf5 :after :default
[x m]
(assoc m :after x))
(t/deftest arity-exception-test
(t/testing "'Wrong number of args' errors should not include `next-method` in the number of args passed (#54)"
(t/testing "Sanity check"
(t/is (= {:x 1, :after :x}
(mf5 :x 1))))
(t/is (thrown-with-msg?
clojure.lang.ArityException
#"Wrong number of args \(1\) passed to: methodical\.macros-test/mf5-primary-method-default"
(mf5 :x)))
(t/is (thrown-with-msg?
clojure.lang.ArityException
#"Wrong number of args \(1\) passed to: methodical\.macros-test/mf5-after-method-bad-after"
(mf5 ::bad-after 1)))))
I poked around at fixing this a bit but I think what we need to is either:
-
implement a custom function type that throws the correct error when the wrong arity is invoked
-
pass
next-method
on its own and return a function e.g. instead of generating functions like(defn mf5-primary-method-default [next-method x y] (+ x y))
we could generate them like
(defn mf5-primary-method-default* [next-method] (fn [x y] (+ x y)))
This seems a little annoying and we'd have to tweak all the method combinations
Messages are also pretty bad if you add methods programmatically e.g. with add-primary-method
. Maybe add-primary-method
can put metadata on the primary method function (if applicable) that is used in the error
Dispatch function arity errors aren't nice either =(
(m/defmulti x
(fn [x y]
[(keyword x) (keyword y)]))
(m/defmethod x [:x :y]
[x y]
{:x x, :y y})
(x :a)
;; => Wrong number of args (1) passed to: methodical.impl.combo-test/eval34222/fn--34224
We could wrap the dispatch function in a try-catch
and throw a better exception but I'm not sure what the performance impact of that would be.
I think the only reasonable way of implementing this would be to automatically wrap the primary method function in something that does a try-catch and catches clojure.lang.ArityException
and throws a better one instead... not sure what performance impact that would have tho.
If the choice is between better errors or slightly better performance, I think we should choose better errors
Wrapping things doesn't actually have a dramatic effect. A third of a nanosecond on my computer
Here's an example of how we might accomplish wrapping things
(defn wrap-dispatch-fn [f ^String fn-description ^Integer num-implicit-args]
(letfn [(rethrow-arity-exception [^clojure.lang.ArityException e]
(throw (doto (clojure.lang.ArityException.
(- (.actual e) num-implicit-args)
fn-description
(.getCause e))
(.setStackTrace (.getStackTrace e)))))]
(fn
([]
(try
(f)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a]
(try
(f a)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a b]
(try
(f a b)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a b c]
(try
(f a b c)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a b c d]
(try
(f a b c d)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a b c d e]
(try
(f a b c d e)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e))))
([a b c d e & more]
(try
(apply f a b c d e more)
(catch clojure.lang.ArityException e
(rethrow-arity-exception e)))))))
(ns-unmap *ns* 'x)
(m/defmulti x
(let [dispatch-fn (fn [x y]
[(keyword x) (keyword y)])]
(wrap-dispatch-fn dispatch-fn
(format "Methodical multimethod %s dispatch function %s"
(pr-str `x)
(some-> dispatch-fn class .getCanonicalName))
0)))
(m/defmethod x [:x :y]
[x y]
{:x x, :y y})
(x :a)
;; => Wrong number of args (1) passed to: Methodical multimethod methodical.impl.combo-test/x dispatch function methodical.impl.combo-test/eval36766/dispatch-fn--36768