Fork me on GitHub
#xtdb
<
2020-07-27
>
lgessler23:07:00

hi, i wrote a macro for the new https://github.com/juxt/crux/releases feature and was curious if anyone had any critiques. nesting the code in replies

🙂 3
lgessler23:07:08

here it is--it accepts a name for the txfn, a crux node ref, and a txfn, and submits the fn to the node and also defines a function for invoking it:

(defmacro deftx [name node tx-fn]
  "Defines a transaction function on the given node."
  (let [kwd-name (keyword (str *ns*) (str name))]
    `(do
       (crux/submit-tx ~node [[:crux.tx/put {:crux.db/id ~kwd-name
                                             :crux.db/fn (quote ~tx-fn)}]])
       (defn ~name [& ~'args]
         (crux/submit-tx ~node (log/spy [(into [:crux.tx/fn ~kwd-name] ~'args)]))))))
for the ivan example linked above, instead of this
(crux/submit-tx node [[:crux.tx/put {:crux.db/id :increment-age
                                     ;; note that the function body is quoted.
                                     ;; and function calls are fully qualified
                                     :crux.db/fn '(fn [ctx eid]
                                                    (let [db (crux.api/db ctx)
                                                          entity (crux.api/entity db eid)]
                                                      [[:crux.tx/put (update entity :age inc)]]))}]])

(crux/submit-tx node [[:crux.tx/fn :increment-age :ivan]])
you would write this:
(deftx increment-age node 
  (fn [ctx eid]
     (let [db (crux.api/db ctx)                        
           entity (crux.api/entity db eid)]
       [[:crux.tx/put (update entity :age inc)]])))

(increment-age :ivan)

lgessler23:07:09

i've only just started using crux today so i wonder if this is actually a good idea... but it seemed like a good way to tame the slight verbosity of using txfns. is there anything obviously bad about it? obviously a result of calling deftx is that the txfn is defined on the crux node (and this will happen every time we launch our application!) but in every scenario i can think of this is harmless--just a bit of noise on the transaction log.

Darin Douglass23:07:07

could your macro allow something like this instead (this is purely from a usability standpoint, as i haven't used txfns much):

(deftx increment-age node
  [db eid]
  (let [entity (crux.api/entity db eid]
    [[:crux.tx/put (update entity :age inc]]))
this would feel much more function-like and IMO less prone to forgetting to quote the main fn. if your use-case always calls crux.api/entity you could even make the bindings [db entity] and do the entity query in the automatically in the macro. (maybe also include the eid value? again i haven't seen much more of these fn's than i've read here in slack)

lgessler23:07:22

oops, the macro actually doesn't need the (fn ...) to be quoted, fixed that. i like your syntax suggestion, that's definitely more ergonomic

lgessler23:07:56

and yeah, you're right--if this is going to be used for a single entity most of the time it's still a little boilerplatey. i'm going to see if i can handle that with another macro

Darin Douglass23:07:05

quick update while i was noodling on it:

(defmacro deftx [name node bindings & body]
                   "Defines a transaction function on the given node."
                   (let [kwd-name (keyword (str *ns*) (str name))]
                     `(do
                        (crux/submit-tx ~node [[:crux.tx/put {:crux.db/id ~kwd-name
                                                              :crux.db/fn (quote (fn [ctx# eid#]
                                                                                   (let [~bindings [(crux.api/db ctx#) eid#]]
                                                                                     ~@body)))}]])
                        (defn ~name [& ~'args]
                          (crux/submit-tx ~node (log/spy [(into [:crux.tx/fn ~kwd-name] ~'args)]))))))

Darin Douglass23:07:45

(macroexpand '(deftx abc node [ctx db-id] [[:crux.tx/put :a]]))
(do
 (crux/submit-tx
  node
  [[:crux.tx/put
    #:crux.db{:fn
              '(clojure.core/fn
                [ctx__32266__auto__ eid__32267__auto__]
                (clojure.core/let
                 [[ctx db-id]
                  [(crux.api/db ctx__32266__auto__)
                   eid__32267__auto__]]
                 [[:crux.tx/put :a]])),
              :id :user/abc}]])
 (clojure.core/defn
  abc
  [& args]
  (crux/submit-tx
   node
   (log/spy
    [(clojure.core/into [:crux.tx/fn :user/abc] args)]))))

refset00:07:58

I am no expert on the pitfalls of macros, but these ideas are certainly interesting to me - thanks for sharing!

🚀 3
lgessler01:07:16

nice @UGRJKK74Y i like those! I took your advice on the bindings & body approach and i wrote a separate macro for the simple "set on an entity" case:

(defmacro deftx [name node bindings & body]
  "Defines a transaction function on the given node."
  (let [kwd-name (keyword (str *ns*) (str name))]
    `(do
       (crux/submit-tx ~node [[:crux.tx/put {:crux.db/id ~kwd-name
                                             :crux.db/fn (quote (fn ~bindings
                                                                  ~@body))}]])
       (defn ~name [& ~'args]
         (crux/submit-tx ~node (log/spy [(into [:crux.tx/fn ~kwd-name] ~'args)]))))))

(defmacro defsetter [name node attr]
  "Shortcut for simple sets on a single attribute"
  `(deftx
     ~name
     ~node
     [ctx# eid# val#]
     (when-let [entity# (crux.api/entity (crux.api/db ctx#) eid#)]
       [[:crux.tx/put (assoc entity# ~attr val#)]])))

lgessler01:07:31

and now you can just (defsetter set-age node :age)

lgessler01:07:32

the main thing i want to change about this is that node essentially requires requires you to feed it a global reference--tomorrow i'm going to rewrite these somehow so calling deftx is non-side effecting, node is removed from its arguments, and its return value can be explicitly registered with a crux node

lgessler01:07:05

(in my project i'm using mount to manage my crux node, which means that when i require a ns with a deftx the node might not have been started yet)

lgessler02:07:01

this is all getting to be a little complicated--I started going down this path so I could avoid put race conditions, but they're rare and relatively inconsequential in my application so i might just bite the bullet

ordnungswidrig08:07:57

Please be aware that for each evaluation of deftx the tx function will be put to the database. Notable during development when you reload the ns but also everythime your applicaton starts and loads the namespace. You maybe want to use a cas operation to only update the tx-function when it actually has changed.

👍 3
lgessler06:07:38

thought about this some more, there are two things that need to happen here: there needs to be a callable function invoking the tx fn, and the db needs to be told somehow what the tx fn is. for the former we can just assume the db exists and bind that invoking function to the deftx var, and for the latter we can attach a (fn [crux-node] ...) to that var's metadata that when invoke will install the tx fn in the crux node. then you can write an install tx fns function separately that can walk a namespace and install any tx fns it finds in var metadata. this allows the deftx to be written without any reference to a crux node for the small price of listing out the namespaces to be scanned at the point where we initialize the crux node