As described in the defmethod doc, they can be given a name. What is very useful in stack traces.
(defmethod foo "a" name-of-method [params] "was a")
When replacing name-of-method by a string. I get the following error:
:cause "Call to clojure.core/fn did not conform to spec."
:data #:clojure.spec.alpha{:problems ({:path [:fn-tail :arity-1 :params], :pred clo
jure.core/vector?, :val "A documentation", :via [:clojure.core.specs.alpha/params+bo
dy :clojure.core.specs.alpha/param-list :clojure.core.specs.alpha/param-list], :in [
0]} {:path [:fn-tail :arity-n], :pred (clojure.core/fn [%] (clojure.core/or (clojure
.core/nil? %) (clojure.core/sequential? %))), :val "A documentation", :via [:clojure
.core.specs.alpha/params+body :clojure.core.specs.alpha/params+body], :in [0]}), :sp
ec #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x24de08c1 "clojure.spec.a
lpha$regex_spec_impl$reify__2509#24de08c1"]
So what's the propper way to document a defmethod?
You can document the defmethod function using a symbol like the following:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(defmulti ident
(fn [item] (type item)) )
; vvvvvv-function-name
(defmethod ident clojure.lang.Keyword ident-fn-kw [item]
"to my heart")
(defmethod ident java.lang.String ident-fn-str [item]
"like cheese")
(defmethod ident clojure.lang.Symbol ident-fn-sym [item]
"crash, bang"
;(throw (ex-info "surprise!" {:reason :dont-need-one}))
)
(dotest
(is= clojure.lang.Keyword (spyx (type :hello)))
(is= java.lang.String (spyx (type "hello")))
(is= clojure.lang.Symbol (spyx (type 'hello)))
(is= "to my heart" (ident :hello))
(is= "like cheese" (ident "hello"))
(is= "crash, bang" (ident 'hello))
)
with result:
----------------------------------
Clojure 1.10.1 Java 13-ea
----------------------------------
Testing tst.demo.core
(type :hello) => clojure.lang.Keyword
(type "hello") => java.lang.String
(type (quote hello)) => clojure.lang.Symbol
Ran 2 tests containing 6 assertions.
0 failures, 0 errors.
and if you uncomment the throw you will get the symbol listed in any Exception stacktrace like this:
ERROR in (dotest-line-19) (core.clj:16)
expected: (clojure.core/= "crash, bang" (ident (quote hello)))
actual: clojure.lang.ExceptionInfo: surprise!
{:reason :dont-need-one}
at tst.demo.core$eval23035$ident_fn_sym__23036.doInvoke (core.clj:16)
clojure.lang.RestFn.invoke (RestFn.java:408)
clojure.lang.MultiFn.invoke (MultiFn.java:229)
tst.demo.core$fn__23039.invokeStatic (core.clj:26)
tst.demo.core/fn (core.clj:19)
Notice that the "function name" is included in the stacktrace as tst.demo.core$eval23035$ident_fn_sym__23036. As with any "anonymous" function, this can be of great use in tracking down the cause of an exception. For example:
(dotest
(let [add2 (fn add2-fn [a b] (+ a b))]
(is= 5 (add2 2 3))))
Here we use add2-fn to name the function as a debugging aid in case of an exception. I like to use a -fn suffix as it helps speed grep-based searching of source code.
Update:
Here is the info from clojuredocs.org
;; Methods can be given a name. Very useful in stack traces.
(defmethod foo "a" name-of-method [params] "was a")
So name-of-method is a symbol, not a docstring.
P.S. Note that, to the user, the fact that a function is implemented as a multi-method (vs a regular function with a cond or if or something else), is really just a implementation detail. The contract of the function itself (name, args, purpose, etc) is defined in the defmulti form, and that is where the docstring belongs.
Related
clojure 1.9.0
A simple test of sort-by on the character array works below,
user=> (sort-by identity [[\B] [\a]])
([\B] [\a])
but why did another test fail to sort-by case-insensitively?
user=> (sort-by (partial map #(Character/toLowerCase %)) [[\B] [\a]])
java.lang.ClassCastException: clojure.lang.LazySeq cannot be cast to java.lang.Comparable
Solution
Using mapv instead of map makes it.
user=> (instance? clojure.lang.LazySeq (map identity []))
true
user=> (instance? clojure.lang.PersistentVector (mapv identity (map identity [])))
true
You don't need map:
(ns tst.demo.core
(:require
[clojure.string :as str] ))
(sort-by #(.toLowerCase (str/join %)) [[\a \b] [\B] [\a]])
;=> ([\a] [\a \b] [\B])
but why did another test fail to sort-by case-insensitively?
map returns a lazy sequence, which doesn't implement Comparable. mapv works because vectors support Comparable and that's what sort-by is using to sort.
(supers (type []))
=> #{... java.lang.Comparable ...}
(supers clojure.lang.LazySeq)
=> #{clojure.lang.IObj clojure.lang.ISeq clojure.lang.Seqable clojure.lang.IMeta java.lang.Iterable java.util.List clojure.lang.IHashEq java.lang.Object clojure.lang.Obj java.util.Collection clojure.lang.IPending clojure.lang.Sequential clojure.lang.IPersistentCollection java.io.Serializable}
Is there anyway to include clojure.spec'd functions in a generalized test suite? I know we can register specs and directly spec functions.
(ns foo
(:require [clojure.spec :as s]
[clojure.spec.test :as stest]))
(defn average [list-sum list-count]
(/ list-sum list-count))
(s/fdef average
:args (s/and (s/cat :list-sum float? :list-count integer?)
#(not (zero? (:list-count %))))
:ret number?)
And later, if I want to run generative tests against that spec'd function, I can use stest/check.
=> (stest/check `average)
({:spec #object[clojure.spec$fspec_impl$reify__14282 0x68e9f37c "clojure.spec$fspec_impl$reify__14282#68e9f37c"], :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1479587517232}, :sym edgar.core.analysis.lagging/average})
But i) is there anyway to include these test runs in my general test suite? I'm thinking of the kind of clojure.test integration that test.check has. The closest thing that I can see ii) is the stest/instrument (see here) function. But that seems to just let us turn on checking at the repl. Not quite what I want. Also, iii) are function specs registered?
(defspec foo-test
100
;; NOT this
#_(prop/for-all [v ...]
(= v ...))
;; but THIS
(stest/some-unknown-spec-fn foo))
Ok, solved this one. Turns out there's no solution out of the box. But some people on the clojure-spec slack channel have put together a defspec-test solution for clojure.spec.test and clojure.test.
So given the code in the question. You can A) define the defspec-test macro that takes your test name and a list of spec'd functions. You can then B) use it in your test suite.
Thanks Clojure community!! And hopefully such a utility function makes it into the core library.
A)
(ns foo.test
(:require [clojure.test :as t]
[clojure.string :as str]))
(defmacro defspec-test
([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
([name sym-or-syms opts]
(when t/*load-tests*
`(def ~(vary-meta name assoc
:test `(fn []
(let [check-results# (clojure.spec.test/check ~sym-or-syms ~opts)
checks-passed?# (every? nil? (map :failure check-results#))]
(if checks-passed?#
(t/do-report {:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym check-results#)))})
(doseq [failed-check# (filter :failure check-results#)
:let [r# (clojure.spec.test/abbrev-result failed-check#)
failure# (:failure r#)]]
(t/do-report
{:type :fail
:message (with-out-str (clojure.spec/explain-out failure#))
:expected (->> r# :spec rest (apply hash-map) :ret)
:actual (if (instance? Throwable failure#)
failure#
(:clojure.spec.test/val failure#))})))
checks-passed?#)))
(fn [] (t/test-var (var ~name)))))))
B)
(ns foo-test
(:require [foo.test :refer [defspec-test]]
[foo]))
(defspec-test test-average [foo/average])
The above example can fail in the case where :failure is false due to how stest/abbrev-result tests for failure. See CLJ-2246 for more details. You can work around this by defining your own version of abbrev-result. Also, the formatting of failure data has changed.
(require
'[clojure.string :as str]
'[clojure.test :as test]
'[clojure.spec.alpha :as s]
'[clojure.spec.test.alpha :as stest])
;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))
(defn failure? [{:keys [:failure]}] (not (or (true? failure) (nil? failure))))
;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
(let [failure (:failure x)]
(if (failure? x)
(-> (dissoc x ::stc/ret)
(update :spec s/describe)
(update :failure unwrap-failure))
(dissoc x :spec ::stc/ret))))
(defn throwable? [x]
(instance? Throwable x))
(defn failure-report [failure]
(let [expected (->> (abbrev-result failure) :spec rest (apply hash-map) :ret)]
(if (throwable? failure)
{:type :error
:message "Exception thrown in check"
:expected expected
:actual failure}
(let [data (ex-data (get-in failure
[::stc/ret
:result-data
:clojure.test.check.properties/error]))]
{:type :fail
:message (with-out-str (s/explain-out data))
:expected expected
:actual (::s/value data)}))))
(defn check?
[msg [_ body :as form]]
`(let [results# ~body
failures# (filter failure? results#)]
(if (empty? failures#)
[{:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym results#)))}]
(map failure-report failures#))))
(defmethod test/assert-expr 'check?
[msg form]
`(dorun (map test/do-report ~(check? msg form))))
Here's a slightly modified version of grzm's excellent answer that works with [org.clojure/test.check "0.10.0-alpha4"]. It uses the new :pass? key that comes from this PR: https://github.com/clojure/test.check/commit/09927b64a60c8bfbffe2e4a88d76ee4046eef1bc#diff-5eb045ad9cf20dd057f8344a877abd89R1184.
(:require [clojure.test :as t]
[clojure.string :as str]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as stest])
(alias 'stc 'clojure.spec.test.check)
;; extracted from clojure.spec.test.alpha
(defn failure-type [x] (::s/failure (ex-data x)))
(defn unwrap-failure [x] (if (failure-type x) (ex-data x) x))
;; modified from clojure.spec.test.alpha
(defn abbrev-result [x]
(if (-> x :stc/ret :pass?)
(dissoc x :spec ::stc/ret)
(-> (dissoc x ::stc/ret)
(update :spec s/describe)
(update :failure unwrap-failure))))
(defn throwable? [x]
(instance? Throwable x))
(defn failure-report [failure]
(let [abbrev (abbrev-result failure)
expected (->> abbrev :spec rest (apply hash-map) :ret)
reason (:failure abbrev)]
(if (throwable? reason)
{:type :error
:message "Exception thrown in check"
:expected expected
:actual reason}
(let [data (ex-data (get-in failure
[::stc/ret
:shrunk
:result-data
:clojure.test.check.properties/error]))]
{:type :fail
:message (with-out-str (s/explain-out data))
:expected expected
:actual (::s/value data)}))))
(defn check?
[msg [_ body :as form]]
`(let [results# ~body
failures# (remove (comp :pass? ::stc/ret) results#)]
(if (empty? failures#)
[{:type :pass
:message (str "Generative tests pass for "
(str/join ", " (map :sym results#)))}]
(map failure-report failures#))))
(defmethod t/assert-expr 'check?
[msg form]
`(dorun (map t/do-report ~(check? msg form))))
Usage:
(deftest whatever-test
(is (check? (stest/check `whatever
;; optional
{:clojure.spec.test.check/opts {:num-tests 10000}})))
Is there a way to retrieve the metadata of the arguments inside a clojure macro without using eval? The only thing I could come up with so far is this:
(def ^{:a :b} my-var)
(defmacro my-macro [s] (prn (eval `(meta (var ~s)))))
(my-macro my-var)
;; Prints {:a :b, :name my-var, ...}
I ended up finding a solution:
(def ^{:a :b} my-var)
(defmacro my-macro [s] (prn (meta (resolve s))))
(my-macro my-var)
;; Prints {:a :b, :name my-var, ...}
So the key part here is to use resolve function to get the var associated to the symbol.
I would like to be able to define an anonymous function in my Lieningen project using Environ.
Here is what that part of the project file looks like:
{:env {:foo (fn [s]
(count s))}}
Then in my code, I would like to use that function. Something like:
(-> "here are a few words"
(env :foo))
And then to get the size of s.
Environ will simply invoke read-string on the slurped file. The value at :foo will be a list containing the symbol fn followed by a vector with the symbol s inside and so on. i.e. the form has not been evaluated and so you will not be able to invoke the anonymous fn.
see environ.core/read-env-file
Consider this:
(def f (read-string "(fn [s] (count s))"))
(f "test")
;; => ClassCastException clojure.lang.PersistentList cannot be cast to clojure.lang.IFn
(def evaled-f (eval (read-string "(fn [s] (count s))")))
(evaled-f "test")
;; => 4
Also, your intended use is a little off. -> is a macro that will take the first arg and "thread" it into the first position of the following form.
(macroexpand '(-> "here are a few words" (env :foo)))
;; => (env "here are a few words" :foo)
I think you're looking for something like:
(let [f (eval (env :foo))]
(-> "here are a few words"
f))
(env :foo) returns list. To make function from it you can use eval or better macro like this:
(defmacro defn-env [fn-name env-key]
`(def ~fn-name ~(env env-key)))
(defn-env env-fn :foo) ; => #'user/env-fn
(env-fn [1 2 3]) ; => 3
Note: if (env :foo) returns nil you need to add :plugins [[lein-environ "1.0.0"]] to your project.clj.
I'm having a hard time getting the multimethods in Clojure to work as I would expect. A distillation of my code is as follows.
(defn commandType [_ command] (:command-type command))
(defmulti testMulti commandType)
(defmethod testMulti :one [game command] (str "blah"))
(defmethod testMulti :default [& args] "Cannot understand")
(testMulti "something" {:command-type :one})
(commandType "something" {:command-type :one})
Now I would expect here to have the method commandType called on the arguments which would of course return :one which should send it to the first defmethod but instead I get a null pointer exception. Even the simplest invocation of a multimethod I could come up with gives me a null pointer:
(defmulti simpleMulti :key)
(defmethod simpleMulti "basic" [params] "basic value")
(simpleMulti {:key "basic"})
And yet the example in the clojure docs located here works fine. Is there something fundamental I'm doing wrong?
So far as I can see, it works.
Given
(defmulti testMulti (fn [_ command] (:command-type command)))
(defmethod testMulti :one [game command] (str "blah"))
(defmethod testMulti :default [& args] "Cannot understand")
then
(testMulti "something" {:command-type :one})
; "blah"
(testMulti "something" {:command-type :two})
; "Cannot understand"
(testMulti "something" 5)
; "Cannot understand"
as expected.
I reset the REPL before running the above afresh.
And the simple example works too. Given
(defmulti simpleMulti :key)
(defmethod simpleMulti "basic" [params] "basic value")
then
(simpleMulti {:key "basic"})
; "basic value"