Fork me on GitHub
#fulcro
<
2021-09-30
>
tony.kay01:09:58

I'm considering an addition to RAD that will help with CLJ code reload. The more you use RAD, the more it gets involved in your resolvers, schema, etc. I use it for generating REST endpoints, etc. On a large project this means that making a change to an attribute might require me to rebuild everything from middleware to schema. But, the attribute is closed over by the list of attributes, and that is closed over by the schema and generated resolvers, etc. The closure means that in order to ensure everything is properly refreshed I often have to reload a lot of namespaces, and end up resorting to tools-ns refresh, which on a large project can take a long time (30s on one project I work on). I avoided the "central atom registry" for the model because using that as a central source of truth is flaky. Renaming something causes the central registry to be wrong, and using the registry means a lot of things have to be derefing atoms all the time (or calling accessor functions for lookup). I like the clarity of having a single def represent a facet of the model...but I don't like the fact that derived things end up closing over that facet in a way that requires code reload. Server restarts are fine...they (re)build things like ID resolvers and middleware based on the model. The problem is all the places where these facets, which in RAD are always represented as maps, are closed over. For example: (def all-attributes (concat person/attributes ...)) which then gets closed over in code in the pathom file (generate-resolvers all-attributes), etc.

tony.kay01:09:21

So, here's the idea: What if I create a registry, but you don't use it directly. The registry thing as an official public artifact causes its own problems. Instead, what if we had a map that looks and acts like an immutable map, but whose central (registered) definition can change on code eval. Using potemkin it is easy to make such a thing:

(ns example
  (:require
    [potemkin.collections :refer [def-map-type]]
    [fulcro-spec.core :refer [specification behavior assertions provided! component when-mocking!]]))

(def registry (atom {}))

(def-map-type RegisteredMap [reg-key]
  (get [this key default-value] (get-in @registry [reg-key key] default-value))
  (assoc [this key value] (assoc (get @registry reg-key) key value))
  (dissoc [this key] (dissoc (get @registry reg-key) key))
  (keys [this] (keys (get @registry reg-key)))
  (meta [this] (meta (get @registry reg-key)))
  (with-meta [this meta] (with-meta (get @registry reg-key) meta)))

(defn registered-map
  "Install the given `value` (a map) as a registered map for `registration-key` and return
   a map-like value that will access the registry for the values."
  [registration-key value]
  (swap! registry assoc registration-key value)
  (->RegisteredMap registration-key))

(def a (registered-map `a {:x 1}))

(def b {:c a})
and if you look at b in a REPL it is just {:c {:x 1}}

tony.kay01:09:47

BUT, if you change the value in def of a, eval that, THEN b will change without being re-evaluated

tony.kay01:09:09

(def a (registered-map `a {:x 1}))

(def b {:c a})

b
;; => {:c {:x 1}}
(def a (registered-map `a {:x 2}))
b
;; => {:c {:x 2}}

tony.kay01:09:21

If used in a general-purpose way such a map introduces all sorts of imperative madness

tony.kay01:09:57

BUT, given that the RAD model is meant to be "identity based" values...like database entities whose value can change over time, but whose identity remains constant, it seems like a relatively sane thing to do

tony.kay01:09:41

These things are not used in computation: they are part of your program definition that should only change at clear "stop, change code, start" points in time

tony.kay01:09:27

CLJS doesn't really have a problem here. The hot code reload story there doesn't generate as many problems...so, I think I'd make the defattr macro emit these special maps only in CLJ. Then, anything that closes over the attributes will update to the new value if an attribute is re-evaluated in the REPL

tony.kay01:09:59

without needing a ns-dependent-refresh

tony.kay01:09:06

Interested in feedback from anyone that has an opinion

👍 3
Jakub Holý (HolyJak)09:10:17

I guess it makes sense to make the developing experience smooth. And as you say, it is hidden from the developer and only applies to the definition of the app, not its runtime behavior.

lance.paine12:10:03

no particular opinion, but I did want to say I appreciate you publicly musing like this, I feel like I'm learning something from it.

lance.paine12:10:42

i have had some fun without RAD when I've had an import chain like model -> specific model -> resolver -> dbcalls and some error in the dbcalls module means model doesn't load properly on a restart, but it doesn't show (that I've noticed) where the error was, and just reports either model or specificmodel. Then i end up manually reevalutating everything in reverse order to get back to working. I assume I'm just missing something that makes it a) easier to find the actual error, and b) re-evaluate the imports automatically

tony.kay02:09:42

I made a branch and published a trial of this as fulcro-rad 1.0.24-SNAPSHOT (git sha be9cfb7a475ad3ada8e4f48286e170df776696e1) if anyone wants to try it out and see if it helps/hurts

tony.kay02:09:20

I'm trying it out in my own production project, and so far it seems promising and non-invasive.