Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better re-frame subscriptions - opinions/ideas requested #170

Closed
mike-thompson-day8 opened this issue Jun 6, 2016 · 41 comments
Closed

Better re-frame subscriptions - opinions/ideas requested #170

mike-thompson-day8 opened this issue Jun 6, 2016 · 41 comments

Comments

@mike-thompson-day8
Copy link
Contributor

mike-thompson-day8 commented Jun 6, 2016

Better re-frame subscriptions

I'd like to pour some macro sugar on re-frame.core/register-sub. I'd like your feedback and ideas.

Naming

First question: what should we call this macro? defsub, def-sub, defs? Other? Below, I assume defsub.

The Goals

Solve these current problems:

  1. The existing method requires a bit too much knowledge about reaction and, in more complex cases, chaining reactions, and how one subscription can depend on the output of another subscription, etc. All doable, and knowable, of course, but I'd love to make it simpler.
  2. Currently, subscription handlers are not pure functions. They take a ratom app-db as a parameter, not a value. They also return a reaction rather than a value. There's a technique to create pure subscription functions but it involves a bit too much boilerplate. I'd like pure functions without the boilerplate (defsub should look after the boilerplate for me)

Improve by adding:

  1. Subscription de-duplication has been added to re-frame (yet to be released) and I'd like to encourage the creation of "signal graphs" where one subscription uses anothers as input. For a start, it feels a very natural thing to do, and second there are efficiency gains to be had. de-duplication means that root branches of the signal graph can quickly prune value propagation when there's been no changes.
  2. Each subscription should optionally provide a cljs.spec for its output, and this should be checked. Maybe, this checking will be be disabled in production.
  3. To assist in debugging:
  • provide a mechanism to trace subscriptions activity to js/console. Trace when they are created, and rerun.
  • provide a mechanism to measure subscriptions. How many times has a subscription been rerun? Accumulated time.

Other goals:

  1. needs to be backwards compatible. Ie. re-frame.core/register-sub should still just work.
  2. Macros are DSLs which bring their own cognitive overhead. Ideally the macro's design should use concepts, names etc already familiar to a clojure programmer. A proficient programmer should almost be able to guess what's going on.
  3. Being a lazy sod, I look at these things through the prism of "how easy would this be to teach". How may pages of docs will I have to write :-) ? And how many slack chats will there be explaining to people what they did wrong. Oh, yeah, and of course, how powerful.

And, what's going on is:
A. There's a pure function
B. it performs computation using the values from:
- one or more "input signals"
- data from a query vector
- zero or more dynamic query parameters

Question: are there other considerations/goals I've missed?

Backgrounder

This ticket is about subscription definition, but let's remind ourselves about use:

   (subscribe [:query-id 1 2 3] [r1 r2])

The 1st parameter is the query vector, which starts with the query-id (generally a namespaced keyword). The 2nd parameter is a vector of input signals (ratoms/reactions). When they change, the subscription should be rerun.

Most subscriptions only involve one parameter. Many re-framers don';t even know about the 2nd.

What defsub use might look like

A super simple example:

(defsub :items
  (fn [db v]
    (:items db))

Notice:

  1. The first parameter to the macro is the query id (:items), just as it would be with re-frame.core/register-sub. So no change there.
  2. The second parameter is a handler fn of the kind normally given to re-frame.core/register-sub except:
  • db is a value, not a reagent.core/atom
  • it returns a value and not a reagent.ratom/reaction
  • so a pure function

Point 2 is 50% of the battle.

Question: does this variation work better for you?

(defsub :items
 :fn [db _]          ;; using an `:fn` keyword
     (:items db))

Or maybe pare it right back to this:

(defsub :items
  [db _]            ;; no mention of fn
  (:items db))

This approach is lovely for simple cases. But, it could be problematic as we get into more complex cases. Read on.

Return Spec Checking

defsub can also take a registered cljs.spec which describes the structure of the value returned by the handler fn.

(defsub :items
 :ret-spec  :info-model/items
 (fn [db _]
     (:items db)))

Signal Graphs

re-frame automatically de-duplicates subscriptions. As a result, an architecture in which subscriptions are built using inputs from other subscriptions will result in an efficient signal graph.

So we want to support that process.

Here's a complex registration from the existing re-frame README:

(re-frame.core/register-sub
 :sorted-items             ;; the query id
 (fn [db [_]]
   (let [items      (reaction (get-in @db [:some :path :to :items]))  ;; reaction #1
         sort-attr  (reaction (get-in @db [:sort-by]))]                ;; reaction #2
       (reaction (sort-by @sort-attr @items)))))                       ;; reaction #3

This formulation has problems. Firstly, it requires a bit of knowledge arranging all those reactions correctly. Hard for a newbie. And easy enough for someone experienced to get wrong. Secondly, this formulation offers no chance for de-duplication of the input reactions. If the same reactions are used elsewhere they will run as duplicates.

So, current best practice is to rewrite it like this:

(re-frame.core/register-sub
  :sorted-items
  (fn [db [_]]
    (let [items      (subscribe [:items])       ;; was a reaction, now a subscription 
          sort-attr  (subscribe [:sort-by]))]     ;; was a reaction, now a subscription 
      (reaction (sort-by @sort-attr @items)))))    ;; reaction #3 (hasn't changed)

Better!! But still a bit hard for a newbie to get right. For example, someone can easily wrap the entire let in a reaction which is wrong.

And, even if that wasn't a problem, we don't want reaction used explicitly any more.

So, here's how it might look with defsub:

(defsub :sorted-items
 :ret-spec  :info-model/items
 :subscribe [:items]       ;; assumes we've already created this subscription
 :subscribe [:sort-by]     ;; ditto
 (fn [items sort-attr query-v]
     (sort-by sort-attr items)))

Note:

  1. input signals are defined via zero or more :subscribe clauses. In the absence of a :subscribe a default will be provided :subscribe [:/] which is a pre-defined subscription which gives you the root of app-db
  2. The values from these N subscriptions are provided to the fn positionally as the first N parameters.
  3. The fn is still pure. Values in, values out. No side effects.

Or perhaps this variation is better:

(defsub :sorted-items
 :ret-spec  :info-model/items
 :subscribe [:items]       
 :subscribe [:sort-by]  
 :fn [[items sort-attr] query-v]   ;; using :fn and a 1st parameter vector
     (sort-by sort-attr items)))

OR maybe we do awayaway with the fn formulation (args etc):

(defsub  :sorted-items
 :ret-spec  :info-model/items
 :subscribe items [:items]        ;; :subscribe, symbol and then query vector
 :subscribe sort-attr [:sort-by]
 :query [_ a b]                     ;; destructure the query vector, if those values are used
 (sort-by sort-attr items))        ;; don't even bother with whole `fn` args thing, just provide the fn body.

but this latest one makes the simple case a bit hard:

(defsub :items
  (:items db))        ;;  without fn "args" how to I name "db"

maybe I have to do this:

(defsub :items
  :subscribe db [:/] 
  (:items db))        ;;  now db is explicitly named

Hmm. I want my DSL to be immediately familiar to a clojure programmer. I'm not sure that loosing the fn part is good. We are writing a function. It is just that we want it to be reactively re-run.

OR maybe we prefer

(defsub :sorted-items
 :ret-spec  :info-model/items
 :fn   sorted-items                     
 :args  items      :from  :subscribe [:items]
        sort-attr  :from  :subscribe [:sort-by]  
        [_ a b c]  :from  :query-v            
 :body (sort-by sort-attr items))

So which of these appeals? Other alternatives?

The Generated code

Let's switch gears and, for the moment, forget about DSL aesthetics.

What code will this macro generate? It will still need to call re-frame.core/register-sub.

;; create the pure function
(defn sorted-items             ;; named from the keyword?  Can name be programmer supplied?
  [items sort-attr query-v]    ;; if needed query-v might be [_ a b c]
  (sort-by sort-attr items)))

;; register the event with the pure function
(re-frame.core/register-sub
  :sorted-items
  (fn [app-db query-v dyn-v]
    ;; some checks
    (assert (vector? query-v)
    (assert (or (nil? dyn-v) (vector? dyn-v))

    ;; some tracing
    (when ^boolean goog.DEBUG (debug "re-frame: creating subscription: "  query-v))

    ;; gather the input signals
    (let [items     (subscript [:items])
          sort-attr (subscribe [:sort-by])]

      ;; now make the rerunnable reaction
      (make-reaction  (fn [] 
                        (when ^boolean goog.DEBUG (debug "re-frame: rerun subscription: " query-v))
                        (sorted-items @items @sorted-attr query-v (mapv deref dyn-v)))))    ;; perhaps also some monitoring here

Further Thoughts

Provide a spec for the query-v ?
Perhaps just fspec the handler?
Remember a subscription can use a value in the query-v
Remember a subscription can use the value from a dynamic parameter :subscription [:some-q 42] [r]

@caioaao
Copy link
Contributor

caioaao commented Jun 6, 2016

About the fn discussion. I think it'd be better if we could pass a function to defsub, so we could do stuff like

(defsub
  :subscribe [:anything]
  :query (comp fn-1 fn-2 fn-3))

Which is better than

(defsub
  :subscribe [:anything]
  :query [x] ((comp fn-1 fn-2 fn-3) x))

Removing fn semantics from defsub can ruin composability (and with no relevant gains, from what i saw)

@arichiardi
Copy link

arichiardi commented Jun 6, 2016

Personally I like that we follow a classic macro approach, keeping (fn [....) as returned value of subscribe, but...

...another thing I noticed from the above description (thanks Mike, as usual awesome writing) is that a data-only pattern is emerging, where you don't need macros and you define params in a map.

I am talking about this in particular:

(defsub :sorted-items
 :ret-spec  :info-model/items
 :fn   sorted-items                     
 :args  items      :from  :subscribe [:items]
        sort-attr  :from  :subscribe [:sort-by]  
        [_ a b c]  :from  :query-v            
 :body (sort-by sort-attr items))

This is an accepted trade in the Clojure community as well (see Onyx), greatly improve composability and allows user to define their own dsl.
However, I am ignoring backward compatibility here and maybe going a bit off topic :)

@rasom
Copy link

rasom commented Jun 6, 2016

I would prefer something like this

(defsub :sorted-items
 :ret-spec  :info-model/items
 [db query-v]
 [items [:items]       
  sort-by [:sort-by]]     
 (sort-by sort-attr items))

where

[items [:items]       
 sort-by [:sort-by]]

is like "extracted" from let form

@Samstiles
Copy link

Samstiles commented Jun 6, 2016

Hey @mike-thompson-day8 ! Thanks again for your work.

My 2c in the order that you listed items.

  1. (defsub) 👍
  2. No mention of (fn) or :fn: IMO. Cleaner and doesn't give people the implication that they can do fancy function things... unless you want them to be able to do those things? In which case your hands are tied and you need to allow it because of (comp) (partial) etc
  3. 👍 absolutely yes spec support in the (defsub) macro!
  4. RE composing multiple subs; the first example imo. I do not like :/ and don't think it'd be familiar to clojure devs
  5. RE further thoughts: yes spec, spec, spec support all the things!

@rhg
Copy link

rhg commented Jun 6, 2016

In my humble opinon, I am not on-board with defsub. As an alternative I propose having pure functions with one arg per subscription and a function that adds metadata to the functions var (and possibly a macro to add sugar to the function). Then subscribe takes a var and looks up metadata.

On the plus side this is very unmagical, but it may not be backwards compatible.

Thus:

  (def sorted-items sort-by)
  (re-frame.core/new-sub #'sorted-items [:sort-by :items])

  (re-frame.core/subscribe #'sorted-items)

@escherize
Copy link

Simplifying subscriptions is a good idea.

In my opinion, defsub should use a pure function (defined inline or otherwise) to do the extraction logic. One reason is that the extraction functions can be defined in cljc, and introspected, tested, and extended via clojure. This also allows mocking http endpoints and testing the behavior of the frontend system from the jvm.

I'd vote:

(defsub :items
 :ret-spec  :info-model/items
 (fn [db _]
     (:items db)))

@mccraigmccraig
Copy link

hmm... defsub is looking a bit like applicative-do aka alet syntax for a reaction-applicative http://funcool.github.io/cats/latest/#alet

(defsub :sorted-items
  :ret-spec :info-model/items
  [db [_ a b c]]
  (alet [items (subscribe [:items])
         sort-attr (subscribe [:sort-by])]
    (sort-by sort-attr items)))

i need to think on this some more

@danielcompton
Copy link
Contributor

Good point @mccraigmccraig, it also reminds me in some ways of manifold's let-flow macro.

@mccraigmccraig
Copy link

right @danielcompton let-flow is very similar to alet for Deferreds

@mccraigmccraig
Copy link

mccraigmccraig commented Jun 7, 2016

one difficulty with alet would be (looking at cats' implementation ) the reaction functions would likely be anonymous (with an applicative instance which i'm guessing (really) wildly would look something like this ) which wouldn't be great for tracing... though maybe useful names for tracing could be passed as metadata on the reactions themselves (which would seem to require Reaction to implement IWithMeta too)

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jun 10, 2016

Using the current regsub proposal to implement todomvc subscriptions:
https://gist.github.com/mike-thompson-day8/077e5bd41ff6cd167f96170604380026

Compare to current:
https://github.com/Day8/re-frame/blob/4b931fe67208c10ae960c683fd837b03c38f88b0/examples/todomvc/src/todomvc/subs.cljs

Trying to accommodate both the simple:

(regsub :showing
  (fn [db _]       
    (:showing db)))

And the more complex:

(regsub :footer-stats                   
  :<- [:todos]
  :<- [:completed-count]
  (fn [[todos completed] _]
    [(- (count todos) completed) completed]))

@mike-thompson-day8
Copy link
Contributor Author

Suggestion from @escherize via slack:

(regsub :footer-stats                    
  :inputs {:todos  (subscribe [:todos])
           :completed  (subscribe [:completed-count])}
  (fn [{:keys [todos completed-count]}  _]
    [(- (count todos) completed) completed]))

@rasom
Copy link

rasom commented Jun 10, 2016

@mike-thompson-day8, :<- looks a bit weird, and I don't understand how this syntax handles Remember a subscription can use a value in the query-v. Actually, any syntax where input subscriptions are defined before query descructuring doesn't seem too clear for me...

@escherize
Copy link

After some reflection, I think that this version is my favorite fwiw.

(regsub :footer-stats
  :<- {:todos     (subscribe [:todos])
       :completed (subscribe [:completed-count])}
  (fn [{:keys [todos completed]} _]
    [(- (count todos) completed) completed]))
  1. It looks like clojure, and the idea that todos and completed are inputs is very clear.
  2. As long as the query-v for the subscriptions right after :<- are static, it works just fine there.
  3. If dynamic subscriptions are nessicary... then maybe this makes sense? (not sure..)
(regsub :footer-stats
  :<- {:todos     (subscribe [:todos "apple"])
       :completed (subscribe [:completed-count @(subscribe [:order-by])])}
  (fn [{:keys [todos completed]} _]
    [(- (count todos) completed) completed]))

I'm not sure if I've addressed your objections though, @rasom.

@rasom
Copy link

rasom commented Jun 10, 2016

@escherize, for example

(register-sub :something
  (fn [db [_ param]]
    (let [something-other (subscribe [:something-other param])]
      (reaction (some-magic (get-in @db [:something param]) @something-other)))))

I mean... Where we will get that param from when we will define input subscriptions inside :<-?

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jun 10, 2016

Okay, after a week of on-again off-again struggle, a good solution turns out to be embarrassingly easy. How did I not see this before?
https://gist.github.com/mike-thompson-day8/9481b281584659d066cacc25a955ac62

So, I'm still not 100% sure about the whole :<- sugar thing (I've left it in for further comment), but that has now taken 2nd place behind a much, much more solid concept.

@escherize
Copy link

Using a function to setup the input signals to the subscription looks good, @mike-thompson-day8.

I was chatting with @gadfly361, and he had an interesting idea for defsub. Basically, his approach is to use the same approach as reagent.core/create-class. Meaning use a map to label the inputs, subscription function, and whatever else is important. There are a few good reasons to use this: it's semantically intuitive, easier to add to and augment later, and doesn't rely on ordering artifacts that actually have names. It boils down to explicit is better than implicit.

@andreasthoelke
Copy link

andreasthoelke commented Jun 12, 2016

@mike-thompson-day8 Thank you for bringing this up for discussion! The proposed changes
(https://gist.github.com/mike-thompson-day8/9481b281584659d066cacc25a955ac62) look great!
e.g.

(regsub :footer-stats
   :<- [:todos]
   :<- [:completed-count]
   (fn [[todos completed] _]
     [(- (count todos) completed) completed]))

I think the macro syntax does in fact facilitate seeing subscriptions more as derivations of other nodes/signals in a graph, rather than mostly (reactive) queries on the db. I think this concept is clear in the ‘first function’ approach a well:

(regsub :footer-stats                      
  (fn [query-v _]
    [(subscribe [:todos])
     (subscribe [:completed-count])])
  (fn [[todos completed] _]
    [(- (count todos) completed) completed]))

But it’s more obscured in the syntax here. Specifically the relation of the returned vector of signals in the first function, to the destructured first arg in the second fn is more difficult to recognize and easier to get wrong. The user also needs to remember that query-v is the first arg in the first fn and second arg in the second fn – not intuitive. I think in comparison the macro syntax will be significantly easier to learn and to remember and will be easier to read and to edit/maintain.

As we are already employing a macro to make subscriptions more user friendly, I was thinking if we should consider to even go a few steps further:

Say I want to subscribe to footer-stats passing a param (which will slightly exaggerate my stats):

(subscribe [:footer-stats 1.2])

(regsub :footer-stats
  :<- [:todos]
  :<- [:completed-count]
  (fn [[todos completed] [_ param]]
    [(- (count todos) completed) (* completed param)]))

Problems:

  • I have to remember how I receive the param: [_ param] in the second fn arg. I'm often mixing this up with event handlers which allow using the trim middleware to get rid of the mostly obsolete first param.
  • I have learned this syntax: (subscribe [:todos]), yet when I saw :<- [:todos] was wondering
    why the subscription ID is written in a vector here. It would be easier to guess/recognize that this is the
    subscription vector if there were params in the vector. Still I'd say in 60% of the cases subscriptions don't have params, and in these cases the vector in the macro syntax looks a bit confusing/redundant.
  • Related to the subscription vector and the destructuring we see 10 angle brackets in 3 lines (lines 2 to 4), still accumulating some visual noise.
  • With this syntax it doesn't seem plausible to 'pass on' subscription parameters to
    'sub subscriptions', e.g. do :<- [:todos param] with the param passed to :footer-stats.

Here is a slightly modified syntax that attempts to address these issues:

(regsub :footer-stats [param]
  :< :todos
  :< :completed-count
  (fn [todos completed param]
    [(- (count todos) completed) (* completed param)]))
  • Arguments to the suscription are listed in an argument vector, right after the suscriptions ID, similar to clojure function definitions
  • The arguments to the computation function are [sub-1, .. sub-n, param-1, .. param-n]. No destructuring.

This would also allow to 'pass on' subscription arguments to the 'sub subscriptions' like this:

(regsub :visible-todos [limit]
  :< [:todos limit]
  :< :showing
  (fn [todos showing _]
    (let [filter-fn (case showing
                      :done   :done
                      :all    identity)]
      (filter filter-fn todos))))

Instead of writing

(regsub :visible-todos [limit]
  :< :todos
  :< :showing
  (fn [todos showing limit]
    (let [filter-fn (case showing
                      :done   :done
                      :all    identity)]
      (->> (filter filter-fn todos)
           (take limit)))))

which would (theoretically) be less efficient.

By omitting the vector around the sub-subscription ids, omitting the additional dash and omitting the destructuring, I believe the syntax gets easier to scan/read and comprehend:

(regsub :visible-todos [limit]
  :< :todos
  :< :showing
  (fn [todos showing limit]
    ..))

vs. the original proposal:

(regsub :visible-todos
  :<- [:todos]
  :<- [:showing]
  (fn [[todos showing] [_ limit]]
    ..))

Interestingly, mixing the vector form (when params are passed) with the non vector forms appears intuitive to me (interested in what others think of cause!):

  :< [:todos limit]
  :< :showing

Small thing: I did not recognize this :<- as an arrow pointing left at first, rather saw it as the frightened monsters from pacman. :< also indicates that something is pulled in and then passed into the function, while it's saving one character/a bit of noise. (can still be seen as a sad monster however)

Lastly, I'd suggest to go all-in with the new regsub syntax and make the input signal declaration mandatory:

(regsub :showing []
  :< :db
  (fn [db]
    (:showing db)))

I think especially beginners will appreciate the consistency in how regsub appears in code: The input signals are always defined and are always matching the first args in the function signature. (the :db signal is available by default.)

In the current abbreviated (one function) form, the user has to know that in this case db is provided in the first arg:

(regsub :showing []
  (fn [db]
    (:showing db)))

It's visually quite different/inconsistent from the explicit signal listing form and may therefore be a slightly confusing.

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jun 14, 2016

@andreasthoelke thanks for that very detailed write up. I've read through it a few times fairly carefully.

I have already wrestled with virtually all the proposals you make. For example, I've been backwards and forwards a few times over how to present signals to the computation function. You suggest positional arguments, rather than a data structure (vector?) as the first parameter.

But I've gone with a non positional approach because some may prefer to do this:

(regsub :footer-stats                      
  (fn [query-v _]
    {:todos (subscribe [:todos])
     :completed (subscribe [:completed-count])})
  (fn [{:keys [todos completed]} _]
    [(- (count todos) completed) completed]))

I've also agonized over the whole destructing of query params in a way that allows their access in subscriptions :< [:todos limit]. But every "solution" seemed to add lots of when this then that to explanations about what was happening, which is never good.

I must admit, I'm pretty happy now with the current approach.

In fact, I'm in half a mind to not even allow for the sugar. There's something very consistent and right about the signal fn. The extra sugar, if it is to be allowed, will just be made available for the simple cases, which you estimate is 60% of the time.

I do like stuff minimal, so I'm thinking seriously about switching to :<

@danielcompton
Copy link
Contributor

danielcompton commented Jun 14, 2016

The extra sugar, if it is to be allowed, will just be made available for the simple cases, which you estimate is 60% of the time.

Looking through our codebase, I'd estimate that 99.9%* of our subscriptions don't use subscription parameters, so the syntactic sugar would be able to be used almost everywhere (I think?).

* not really an estimate

@kirked
Copy link

kirked commented Jun 16, 2016

I wonder about the need for multiple :< or :<- keywords. Why not just a vector-of-vectors:

(regsub :visible-todos
  :<- [[:todos] [:showing]]
  (fn [[todos showing] [_ limit]]
    ..))

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jun 27, 2016

This code is now in the develop branch. The todomvc example has been changed across to use regsub.

@arichiardi
Copy link

arichiardi commented Jul 5, 2016

@mike-thompson-day8 I am thinking of putting the new reg-sub in a project, maybe we can cut an alpha release?

I saw the todo app and it looks very neat (there is a typo, the sub fn is called register-pure-sub instead of reg-sub in the comments), so I might give it a go. The code base is very little at the moment so it is the right time to do it.

@mike-thompson-day8
Copy link
Contributor Author

Sorry, yes, I've been progressing slowly on this. Can you give me two more days please and I'll cut an alpha? In the meantime, you can of course, grab develop and lein install it into your local maven, if you absolutely have to have it sooner.

@vspinu
Copy link

vspinu commented Jul 6, 2016

Here is a newbie prospective who has been with clojurescript and re-frame for just a couple of days.

I am continuously tempted to abandon re-frame patterns for two simple reasons. First is the annoying repetition that (register-sub ...) and (subscribe ...) bring along. Most of my subs are basic extractions like this

(register-sub :user-ids
  (fn [db]
    (reaction (get @db :user-ids []))))

The simple alternative of using (reaction (:user-ids @db)) directly in views.clj is magnitudes simpler and very tempting indeed. Second reason is that the whole paradigm uses non-standard dispatch mechanisms - (subscribe [:id]) and (dispatch [:id ...]) instead of straightforward function calls.

So here is the proposal. Why not make defsub (and defhandler) look and behave like standard function definitions that would create both the actual subscription and the subcription invocation? Something along the following lines:

(defsub sorted-items []
   (sort-by @(sort-attr) @(items))))

The above would register the subscription function like this

(re-frame.core/register-sub
  :sorted-items
  (fn [db [_]]
    (let [items     (items)           ;; defsub-ed before
          sort-attr  (sort-attr))]     ;; defsub-ed before
      (reaction (sort-by @sort-attr @items))))) 

It also would define the actual subscription invocation function sorted-items

(defn sorted-items []
   (subscribe [:sorted-items]))

So no more awkward (subscribe [:sorted-items]) but direct call to (sorted-items) that ordinary people could understand. You can call it in views.clj or in other defsubs just as you would do with normal functions.

The above code assumes that defsub can identify which calls are subscription invocations (@(sort-attr) and @(items)) and which are standard functions. That could be done either by attaching metadata to the subscription invocation function (sorted-items) or looking into the database of already registered subscriptions. This also would address the redundancy issue. The only issue is that you would need to define subscription in order of their usage, just as with normal clojure functions.

In any case, it looks to me that, explicit declaration of subscriptions in defsub as in all proposed DSLs could be entirely avoided or at least be made optional.

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jul 6, 2016

@vspinu Both dispatch and subscribe are more than function calls, each in a different way. For a start dispatch implements an asynchronous function call, with middleware. That's critical. Second subscribe implements a reactive function call, with deduplication (coming in v0.8.0).

Remember also, when you "invoke functions" via dispatch and subscribe you are down the path to "programming with data". There are many nice implications.

If your application is simple, then sure abandon re-frame - almost anything will work. But, if your application is a bit more complex, you'll be glad you didn't.

@vspinu
Copy link

vspinu commented Jul 6, 2016

Both dispatch and subscribe are more than function calls, each in a different way.

Sure, I was not suggesting that. Both are not pure functions but I would still prefer to pass #(store-user-ids %) instead of #(dispatch [:store-user-ids %]), and get reactions with (user-ids param) instead of (subscribe [:user-ids param]). And if this can lead to getting rid of the explicit declaration of the signals in defsub and defhandler DSLs, even better.

@mike-thompson-day8
Copy link
Contributor Author

mike-thompson-day8 commented Jul 7, 2016

@arichiardi I've cut a version 0.8.0-alpha1. Will be in clojars shortly.
I'm cautious because this really is alpha quality. But you did ask.
There are breaking changes. See:
https://github.com/Day8/re-frame/blob/master/CHANGES.md#080--201607xxxx
Be aware that things marked with XXX are not yet complete (hence alpha).

@arichiardi
Copy link

arichiardi commented Jul 7, 2016

That's awesome, I think I can handle the changes no problem, the new features are super cool ;)

@arichiardi
Copy link

arichiardi commented Jul 7, 2016

Not related to re-frame but I just saw what puts me off for now. Maybe it is not relevant for an alpha, but it might be good to track: http://dev.clojure.org/jira/browse/CLJS-1701

@mike-thompson-day8
Copy link
Contributor Author

@arichiardi CLJS-1701 shouldn't be an issue for re-frame. I've held off involving spec. .

@rtomsick
Copy link

rtomsick commented Jul 11, 2016

How sure are we that query de-dup is going to be a thing in re-frame's future?

I know API breakage is on the cards, as are bugs (given the alpha), but is it confirmed that the de-duplication concept is here to stay?

I ask because right now I'm struggling with incorporating de-duplication as mentioned in Subscribing-To-A-Database . Lots of using counter atoms to prevent firing off multiple remote queries just because a query reaction is de-refd multiple times, etc.

I love the de-dup that's in 0.8.0-alpha1 so far, as it's a great way to minimize net traffic in the face of laziness, but I don't want to start depending on it and then having it go away. (Totally within your rights, given the whole alpha thing!)

(BTW, thanks for such an awesome framework pattern way of thinking about applications set of revelations. I really dig re-frame.)

@danielcompton
Copy link
Contributor

@rtomsick, it's very unlikely query dedup is going away, barring major issues. I wasn't clear from your post, are you having issues with re-frames query dedup in the alpha, or implementing your own dedup?

@worrel
Copy link

worrel commented Aug 2, 2016

@vspinu not necessarily a permanent solution, but for repetitious simple db gets, I created a single :raw-query subscription that takes a vector param which is passed to (get-in) on the app-db. This has been great for quick development. Searching the codebase for the paths allows you to identify any re-used :raw-query's that deserve being converted into explicit subscriptions. You could make it more 'secure' by applying a filter to the paths in the :raw-query subscriptions itself to restrict access to parts of the db.

@vspinu
Copy link

vspinu commented Aug 2, 2016

but for repetitious simple db gets, I created a single :raw-query subscription that takes a vector param which is passed to (get-in) on the app-db.

Yes. I did exactly that. It might seem natural, but it wasn't so obvious to me when I started with re-frame. The following lines essentially removed 80% of my sub and dispatch querying code:

(re-frame.core/register-sub :db-get-in
  (fn [db [_ & ks]]
    (reagent.ratom/reaction (get-in @db ks))))

(defn db-sub [ks]
  (re-frame.core/subscribe (into [:db-get-in] ks)))

(re-frame.core/register-handler :db-assoc-in
  (fn [db [_ ks val]]
    (assoc-in db ks val)))

(defn db-assoc-in [ks val]
  (re-frame.core/dispatch [:db-assoc-in ks val]))

@danielcompton
Copy link
Contributor

Those handlers and subscriptions will reduce the number of subscriptions and handlers you have to write, at the cost of tightly coupling your application to the structure of app-db. The idea of subscriptions and handlers is to separate the what from the where. If you need to change the structure of app-db for some reason, then you will need to go through all of your view code, subscriptions, and handlers to makes sure you've updated those places that depend on it.

If you make more specific handlers and subscriptions, then you can refactor app-db without a lot of code churn. A middle ground we've sometime used is to create handlers to update parts of an app-db, e.g. :messages-update which has an assoc-in starting from the :messages key. That way, you have some degree of isolation.

@newhook
Copy link

newhook commented Aug 5, 2016

What does the subscription in https://github.com/Day8/re-frame/wiki/Subscribing-To-A-Database look like with reg-sub? I've been playing with that for an hour or so now and haven't worked out the right magics.

@mike-thompson-day8
Copy link
Contributor Author

@newhook I haven't had time to think that through. So I'd just continue to use the same pattern described in that Wiki page, via reg-sub-raw

@rtomsick
Copy link

rtomsick commented Aug 6, 2016

@danielcompton I'm not having any issues with the alpha, but I'm trying to figure out how long-lived my own de-dup code will be. 😄 If it looks like de-dup is here to stay then I won't worry about implementation cleanliness (since I'll probably see a new release of re-frame before I ship).

Thanks!

@mike-thompson-day8
Copy link
Contributor Author

STATUS:

We're getting close to a release of v0.8.0.
The various alpha releases (in clojars) fully implement the new reg-sub. We've been using the new subscriptions feature in production, and it is working out nicely, so I can see no further changes likely. It is almost to the point I could close this issue - i think we are there.

But I won't because good docs are still pending. Currently the best explanation is:
https://github.com/Day8/re-frame/blob/develop/examples/todomvc/src/todomvc/subs.cljs

@mike-thompson-day8
Copy link
Contributor Author

Released as part of v0.8.0. Although docs are still lacking.

At this point, todomvc tutotial continues to be the best soruce of information:
https://github.com/Day8/re-frame/blob/develop/examples/todomvc/src/todomvc/subs.cljs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests