Skip to content

Commit

Permalink
0.14.0 Documentation (#124)
Browse files Browse the repository at this point in the history
* Describe improvements

* Document everything in 14

* Update image

* Update Kondo blurb

* Improved README

* Link to CIDER

* Remove busted link
  • Loading branch information
camsaul committed Sep 9, 2022
1 parent 50e2b80 commit 274d0ef
Show file tree
Hide file tree
Showing 24 changed files with 165 additions and 53 deletions.
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ next method. In vanilla Clojure multimethods, you'd have to do something like th

If you're not sure whether a `next-method` exists, you can check whether it's `nil` before calling it.

Methodical exports custom [clj-kondo](https://github.com/clj-kondo/clj-kondo) configuration and hooks for `defmulti`
and `defmethod`; with the exported configuration it will even tell you if you call `next-method` with the wrong number
of args:

![Kondo](assets/kondo.png)

## Auxiliary Methods: `:before`, `:after`, and `:around`

Expand Down Expand Up @@ -485,12 +490,95 @@ following summarizes all component implementations that currently ship with Meth

* `cached-multifn-impl` -- wraps another multifn impl and an instance of `Cache` to implement caching.

### Validation

Methodical offers a few opportunities for validation above and beyond what normal Clojure multimethods offer.

#### `:dispatch-value-spec`

If you include a `:dispatch-value-spec` in the metadata of a `defmulti`, it will automatically be used to validate the
dispatch value form of any `defmethod` forms at macroexpansion time:

```clj
(macros/defmulti mfx
{:arglists '([x y]), :dispatch-value-spec (s/cat :x keyword?, :y int?)}
(fn [x y] [x y]))

(macros/defmethod mfx [:x 1]
[x y]
{:x x, :y y})
;; => #'methodical.macros-test/mfx

(macros/defmethod mfx [:x]
[x y]
{:x x, :y y})
;; failed: Insufficient input in: [0] at: [:args-for-method-type :primary :dispatch-value :y] [:x]
```

This is a great way to make sure people use your multimethods correctly and catch errors right away.

### Debugging

Methodical offers debugging facilities so you can see what's going on under the hood, such as the `trace` utility:

![Trace](assets/tracing.png)

and the `describe` utility, which outputs Markdown-formatted documentation, for human-friendly viewing in tools like
[CIDER](https://github.com/clojure-emacs/cider):

![Describe](assets/describe.png)

Methodical multimethods also implement `datafy`:

```clj
(clojure.datafy/datafy mf)

=>

{:ns 'methodical.datafy-test
:name 'methodical.datafy-test/mf
:file "methodical/datafy_test.clj"
:line 11
:column 1
:arglists '([x y])
:class methodical.impl.standard.StandardMultiFn
:combo {:class methodical.impl.combo.threaded.ThreadingMethodCombination
:threading-type :thread-last}
:dispatcher {:class methodical.impl.dispatcher.multi_default.MultiDefaultDispatcher
:dispatch-fn methodical.datafy-test/dispatch-first
:default-value :default
:hierarchy #'clojure.core/global-hierarchy
:prefs {:x #{:y}}}
:method-table {:class methodical.impl.method_table.standard.StandardMethodTable
:primary {:default
{:ns 'methodical.datafy-test
:name 'methodical.datafy-test/mf-primary-method-default
:doc "Here is a docstring."
:file "methodical/datafy_test.clj"
:line 15
:column 1
:arglists '([next-method x y])}}
:aux {:before {[:x :default] [{:ns 'methodical.datafy-test
:name 'methodical.datafy-test/mf-before-method-x-default
:doc "Another docstring."
:file "methodical/datafy_test.clj"
:column 1
:line 20
:arglists '([_x y])
:methodical/unique-key 'methodical.datafy-test}]}
:around {[:x :y] [{:ns 'methodical.datafy-test
:name 'methodical.datafy-test/mf-around-method-x-y
:file "methodical/datafy_test.clj"
:column 1
:line 25
:arglists '([next-method x y])
:methodical/unique-key 'methodical.datafy-test}]}}}
:cache {:class methodical.impl.cache.watching.WatchingCache
:cache {:class methodical.impl.cache.simple.SimpleCache
:cache {}}
:refs #{#'clojure.core/global-hierarchy}}}
```

## Performance

Methodical is built with performance in mind. Although it is written entirely in Clojure, and supports many more
Expand Down
Binary file added assets/describe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/kondo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ coverage:
status:
project:
default:
# Project must always have at least 80% coverage (by line)
# Project must always have at least 85% coverage (by line)
target: 85%
# Whole-project test coverage is allowed to drop up to 5%. (For situations where we delete code with full coverage)
threshold: 5%
patch:
default:
# Changes must have at least 80% test coverage (by line)
target: 90%
target: 80%
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns hooks.methodical.macros
(:refer-clojure :exclude [defmulti defmethod])
(:require [clj-kondo.hooks-api :as hooks]))
(:require
[clj-kondo.hooks-api :as hooks]))

;;; The code below is basically simulating the spec for parsing defmethod args without using spec. It uses a basic
;;; backtracking algorithm to achieve a similar result. Parsing defmethod args is kinda complicated.
Expand Down Expand Up @@ -116,7 +117,9 @@
[(hooks/list-node
(list*
(hooks/token-node 'fn)
(hooks/token-node 'next-method)
(hooks/token-node (if (contains? #{nil :around} (some-> (:qualifier parsed) hooks/sexpr))
'next-method
'__FN__NAME__THAT__YOU__CANNOT__REFER__TO__))
fn-tail))]))]
#_(println "=>")
#_(clojure.pprint/pprint (hooks/sexpr result))
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/cache/simple.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
{:class (class this)
:cache @atomm})

describe/Describeable
describe/Describable
(describe [this]
(format "It caches methods using a %s." (.getCanonicalName (class this)))))
(format "It caches methods using a [[%s]]." (.getCanonicalName (class this)))))
4 changes: 2 additions & 2 deletions src/methodical/impl/cache/watching.clj
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
:cache (datafy/datafy cache)
:refs refs})

describe/Describeable
describe/Describable
(describe [this]
(format "It caches methods using a %s." (.getCanonicalName (class this)))))
(format "It caches methods using a [[%s]]." (.getCanonicalName (class this)))))

(defn- cache-watch-fn [cache]
(let [cache-weak-ref (WeakReference. cache)]
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/clojure.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
(datafy [this]
{:class (class this)})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method combination %s." (.getCanonicalName (class this)))))
(format "It uses the method combination [[%s]]." (.getCanonicalName (class this)))))
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/clos.clj
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,6 @@
(datafy [this]
{:class (class this)})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method combination %s." (.getCanonicalName (class this)))))
(format "It uses the method combination [[%s]]." (.getCanonicalName (class this)))))
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/operator.clj
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@
{:class (class this)
:operator operator-name})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method combination %s\nwith the operator %s."
(format "It uses the method combination [[%s]]\nwith the operator `%s`."
(.getCanonicalName (class this))
(pr-str operator-name))))

Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/threaded.clj
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@
{:class (class this)
:threading-type threading-type})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method combination %s\nwith the threading strategy %s."
(format "It uses the method combination [[%s]]\nwith the threading strategy `%s`."
(.getCanonicalName (class this))
(pr-str threading-type))))

Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/dispatcher/everything.clj
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@
:hierarchy hierarchy-var
:prefs prefs})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s."
(format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`."
(.getCanonicalName (class this))
(pr-str hierarchy-var)
(pr-str prefs))))
4 changes: 2 additions & 2 deletions src/methodical/impl/dispatcher/multi_default.clj
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@
:hierarchy hierarchy-var
:prefs prefs})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s.\n\nThe default value is %s."
(format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`.\n\nThe default value is `%s`."
(.getCanonicalName (class this))
(pr-str hierarchy-var)
(pr-str prefs)
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/dispatcher/standard.clj
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@
:hierarchy hierarchy-var
:prefs prefs})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the dispatcher %s\nwith hierarchy %s\nand prefs %s.\n\nThe default value is %s."
(format "It uses the dispatcher [[%s]]\nwith hierarchy `%s`\nand prefs `%s`.\n\nThe default value is `%s`."
(.getCanonicalName (class this))
(pr-str hierarchy-var)
(pr-str prefs)
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/method_table/clojure.clj
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
{:class (class this)
:primary (method-table.common/datafy-primary-methods m)})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method table %s. These primary methods are known:\n\n%s"
(format "It uses the method table [[%s]]. These primary methods are known:\n\n%s"
(.getCanonicalName (class this))
(method-table.common/describe-primary-methods m))))
10 changes: 5 additions & 5 deletions src/methodical/impl/method_table/common.clj
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
(str/join
\space
[(when method-ns
(format "defined in %s" (ns-name method-ns)))
(format "defined in [[%s]]" (ns-name method-ns)))
(cond
(and file line)
(format "(%s:%d)" file line)
Expand All @@ -55,9 +55,9 @@
(format "\n\nIt has the following documentation:\n\n%s" doc))])))

([dispatch-value f]
(format "* %s, %s" (pr-str dispatch-value) (str/join
"\n "
(str/split-lines (describe-method f))))))
(format "* `%s`, %s" (pr-str dispatch-value) (str/join
"\n "
(str/split-lines (describe-method f))))))

(defn describe-primary-methods
"Helper for [[methodical.util.describe/describe]]ing the primary methods in a method table."
Expand All @@ -80,7 +80,7 @@
"\n\n"
(for [[qualifier dispatch-value->methods] (sort-by first qualifier->dispatch-value->methods)]
(format
"%s methods:\n\n%s"
"`%s` methods:\n\n%s"
(pr-str qualifier)
(str/join
"\n\n"
Expand Down
5 changes: 3 additions & 2 deletions src/methodical/impl/method_table/standard.clj
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@
:primary (method-table.common/datafy-primary-methods primary)
:aux (method-table.common/datafy-aux-methods aux)})

describe/Describeable
describe/Describable
(describe [this]
(format "It uses the method table %s.%s%s" (.getCanonicalName (class this))
(format "It uses the method table [[%s]].%s%s"
(.getCanonicalName (class this))
(method-table.common/describe-primary-methods primary)
(method-table.common/describe-aux-methods aux))))
2 changes: 1 addition & 1 deletion src/methodical/impl/multifn/cached.clj
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
:class (class this)
:cache (datafy/datafy cache)))

describe/Describeable
describe/Describable
(describe [_this]
(str (describe/describe cache)
\newline \newline
Expand Down
2 changes: 1 addition & 1 deletion src/methodical/impl/multifn/standard.clj
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
:dispatcher (datafy/datafy dispatcher)
:method-table (datafy/datafy method-table)})

describe/Describeable
describe/Describable
(describe [_this]
(str (describe/describe combo)
\newline \newline
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/standard.clj
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,13 @@
{:class (class this)})
mta))

describe/Describeable
describe/Describable
(describe [_this]
(let [{mf-name :name, mf-ns :ns, :keys [file line]} mta]
(str (pr-str mf-name)
(let [message (str
(when mf-ns
(ns-name mf-ns))
(format "[[%s]]" (ns-name mf-ns)))
(cond
(and file line) (format " (%s:%d)" file line)
file (str \space file)
Expand Down
3 changes: 2 additions & 1 deletion src/methodical/macros.clj
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@
(let [allowed-qualifiers (i/allowed-qualifiers multifn)
primary-methods-allowed? (contains? allowed-qualifiers nil)
allowed-aux-qualifiers (disj allowed-qualifiers nil)
dispatch-value-spec (get (meta multifn) :dispatch-value-spec (default-dispatch-value-spec allowed-aux-qualifiers))]
dispatch-value-spec (or (some-> (get (meta multifn) :dispatch-value-spec) s/spec)
(default-dispatch-value-spec allowed-aux-qualifiers))]
(s/cat :args-for-method-type (s/alt :primary (if primary-methods-allowed?
(s/cat :dispatch-value dispatch-value-spec)
(constantly false))
Expand Down
6 changes: 3 additions & 3 deletions src/methodical/util/describe.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
(:require [clojure.datafy :as datafy]
[potemkin.types :as p.types]))

(p.types/defprotocol+ Describeable
(p.types/defprotocol+ Describable
(describe ^String [this]
"Return a string description of a Methodical object, such as a multifn."))
"Return a Markdown-formatted string description of a Methodical object, such as a multifn."))

(extend-protocol Describeable
(extend-protocol Describable
nil
(describe [_this]
"nil")
Expand Down
19 changes: 19 additions & 0 deletions test/methodical/macros_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,22 @@
#_{:clj-kondo/ignore [:unresolved-symbol]} #'docstring-multifn-primary-method-docstring
(first (u/aux-methods docstring-multifn :around :docstring))
#_{:clj-kondo/ignore [:unresolved-symbol]} #'docstring-multifn-around-method-docstring))


(macros/defmulti mf-dispatch-value-spec-2
{:arglists '([x y]), :dispatch-value-spec (s/cat :x keyword?, :y int?)}
(fn [x y] [x y]))

(t/deftest dispatch-value-spec-test-2
(t/testing "We should specize :dispatch-value-spec if needed"
(t/is (some?
(macroexpand
'(macros/defmethod mf-dispatch-value-spec-2 [:x 1]
[x y]
{:x x, :y y}))))
(t/is (thrown?
clojure.lang.Compiler$CompilerException
(macroexpand
'(macros/defmethod mf-dispatch-value-spec-2 [:x]
[x y]
{:x x, :y y}))))))
Loading

0 comments on commit 274d0ef

Please sign in to comment.