{"action":"created","issue":{"url":"https://api.github.com/repos/Day8/re-frame/issues/170","repository_url":"https://api.github.com/repos/Day8/re-frame","labels_url":"https://api.github.com/repos/Day8/re-frame/issues/170/labels{/name}","comments_url":"https://api.github.com/repos/Day8/re-frame/issues/170/comments","events_url":"https://api.github.com/repos/Day8/re-frame/issues/170/events","html_url":"https://github.com/Day8/re-frame/issues/170","id":158702492,"number":170,"title":"Better re-frame subscriptions - opinions/ideas requested ","user":{"login":"mike-thompson-day8","id":3033376,"avatar_url":"https://avatars.githubusercontent.com/u/3033376?v=3","gravatar_id":"","url":"https://api.github.com/users/mike-thompson-day8","html_url":"https://github.com/mike-thompson-day8","followers_url":"https://api.github.com/users/mike-thompson-day8/followers","following_url":"https://api.github.com/users/mike-thompson-day8/following{/other_user}","gists_url":"https://api.github.com/users/mike-thompson-day8/gists{/gist_id}","starred_url":"https://api.github.com/users/mike-thompson-day8/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mike-thompson-day8/subscriptions","organizations_url":"https://api.github.com/users/mike-thompson-day8/orgs","repos_url":"https://api.github.com/users/mike-thompson-day8/repos","events_url":"https://api.github.com/users/mike-thompson-day8/events{/privacy}","received_events_url":"https://api.github.com/users/mike-thompson-day8/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":20,"created_at":"2016-06-06T15:13:57Z","updated_at":"2016-06-16T20:37:40Z","closed_at":null,"body":"### Better re-frame subscriptions\r\n\r\nI'd like to pour some macro sugar on `re-frame.core/register-sub`. I'd like your feedback and ideas. \r\n\r\n### Naming\r\n\r\nFirst question: what should we call this macro?   `defsub`, `def-sub`, `defs`?  Other?  Below, I assume `defsub`.\r\n\r\n### The Goals\r\n\r\nSolve these current problems: \r\n  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.\r\n  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)\r\n  \r\nImprove by adding:\r\n  3.  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. \r\n  4.  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.\r\n  5.  To assist in debugging:\r\n      - provide a mechanism to trace subscriptions activity to js/console.  Trace when they are created, and rerun. \r\n      - provide a mechanism to measure subscriptions.  How many times has a subscription been rerun?  Accumulated time.  \r\n\r\nOther goals:\r\n  6. needs to be backwards compatible. Ie. `re-frame.core/register-sub` should still just work.\r\n  7. 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.\r\n  8. 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. \r\n\r\nAnd, what's going on is:\r\n   A. There's a pure function \r\n   B. it performs computation using the values from:\r\n       - one or more \"input signals\" \r\n       - data from a query vector\r\n       - zero or more dynamic query parameters\r\n\r\nQuestion: are there other considerations/goals I've missed?  \r\n\r\n### Backgrounder\r\n\r\nThis ticket is about subscription definition, but let's remind ourselves about use: \r\n```\r\n   (subscribe [:query-id 1 2 3] [r1 r2])\r\n```\r\n\r\nThe 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. \r\n\r\nMost subscriptions only involve one parameter.  Many re-framers don';t even know about the 2nd. \r\n\r\n### What `defsub` use might look like\r\n\r\nA super simple example:\r\n```clj\r\n(defsub :items\r\n  (fn [db v]\r\n    (:items db))\r\n```\r\n\r\nNotice:\r\n  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. \r\n  2.  The second parameter is a handler `fn` of the kind normally given to `re-frame.core/register-sub` except:\r\n    - `db` is a value, not a `reagent.core/atom`\r\n    - it returns a value and not a `reagent.ratom/reaction`\r\n    - so a pure function\r\n\r\nPoint 2 is 50% of the battle. \r\n\r\nQuestion:  does this variation work better for you? \r\n```clj\r\n(defsub :items\r\n :fn [db _]          ;; using an `:fn` keyword\r\n     (:items db))\r\n```\r\n\r\nOr maybe pare it right back to this:\r\n```clj\r\n(defsub :items\r\n  [db _]            ;; no mention of fn\r\n  (:items db))\r\n```\r\nThis approach is lovely for simple cases. But, it could be problematic as we get into more complex cases. Read on. \r\n\r\n### Return Spec Checking\r\n\r\n`defsub` can also take a registered `cljs.spec` which describes the structure of the value returned by the handler `fn`.\r\n\r\n```clj\r\n(defsub :items\r\n :ret-spec  :info-model/items\r\n (fn [db _]\r\n     (:items db)))\r\n```\r\n\r\n### Signal Graphs\r\n\r\nre-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.  \r\n\r\nSo we want to support that process.\r\n\r\nHere's a complex registration from the existing re-frame README:\r\n```clj\r\n(re-frame.core/register-sub\r\n :sorted-items             ;; the query id\r\n (fn [db [_]]\r\n   (let [items      (reaction (get-in @db [:some :path :to :items]))  ;; reaction #1\r\n         sort-attr  (reaction (get-in @db [:sort-by]))]                ;; reaction #2\r\n       (reaction (sort-by @sort-attr @items)))))                       ;; reaction #3\r\n```\r\n\r\nThis 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 `reaction`s are used elsewhere they will run as duplicates. \r\n\r\nSo, current best practice is to rewrite it like this:\r\n```clj\r\n(re-frame.core/register-sub\r\n  :sorted-items\r\n  (fn [db [_]]\r\n    (let [items      (subscribe [:items])       ;; was a reaction, now a subscription \r\n          sort-attr  (subscribe [:sort-by]))]     ;; was a reaction, now a subscription \r\n      (reaction (sort-by @sort-attr @items)))))    ;; reaction #3 (hasn't changed)\r\n```\r\n\r\nBetter!! 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. \r\n\r\nAnd, even if that wasn't a problem, we don't want `reaction` used explicitly any more.\r\n\r\nSo, here's how it might look with `defsub`:\r\n```clj\r\n(defsub :sorted-items\r\n :ret-spec  :info-model/items\r\n :subscribe [:items]       ;; assumes we've already created this subscription\r\n :subscribe [:sort-by]     ;; ditto\r\n (fn [items sort-attr query-v]\r\n     (sort-by sort-attr items)))\r\n```\r\n\r\nNote: \r\n\r\n1. 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`\r\n2. The **values** from these N subscriptions are provided to the `fn` positionally as the first N parameters.\r\n3. The `fn` is still pure. Values in, values out. No side effects. \r\n\r\n\r\n**Or** perhaps this variation is better:\r\n```clj\r\n(defsub :sorted-items\r\n :ret-spec  :info-model/items\r\n :subscribe [:items]       \r\n :subscribe [:sort-by]  \r\n :fn [[items sort-attr] query-v]   ;; using :fn and a 1st parameter vector\r\n     (sort-by sort-attr items)))\r\n```\r\n\r\n**OR** maybe we do awayaway with the `fn` formulation (args etc): \r\n```clj\r\n(defsub  :sorted-items\r\n :ret-spec  :info-model/items\r\n :subscribe items [:items]        ;; :subscribe, symbol and then query vector\r\n :subscribe sort-attr [:sort-by]\r\n :query [_ a b]                     ;; destructure the query vector, if those values are used\r\n (sort-by sort-attr items))        ;; don't even bother with whole `fn` args thing, just provide the fn body.\r\n```\r\n\r\nbut this latest one makes the simple case a bit hard:\r\n```clj\r\n(defsub :items\r\n  (:items db))        ;;  without fn \"args\" how to I name \"db\"\r\n```\r\nmaybe I have to do this:\r\n```clj\r\n(defsub :items\r\n  :subscribe db [:/] \r\n  (:items db))        ;;  now db is explicitly named\r\n```\r\n\r\nHmm.  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.\r\n\r\n**OR** maybe we prefer \r\n\r\n```clj\r\n(defsub :sorted-items\r\n :ret-spec  :info-model/items\r\n :fn   sorted-items                     \r\n :args  items      :from  :subscribe [:items]\r\n        sort-attr  :from  :subscribe [:sort-by]  \r\n        [_ a b c]  :from  :query-v            \r\n :body (sort-by sort-attr items))\r\n```\r\n\r\nSo which of these appeals?  Other alternatives?\r\n\r\n### The Generated code\r\n\r\nLet's switch gears and, for the moment, forget about DSL aesthetics. \r\n\r\nWhat code will this macro generate?  It will still need to call  `re-frame.core/register-sub`. \r\n\r\n```clj\r\n;; create the pure function\r\n(defn sorted-items             ;; named from the keyword?  Can name be programmer supplied?\r\n  [items sort-attr query-v]    ;; if needed query-v might be [_ a b c]\r\n  (sort-by sort-attr items)))\r\n     \r\n;; register the event with the pure function\r\n(re-frame.core/register-sub\r\n  :sorted-items\r\n  (fn [app-db query-v dyn-v]\r\n    ;; some checks\r\n    (assert (vector? query-v)\r\n    (assert (or (nil? dyn-v) (vector? dyn-v))\r\n    \r\n    ;; some tracing\r\n    (when ^boolean goog.DEBUG (debug \"re-frame: creating subscription: \"  query-v))\r\n    \r\n    ;; gather the input signals\r\n    (let [items     (subscript [:items])\r\n          sort-attr (subscribe [:sort-by])]\r\n          \r\n      ;; now make the rerunnable reaction\r\n      (make-reaction  (fn [] \r\n                        (when ^boolean goog.DEBUG (debug \"re-frame: rerun subscription: \" query-v))\r\n                        (sorted-items @items @sorted-attr query-v (mapv deref dyn-v)))))    ;; perhaps also some monitoring here\r\n```                \r\n                \r\n### Further Thoughts\r\n\r\nProvide a spec for the query-v ? \r\nPerhaps just fspec the handler?\r\nRemember a subscription can use a value in the query-v\r\nRemember a subscription can use the value from a dynamic parameter   `:subscription [:some-q 42] [r]`\r\n"},"comment":{"url":"https://api.github.com/repos/Day8/re-frame/issues/comments/226606381","html_url":"https://github.com/Day8/re-frame/issues/170#issuecomment-226606381","issue_url":"https://api.github.com/repos/Day8/re-frame/issues/170","id":226606381,"user":{"login":"kirked","id":382765,"avatar_url":"https://avatars.githubusercontent.com/u/382765?v=3","gravatar_id":"","url":"https://api.github.com/users/kirked","html_url":"https://github.com/kirked","followers_url":"https://api.github.com/users/kirked/followers","following_url":"https://api.github.com/users/kirked/following{/other_user}","gists_url":"https://api.github.com/users/kirked/gists{/gist_id}","starred_url":"https://api.github.com/users/kirked/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kirked/subscriptions","organizations_url":"https://api.github.com/users/kirked/orgs","repos_url":"https://api.github.com/users/kirked/repos","events_url":"https://api.github.com/users/kirked/events{/privacy}","received_events_url":"https://api.github.com/users/kirked/received_events","type":"User","site_admin":false},"created_at":"2016-06-16T20:37:40Z","updated_at":"2016-06-16T20:37:40Z","body":"I wonder about the need for multiple `:<` or `:<-` keywords. Why not just a vector-of-vectors: \r\n\r\n```clojure\r\n(regsub :visible-todos\r\n  :<- [[:todos] [:showing]]\r\n  (fn [[todos showing] [_ limit]]\r\n    ..))\r\n```"}}
{"actor":{"display_login":"kirked"}}