How to document a defmethod function? - clojure

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

LazySeq cannot be cast to Comparable on sort-by

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}

Howto include clojure.spec'd functions in a test suite

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}})))

Accessing argument's metadata in Clojure macro

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.

Defining a function in Environ (Clojure) and then using it in code

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.

Clojure multimethod giving unexpected null pointer

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"