CoNarrative / precept

A declarative programming framework
MIT License
657 stars 33 forks source link

Maintain and generate lists at API level #45

Closed alex-dixon closed 7 years ago

alex-dixon commented 7 years ago

We should be able to have an api for the following:

(def-tuple-rule create-list-of-visible-todos
  {:group :report}
  [?eids <- (by-fact-id :e) :from [:todo/visible]]
  [:test (seq ?eids)]
  =>
  (trace "List!" ?eids)
  (insert! [(guid) :todos/by-last-modified*order ?eids])
  (doseq [x ?eids]
    (insert! [(guid) :todos/by-last-modified*eid x])))

(def-tuple-rule update-list-of-visible-todos
  {:group :report}
  [[_ :todos/by-last-modified*eid ?e]]
  [?entity <- (acc/all) :from [?e :all]] ;; TODO. Can substitute entity
  =>
  (trace "Entity list!" ?entity)
  (insert! [(guid) :todos/by-last-modified*item ?entity]))

(defsub :task-list
  [[_ :todos/by-last-modified*order ?eids]]
  [?items <- (acc/all :v) :from [:todos/by-last-modified*item]]
  [[_ :active-count ?active-count]]
  [:test (seq ?eids)]
  =>
  (let [items (group-by :e (flatten ?items))
        ordered (vals (select-keys items (into [] ?eids)))
        entities (util/entity-Tuples->entity-maps ordered)]
    (trace "Entities" entities)
    {:visible-todos entities
     :all-complete? (= 0 ?active-count)}))

The end result is a list of entities in a specified order. We could have rules that do this under the covers and surface the resulting list as a fact.

alex-dixon commented 7 years ago

This feature appears to require a rule to return one or more generated rules in addition to itself.

(defsub :task-list
  [(<- ?ordered-visible-todos (entities (by-fact-id :e) :from [:todo/visible]))]
  [[_ :active-count ?active-count]]
  =>
  {:visible-todos ?ordered-visible-todos
   :active-count ?active-count})

Note we can hone the syntax for kicking off list generation as we go along. Provided is just an best example I've come up with so far.

entities could signal the generation of the following rules:

(def-tuple-rule create-list-of-visible-todos
  {:group :report}
  [?eids <- (by-fact-id :e) :from [:todo/visible]]
  [:test (seq ?eids)]
  =>
  (insert! [(guid) :todos/by-last-modified*order ?eids])
  (doseq [x ?eids]
    (insert! [(guid) :todos/by-last-modified*eid x])))

(def-tuple-rule update-list-of-visible-todos
  {:group :report}
  [[_ :todos/by-last-modified*eid ?e]]
  [(<- ?entity (entity ?e))]
  =>
  (trace "Entity list!" ?entity)
  (insert! [(guid) :todos/by-last-modified*item ?entity]))

(defsub :task-list
  [[_ :todos/by-last-modified*order ?eids]]
  [?items <- (acc/all :v) :from [:todos/by-last-modified*item]]
  [[_ :active-count ?active-count]]
  [:test (seq ?eids)]
  =>
  (let [items (group-by :e (flatten ?items))
        ordered (vals (select-keys items (into [] ?eids)))
        entities (util/entity-Tuples->entity-maps ordered)]
    (trace "Entities" entities)
    {:visible-todos entities
     :all-complete? (= 0 ?active-count)}))

This should still allow users to insert the list as a fact like so:

(def-tuple-rule list-visible-todos-by-last-modified
  [(<- ?ordered-visible-todos (entities (by-fact-id :e) :from [:todo/visible]))]
  =>
 (insert! [(guid) :todos/by-last-modified ?ordered-visible-todos]))

The biggest challenge seems to be dynamic rule generation in Clojurescript. We've already had problems with more complex DSL implementations (#40). It also appears there's no intern.

It might be beneficial to borrow liberally from mfikes' Planck library, in particular these functions:

alex-dixon commented 7 years ago

First pass at how we might implement entities:

;; Usage:

;; User writes:
(rule my-rule
  [?eids <- (acc/all :e) :from [:interesting-fact]] ;; Maybe new special form `(eids [:interesting-fact])`
  [(<- ?interesting-entities (entities ?eids))]
  =>
  ;; Prints list of Tuples
  (println "Found entities with interesting fact" ?interesting-entities))

;; We generate:

;; A rule that contains the accumulator expression with the binding provided in `(entities)`
(rule my-rule___split-0
  [?eids <- (acc/all :e) :from [:interesting-fact]
   =>
   (let [req-id (guid)]
          gen-fact-req [req-id ::gen-fact/request :entities]
          _ (swap! state/generated-facts assoc req-id {:req gen-fact-req :rule-name ???})
     (insert! gen-fact-req)
     (insert! [req-id :entities/order ?eids])
     (doseq [eid ?eids] (insert! [req-id :entities/eid ?eid])))])

;: And the original, rewritten to match on the ::gen-fact/response, keeping the same name
(rule my-rule
  ;; ...rest LHS
  ;; Want to remove acc expr (it exists in a genned rule), 
  ;; but then no access to its bound variable inside this rule...
  [?eids <- (acc/all :e) :from [:interesting-fact]] 
  ;; Replaces `entities` on same "line" as original
   [[req-id ::gen-fact/response ?interesting-entities]] 
   =>
   ;; ...original RHS

;; precept.rules.impl:

;; These are "always on", waiting for an :entity request

(rule entities___impl-a
  [[?req ::gen-fact/request :entities]]
  [[?req :entities/eid ?e]]
  [[(<- ?entity (entity ?e))]]
 =>
 (insert! [?req :entities/entity ?e]))

(rule entities___impl-b
  [[?req :entities/order ?eids]]
  [?ents <- (acc/all :v) :from [?req :entities/entity]]
 =>
  (let [items (group-by :e (flatten ?ents))
        ordered (vals (select-keys items (into [] ?eids)))
 (insert! [?req ::gen-fact/response ordered]))
alex-dixon commented 7 years ago

Implemented very closely to the outline above in #57. 🎉