Skip to content

Commit

Permalink
Feature Request: Syntactic sugar for trivial reg-sub declarations. (#634
Browse files Browse the repository at this point in the history
)

* Added syntactic sugar to reg-sub to allow submission of functions that only take input signals or input signals and data in query vector.

* Added tests for both computation function syntactic sugar options

* Restore package.json to original content after accidental add

* Add first pass for updated reg-sub docstring

* Added more error checking into the reg-sub implementation

* Reset config files to original

* Update docs
  • Loading branch information
bsboiko authored Dec 19, 2021
1 parent 0184aa6 commit 7feb729
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 4 deletions.
89 changes: 89 additions & 0 deletions src/re_frame/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,85 @@
can be created later, when a node is bought into existence by the
use of `subscribe` in a `View Function`.
`reg-sub` arguments are:
- a `query-id` (typically a namespaced keyword)
- a function which returns the inputs required by this kind of node (can be supplied in one of three ways)
- a function which computes the value of this kind of node (can be supplied in one of three ways)
The `computation function` is always the last argument supplied and has three ways to be called.
Two of these methods are syntactic sugar to provide easier access to functional abstractions around your data.
1. A function that will accept two parameters, the `input-values` and `query-vector`. This is the
standard way to provide a `computation-function`
#!clj
(reg-sub
:query-id
(fn [input-values query-vector]
(:foo input-values)))
2. A single sugary tuple of `:->` and a 1-arity `computation-function`:
#!clj
(reg-sub
:query-id
:-> computation-fn)
This sugary variation allows you to pass a function that will expect only one parameter,
namely the `input-values` and entirely omit the `query-vector`. A typical `computation-function`
expects two pramenters which can cause unfortunate results when attempting to use
clojure standard library functions, or other functions, in a functional manner.
For example, a significant number of subscriptions exist only to get a value
from the `input-values`. As shown below, this subscription will simply retrieve
the value associated with the `:foo` key in our db:
#!clj
(reg-sub
:query-id
(fn [db _] ;; :<---- trivial boilerplate we might want to skip over
(:foo db)))
This is slightly more boilerplate than we might like to do,
as we can use a keyword directly as a function, and we might like to do this:
#!clj
(reg-sub
:query-id
:foo) ;; :<---- This could be dangerous. If `:foo` is not in db, we get the `query-vector` instead of `nil`.
By using `:->` our function would not contain the `query-vector`, and any
missing keys would be represented as such:
#!clj
(reg-sub
:query-id
:-> :foo)
This form allows us to ignore the `query-vector` if our `computation-function`
has no need for it, and be safe from any accidents. Any 1-arity function can be provided,
and for more complicated use cases, `partial`, `comp`, and anonymous functions can still be used.
3. A single sugary tuple of `:=>` and a multi-arity `computation-function`
#!clj
(reg-sub
:query-id
:=> computation-fn)
The `query-vector` can be broken into two components `[query-id & optional-values]`, and
some subscriptions require the `optional-values` for extra work within the subscription.
To use them in variation #1, we need to destructure our `computation-function` parameters
in order to use them.
#!clj
(reg-sub
:query-id
(fn [db [_ foo]]
[db foo]))
Again we are writing boilerplate just to reach our values, and we might prefer to
have direction access through a parameter vector like `[input-values optional-values]`
instead, so we might be able to use a multi-arity function directly as our `computation-function`.
A rewrite of the above sub using this sugary syntax would look like this:
#!clj
(reg-sub
:query-id
:=> vector) ;; :<---- Could also be `(fn [db foo] [db foo])`
The `computation function` is expected to take two arguments:
- `input-values` - the values which flow into this node (how is it wired into the graph?)
Expand Down Expand Up @@ -331,6 +410,16 @@
(fn [a query-vec] ;; only one pair, so 1st argument is a single value
...))
Syntactic sugar for both the `signal-fn` and `computation-fn` can be used together
and the direction of arrows shows the flow of data and functions. The example from
directly above is reproduced here:
#!clj
(reg-sub
:a-b-sub
:<- [:a-sub]
:<- [:b-sub]
:-> (partial zipmap [:a :b]))
For further understanding, read the tutorials, and look at the detailed comments in
/examples/todomvc/src/subs.cljs.
Expand Down
26 changes: 22 additions & 4 deletions src/re_frame/subs.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,30 @@
(trace/merge-trace! {:tags {:input-signals (doall (to-seq (map-signals reagent-id signals)))}})
dereffed-signals))


(defn reg-sub
[query-id & args]
(let [computation-fn (last args)
input-args (butlast args) ;; may be empty, or one signal fn, or pairs of :<- / vector
err-header (str "re-frame: reg-sub for " query-id ", ")
(let [err-header (str "re-frame: reg-sub for " query-id ", ")
[input-args ;; may be empty, or one signal fn, or pairs of :<- / vector
computation-fn] (let [[op f :as comp-f] (take-last 2 args)]
(if (or (= 1 (count comp-f))
(fn? op)
(vector? op))
[(butlast args) (last args)]
(let [args (drop-last 2 args)]
(case op
;; return a function that calls the computation fn
;; on the input signal, removing the query vector
:->
[args (fn [db _]
(f db))]
;; return a function that calls the computation fn
;; on the input signal and the data in the query vector
;; that is not the query-id
:=>
[args (fn [db [_ & qs]]
(apply f db qs))]
;; an incorrect keyword was passed
(console :error err-header "expected :-> or :=> as second to last argument, got:" op)))))
inputs-fn (case (count input-args)
;; no `inputs` function provided - give the default
0 (fn
Expand Down
78 changes: 78 additions & 0 deletions test/re_frame/subs_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,84 @@
(reset! db/app-db {:a 1 :b 2})
(is (= {:a 1 :b 2} @test-sub))))

(deftest test-sub-macros-->
"test the syntactical sugar for input signal"
(subs/reg-sub
:a-sub
:-> :a)

(subs/reg-sub
:b-sub
:-> :b)

(subs/reg-sub
:c-sub
:-> :c)

(subs/reg-sub
:d-sub
:-> :d)

(subs/reg-sub
:d-first-sub
:<- [:d-sub]
:-> first)

;; variant of :d-first-sub without an input parameter
(subs/reg-sub
:e-first-sub
:-> (comp first :e))

;; test for equality
(subs/reg-sub
:c-foo?-sub
:<- [:c-sub]
:-> #{:foo})

(subs/reg-sub
:a-b-sub
:<- [:a-sub]
:<- [:b-sub]
:-> (partial zipmap [:a :b]))

(let [test-sub (subs/subscribe [:a-b-sub])
test-sub-c (subs/subscribe [:c-foo?-sub])
test-sub-d (subs/subscribe [:d-first-sub])
test-sub-e (subs/subscribe [:e-first-sub])]
(is (= nil @test-sub-c))
(reset! db/app-db {:a 1 :b 2 :c :foo :d [1 2] :e [3 4]})
(is (= {:a 1 :b 2} @test-sub))
(is (= :foo @test-sub-c))
(is (= 1 @test-sub-d))
(is (= 3 @test-sub-e))))

(deftest test-sub-macros-=>
"test the syntactical sugar for input signals and query vector arguments"
(subs/reg-sub
:a-sub
:-> :a)

(subs/reg-sub
:b-sub
:-> :b)

(subs/reg-sub
:test-a-sub
:<- [:a-sub]
:=> vector)

;; test for equality of input signal and query parameter
(subs/reg-sub
:test-b-sub
:<- [:b-sub]
:=> =)

(let [test-a-sub (subs/subscribe [:test-a-sub :c])
test-b-sub (subs/subscribe [:test-b-sub 2])]
(reset! db/app-db {:a 1 :b 2})
(is (= [1 :c] @test-a-sub))
(is (= true @test-b-sub))))

(deftest test-registering-subs-doesnt-create-subscription
(let [sub-called? (atom false)]
(with-redefs [subs/subscribe (fn [& args] (reset! sub-called? true))]
Expand Down

0 comments on commit 7feb729

Please sign in to comment.