Skip to content

Commit

Permalink
Add multi-default dispatcher (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
camsaul committed Apr 23, 2020
1 parent 132bf1e commit 65807bf
Show file tree
Hide file tree
Showing 15 changed files with 574 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ workflows:
name: deps
requires:
- checkout
lein-command: all-deps
lein-command: deps
after-steps:
- save_cache:
key: deps-{{ checksum "project.clj" }}
Expand Down
5 changes: 0 additions & 5 deletions .eastwood-config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@
:if-inside-macroexpansion-of #{'clojure.core/doseq}
:within-depth 50
:reason "doseq is done for side-effects. Of course the return values will be unused." })

#_(disable-warning
{:linter :unused-fn-args
:if-inside-macroexpansion-of #{'methodical.core/defmethod}
:within-depth 10})
27 changes: 12 additions & 15 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
(defproject methodical "0.9.6-alpha"
:description ""
(defproject methodical "0.10.0-alpha"
:url "https://github.com/camsaul/methodical"
:min-lein-version "2.5.0"

Expand All @@ -11,7 +10,7 @@
;; run lein deps with all dependencies from all the various profiles merged in. Useful for CI so we can cache
;; everything
"deploy" ["with-profile" "+deploy" "deploy"]
"all-deps" ["with-profiles" "-user,+all-profiles" "deps"]
"all-deps" ["with-profile" "-user,+all-profiles" "deps"]
"test" ["with-profile" "+test" "test"]
"cloverage" ["with-profile" "+cloverage" "cloverage"]
"profile" ["with-profile" "+profile" "run"]
Expand All @@ -26,7 +25,7 @@
["docstring-checker"]]}

:dependencies
[[pretty "1.0.0"]
[[pretty "1.0.4"]
[potemkin "0.4.5"]]

:aot [methodical.interface methodical.impl.standard]
Expand All @@ -49,7 +48,7 @@
:source-paths ["dev"]}

:repl
{:injections [(set! *warn-on-reflection* true)]}
{:global-vars {*warn-on-reflection* true}}

:test
{}
Expand All @@ -61,7 +60,7 @@
[[cloverage "1.1.2"]
;; Required by both Potemkin and Cloverage, but Potemkin uses an older version that breaks Cloverage's ablity to
;; understand certain forms. Explicitly specify newer version here.
[riddley "0.1.14"]]
[riddley "0.2.0"]]

:plugins
[[lein-cloverage "1.1.2"]]
Expand All @@ -70,43 +69,41 @@
:source-paths ^:replace ["src"]

:cloverage
{:fail-threshold 90}}
{:fail-threshold 92}}

:profile
{:main ^:skip-aot methodical.profile}

:eastwood
{:plugins
[[jonase/eastwood "0.3.5" :exclusions [org.clojure/clojure]]]
[[jonase/eastwood "0.3.11" :exclusions [org.clojure/clojure]]]

:eastwood
{:config-files
["./.eastwood-config.clj"]

:exclude-namespaces [:test-paths]

:remove-linters
;;disabled for now until I figure out how to disable it in the one place it's popping up
[:unused-ret-vals]
;; disabled for now until I figure out how to disable it in the one place it's popping up
#_:remove-linters
#_[:unused-ret-vals]

:add-linters
[:unused-private-vars
:unused-namespaces
#_:unused-fn-args ; disabled for now since it gives false positives that can't be disabled
:unused-locals]}}

:bikeshed
{:dependencies
;; use latest tools.namespace instead of older version so we only need to fetch it once for all plugins.
[[org.clojure/tools.namespace "0.2.11"]]
[[org.clojure/tools.namespace "1.0.0"]]

:plugins
[[lein-bikeshed "0.5.2"
:exclusions [org.clojure/tools.namespace]]]}

:kibit
{:plugins
[[lein-kibit "0.1.7"
[[lein-kibit "0.1.8"
:exclusions [org.clojure/clojure]]]}

:check-namespace-decls
Expand Down
7 changes: 5 additions & 2 deletions src/methodical/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
remove-all-methods
prefer-method
prefers])
(:require [potemkin :as p]))
(:require [methodical impl interface macros util]
[potemkin :as p]))

(require '[methodical macros impl interface util])
;; fool cljr-clean-ns and the namespace linter so it doesn't remove these automatically
(comment methodical.macros/keep-me methodical.impl/keep-me methodical.interface/keep-me methodical.util/keep-me)

(p/import-vars
[methodical.macros
Expand Down Expand Up @@ -62,6 +64,7 @@
;; dispatchers
standard-dispatcher
everything-dispatcher
multi-default-dispatcher
;; method tables
clojure-method-table
standard-method-table
Expand Down
14 changes: 13 additions & 1 deletion src/methodical/impl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[threaded :as combo.threaded]]
[methodical.impl.dispatcher
[everything :as dispatcher.everything]
[multi-default :as dispatcher.multi-default]
[standard :as dispatcher.standard]]
[methodical.impl.method-table
[clojure :as method-table.clojure]
Expand Down Expand Up @@ -145,6 +146,17 @@
prefers {}}}]
(dispatcher.everything/->EverythingDispatcher hierarchy prefers))

(defn multi-default-dispatcher
"Like the standard dispatcher, with one big improvement: when dispatching on multiple values, it supports default
methods that specialize on some args and use the default for others. (e.g. `[String :default]`)"
{:style/indent 1}
^Dispatcher [dispatch-fn & {:keys [hierarchy default-value prefers]
:or {hierarchy #'clojure.core/global-hierarchy
default-value :default
prefers {}}}]
{:pre [(ifn? dispatch-fn) (var? hierarchy) (map? prefers)]}
(dispatcher.multi-default/->MultiDefaultDispatcher dispatch-fn hierarchy default-value prefers))


;;;; ### Method Tables

Expand Down Expand Up @@ -202,7 +214,7 @@
^MultiFnImpl [dispatch-fn & dispatcher-options]
(standard-multifn-impl
(thread-last-method-combination)
(apply standard-dispatcher dispatch-fn dispatcher-options)
(apply multi-default-dispatcher dispatch-fn dispatcher-options)
(standard-method-table)))

(defn clojure-multifn-impl
Expand Down
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/operator.clj
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@

(combine-methods [_ primary-methods {:keys [around]}]
(when (seq primary-methods)
(-> ((operator operator-name) primary-methods)
(combo.common/apply-around-methods around))))
(combo.common/apply-around-methods ((operator operator-name) primary-methods)
around)))

(transform-fn-tail [_ qualifier fn-tail]
(if (= qualifier :around)
Expand Down
17 changes: 16 additions & 1 deletion src/methodical/impl/dispatcher/common.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"Utility functions for implementing Dispatchers.")

(defn add-preference
"Add a method preference to `prefs` for dispatch value `x` over `y`. Used to implement `prefer-method`."
"Add a method preference to `prefs` for dispatch value `x` over `y`. Used to implement `prefer-method`. `isa?*` is
used to determine whether a relationship between `x` and `y` that precludes this preference already exists; it can
be `clojure.core/isa?`, perhaps partially bound with a hierarchy, or some other 2-arg predicate function."
[isa?* prefs x y]
(when (= x y)
(throw (IllegalStateException. (format "Cannot prefer dispatch value %s over itself." x))))
Expand Down Expand Up @@ -59,3 +61,16 @@
are both equally-specific ancestors."
[hierarchy prefs dispatch-value dispatch-val-x dispatch-val-y]
(zero? ((domination-comparitor hierarchy prefs dispatch-value) dispatch-val-x dispatch-val-y)))

(defn distinct-by
"Like `distinct`, but uses value of `(f item)` to determine whether to keep each `item` in the resulting collection."
[f coll]
(first
(reduce
(fn [[items already-seen? :as acc] item]
(let [v (f item)]
(if (already-seen? v)
acc
[(conj items item) (conj already-seen? v)])))
[[] #{}]
coll)))
187 changes: 187 additions & 0 deletions src/methodical/impl/dispatcher/multi_default.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
(ns methodical.impl.dispatcher.multi-default
"A single-hierarchy dispatcher similar to the standard dispatcher, with one big improvement: when dispatching on
multiple values, it supports default methods that specialize on some args and use the default for others. (e.g.
`[String :default]`"
(:require [methodical.impl.dispatcher
[common :as dispatcher.common]
[standard :as dispatcher.standard]]
[methodical.interface :as i]
[potemkin.types :as p.types]
[pretty.core :refer [PrettyPrintable]])
(:import methodical.interface.Dispatcher))

(defn- partially-specialized-default-dispatch-values* [dispatch-value default-value]
;; The basic idea here is to count down from (2^(count dispatch-value) - 2) to 0, then treat each bit as whether the
;; value at the corresponding position in `dispatch-value` should be included (if the bit is `1`) or if
;; `default-value` should be included in its place (if the bit is `0`). e.g. for
;;
;; (partially-specialized-default-dispatch-values [:x :y] :default)
;;
;; then
;;
;; (count dispatch-value)` is 2
;; 2^count = 4
;;
;; i.e., count from 2 down to 0. The table below illustrates how this works:
;;
;; i | binary | corresponding dispatch val
;; --+--------+---------------------------
;; 2 | 10 | [:x :default]
;; 1 | 01 | [:default :y]
;; 0 | 00 | [:default :default]
(let [cnt (count dispatch-value)]
(for [i (reverse (range (dec (int (Math/pow 2 cnt)))))]
(vec
(for [j (reverse (range 0 cnt))]
(if (pos? (bit-and i (bit-shift-left 1 j)))
(nth dispatch-value (- cnt j 1))
default-value))))))

(defn partially-specialized-default-dispatch-values
"Return a sequence of all partially-specialized default dispatch values for a given `dispatch-value` and
`default-value`, in order from most-specific to least-specific.
(default-dispatch-values [:x :y] :default)
->
([:x :default] ; if no method for [:x :y] exists, look for [:x :default]...
[:default :y] ; or [:default :y] ...
[:default :default])"
[dispatch-value default-value]
(when (and (sequential? dispatch-value)
(not (sequential? default-value)))
(partially-specialized-default-dispatch-values* dispatch-value default-value)))

(defn- matching-partially-specialized-default-primary-method-pairs*
[{:keys [default-value dispatch-value unambiguous-pairs-seq-fn]
:or {unambiguous-pairs-seq-fn dispatcher.standard/unambiguous-pairs-seq}
:as opts}]
(mapcat
(fn [partial-default]
(let [pairs (dispatcher.standard/matching-primary-pairs-excluding-default
(assoc opts :dispatch-value partial-default))]
(unambiguous-pairs-seq-fn opts pairs)))
(partially-specialized-default-dispatch-values dispatch-value default-value)))

(defn matching-partially-specialized-default-primary-method-pairs
"Return pairs of `[dispatch-value method]` for all matching partially-specialized default methods, sorted from
most-specific to least-specific"
;; TODO - this is too many args!
[opts standard-dispatch-vals]
(->> (matching-partially-specialized-default-primary-method-pairs* opts)
(dispatcher.common/distinct-by first)
(remove
(fn [[dispatch-val]]
(contains? standard-dispatch-vals dispatch-val)))))

(defn matching-primary-methods
"Return a lazy sequence of applicable priamry methods for `dispatch-value`, sorted from most-specific to
least-specific. Similar to the implementation in `methodical.impl.dispatcher.standard`, but supports
partially-specialized default methods; see explaination in ns docstring."
[{:keys [default-value method-table unambiguous-pairs-seq-fn]
:or {unambiguous-pairs-seq-fn dispatcher.standard/unambiguous-pairs-seq}
:as opts}]
{:pre [(some? method-table)]}
;; this is basically the same logic as the version in `standard`, but instead `matches + default` we return
;; `matches + partial-defaults + default`
(let [primary-methods (i/primary-methods method-table)
opts (assoc opts :method-map primary-methods)
standard-pairs (dispatcher.standard/matching-primary-pairs-excluding-default opts)
;; filter out any partially-specialized default methods that already appear in the standard matches, e.g. if
;; dispatch value is something like [:x :default]
standard-dispatch-vals (set (map first standard-pairs))
partial-default-pairs (matching-partially-specialized-default-primary-method-pairs opts standard-dispatch-vals)
default-pair (when-not (or (contains? standard-dispatch-vals default-value)
(contains? (set (map first partial-default-pairs)) default-value))
(when-let [default-method (get primary-methods default-value)]
[default-value default-method]))
pairs (concat
(unambiguous-pairs-seq-fn opts standard-pairs)
partial-default-pairs
(when default-pair [default-pair]))]
(->> pairs
(dispatcher.common/distinct-by first)
(map second))))

(defn- aux-dispatch-values [qualifier {:keys [default-value method-table dispatch-value hierarchy prefs]}]
(let [comparitor (dispatcher.common/domination-comparitor hierarchy prefs dispatch-value)]
(distinct
(sort-by
identity
comparitor
(for [dispatch-value (concat [dispatch-value]
(partially-specialized-default-dispatch-values dispatch-value default-value)
[default-value])
dv (keys (get (i/aux-methods method-table) qualifier))
:when (isa? hierarchy dispatch-value dv)]
dv)))))

(defn- matching-aux-methods*
[qualifier {:keys [method-table] :as opts}]
(let [method-map (i/aux-methods method-table)]
(for [dispatch-value (aux-dispatch-values qualifier opts)
m (get-in method-map [qualifier dispatch-value])]
m)))

(defn matching-aux-methods
"Impl of `Dispatcher` `matching-aux-methods` for the multi-default dispatcher."
[{:keys [method-table] :as opts}]
(into {} (for [[qualifier] (i/aux-methods method-table)]
[qualifier (matching-aux-methods* qualifier opts)])))

(p.types/deftype+ MultiDefaultDispatcher [dispatch-fn hierarchy-var default-value prefs]
PrettyPrintable
(pretty [_]
(concat ['multi-default-dispatcher dispatch-fn]
(when (not= hierarchy-var #'clojure.core/global-hierarchy)
[:hierarchy hierarchy-var])
(when (not= default-value :default)
[:default-value default-value])
(when (seq prefs)
[:prefers prefs])))

Object
(equals [_ another]
(and
(instance? MultiDefaultDispatcher another)
(let [^MultiDefaultDispatcher another another]
(and
(= dispatch-fn (.dispatch-fn another))
(= hierarchy-var (.hierarchy-var another))
(= default-value (.default-value another))
(= prefs (.prefs another))))))

Dispatcher
(dispatch-value [_] (dispatch-fn))
(dispatch-value [_ a] (dispatch-fn a))
(dispatch-value [_ a b] (dispatch-fn a b))
(dispatch-value [_ a b c] (dispatch-fn a b c))
(dispatch-value [_ a b c d] (dispatch-fn a b c d))
(dispatch-value [_ a b c d more] (apply dispatch-fn a b c d more))

(matching-primary-methods [_ method-table dispatch-value]
(matching-primary-methods
{:hierarchy (var-get hierarchy-var)
:prefs prefs
:default-value default-value
:method-table method-table
:dispatch-value dispatch-value}))

(matching-aux-methods [_ method-table dispatch-value]
(matching-aux-methods
{:hierarchy (var-get hierarchy-var)
:prefs prefs
:default-value default-value
:method-table method-table
:dispatch-value dispatch-value}))

(default-dispatch-value [_]
default-value)

(prefers [_]
prefs)

(prefer-method [this x y]
(let [new-prefs (dispatcher.common/add-preference (partial isa? (var-get hierarchy-var)) prefs x y)]
(if (= prefs new-prefs)
this
(MultiDefaultDispatcher. dispatch-fn hierarchy-var default-value new-prefs)))))
Loading

0 comments on commit 65807bf

Please sign in to comment.