Fork me on GitHub
#clojurescript
<
2022-08-28
>
2FO17:08:45

Good day, Between the different cljs react libraries (rum, reagent, helix etc) which ones offer the thinnest abstraction over vanilla cljs?

danieroux17:08:09

Helix is what I would suggest. Go read the code top to bottom, it's a small surface area

🙏 2
chrisetheridge06:08:02

Rum is also very small and minimal. it uses hiccup syntax for defining HTML which is really nice. however, i think Helix is more up to date with the latest React features

🙏 1
Lidor Cohen19:08:18

Hello 👋, I have an itchy question for sometime now, that I couldn't find a good solution to yet: I have a nested data structure where some of the values are computations over other values in that data structure. So basically I see this as two requirements: 1. Compute values based on primer values 2. Ship the entire data structure as a whole Now, I can split the computation to defs and then construct the entire structure for export, but then I have two places to update, it feels wrong to split this code, I'd like to express both structure and dependencies once. My current solution was to create some kind of self referential structure that macro-expands to a let that returns the appropriate map, but it's certainly not an idiomatic code and I wanted to hear more ideas and approaches to this problem, what a good solution in cljs would look like?

Lidor Cohen19:08:09

Example of the current solution:

(smart-map [:a 1
            :b 2
            :c [:ab (+ a b)]])
=>
{:a 1
 :b 2
 :c {:ab 3}}

p-himik20:08:50

> but then I have two places to update How so?

(def data {:a 1, :b 2})

(defn add-c [data]
  (assoc :c (+ (:a data) (:b data))))

(defn send-data [data]
  (send (-> data (add-c))))
If you change :a or :b that's a single place. If you change how you add :c - it's a different, independent, place. If you decide to also add :d then it's a two-place change, sure. But if it makes sense, you can rename add-c to augment and chuck the :d in there.

lilactown21:08:04

stupid question but: why not a let?

(let [a 1
      b 2
      ab (+ a b)}]
  {:a a :b b :c {:ab ab}})

skylize00:08:39

@U2FRKM4TW The "2 places" in your example are 1) data and 2) augment. The claim for the use case under question, is that the existing-data and the derivable-data conceptually belong together as a single unit . But the derivable subset needs to be fully realized before it can be realeased to certain consumers.

skylize00:08:58

@U4YGF4NGM Not stupid, but easy to answer once you recognize the usage @U8RHR1V60 has described. The primary concern is about storing unrealized computations for later. let instead evaluates to the concrete results of said computions.

Lidor Cohen06:08:16

Thank you for your response, you've all addressed different aspects of the problem: @U2FRKM4TW thank you for your idea, if I take your suggestion to our own use case I think it would mean augmentation (or enrichment) function for every layer of dependency (I don't know how many we have but it is more than 1 or 2) and changes will require distinguishing and knowing on what layer you want to add your change (say :d depends on :c it only has it available on augmentation2 and above) I think this is quite the overhead... Today we have a related overhead but much smaller, every value is available only after it was introduced so you have to make sure that dependant values are introduced after their dependencies (which is coupling the structure of the data and it's dependency structure which is a con, but that is the best I got to so far). @U4YGF4NGM thank you, actually that is kinda what we use today, only, we have a macro layer that takecare of the structure so we don't have to assign values and construct in 2 steps (we use letm from kezban lib). @U90R0EPHA you hit the point exactly! That is a use case I haven't brought in yet but we really desire it: a perfect solution would involve a lazy evaluation and a declarative way to declare structure and dependencies so that if later merged with another structure, any dependant values that are indirectly affected by the merge will be evaluated to the correct new value. I reckon that laziness/ reactivity can be used here to achieve the desired results but I'm not sure how, and I'm not sure what a declarative self-referencing data structure should look like. Two ways I thought about are letm which allows you to refer to substructures assigned before (as I mentioned, coupling data structure with dependencies structure) vs this symby which would allow arbitrary lazy access to the root of the structure and any nested values that are trusted to be there at the time of computation (dependencies between deeply nested adjacent nodes will be less fun than letm). Thank you all for brainstorming here with me 😊

phronmophobic07:08:43

I think there's a few general techniques/approaches that might be helpful, but I feel like there's some missing context about your use case. As an example, there's dozens of libraries that essentially help manage a directed acyclical graph of values and derived values. The final result would just be a graph node that puts all derived info together into a single map. The different libraries each have a different take on specifying the who, what, where, and why. There's also different trade-offs for scaling the amount of data, updating the compute graph, handling live updates, etc. So I'm not sure if your use case is closer to rules engines, streaming libraries, a simple DSL, or something else.

Lidor Cohen07:08:46

@U7RJTCH6J what you describe sounds exactly like what we need, could you point me to some libraries that might offer solutions? And I'm also not sure about rules engine vs reactive vs lazy evaluation, I think lazy evaluation is the appropriate solution here but I might be clouded with the problem. I'll post a real example of our data (it's not fully derived yet, ideally most of the values should be computed but we haven't finished migrating to the new structure yet): https://gist.github.com/lidorcg/e0f2a2a844524bfe6fca0cf9442fca88

phronmophobic07:08:54

Ah, ok. I think I have little better idea. Is it right to say that the final result (eg. lens in this example) is immutable and doesn't get updated or change at runtime?

phronmophobic08:08:51

How is lens being used? Is it being shipped to the browser (ie. does bundle size matter)? Based on the example, it seems like lazy evaluation or even re-calculating data each time it's used wouldn't make a noticeable difference. Is there any reason that indicates runtime calculation might be a bottleneck? If not, it seems like the main goal is to improve the readability and writability?

Lidor Cohen09:08:41

It is immutable in the clojurian sense but we will want to update values within it and have the changes propagate (still not sure if lazily or reactively but I tent toward lazyness). Lens does shipped to the browser but again that is less of a consern at the moment, so it's the bundle size and recalculations performance. The main goals are indeed readability and writability but also the ability to update some values and have the changes propagate.

phronmophobic18:08:51

If lens is immutable, then I'm having trouble understanding what you mean by updating values and propagating the changes. Are there lots and lots of maps that need to be recalculated, a very short update window, or are the maps much larger than your example? Depending on the constraint, there are different options. Otherwise, it seems it doesn't matter too much how the map is recalculated.

Lidor Cohen18:08:11

Immutable by the clojurian sense means that, as always, updating a value will produce a new copy with the change. After that if some other value that was dependant of the changed value is accessed I'm expecting to see an updated calculation result. That said, the answer to all your questions is no, it doesn't matter how the map is recalculated.

phronmophobic18:08:16

> After that if some other value that was dependant of the changed value is accessed I'm expecting to see an updated calculation result. Is the result stored in a reference somewhere? Can you give an example of how you expect to be able to make changes?

Lidor Cohen18:08:47

I'm not sure I understand the question about the result being stored, it should be just a value and once I extract it from the map it should be just the value (not the calculation). I was hoping to use the regular map api (update-in, assoc, merge...)

phronmophobic18:08:13

if it's just an immutable value, then how would someone ever get a different result when extracting a property?

phronmophobic18:08:11

if lens is going to change over time, then you almost always want to use a reference (eg. atom)

Lidor Cohen18:08:03

No one should ever get different results from the same lens on the same paths.

Lidor Cohen18:08:31

Let me give you an example:

(def lens1 {:a 1
            :b 2
            :c (+ a b)})

(lens1 :a) => 1
(lens1 :b) => 2
(lens1 :c) => 3

(def lens2 (update-in lens1 [:a] inc))

(lens2 :a) => 2
(lens2 :b) => 2
(lens2 :c) => 4

(lens1 :a) => 1
(lens1 :b) => 2
(lens1 :c) => 3

(def lens3 (merge lens1 {:a 10 :b 20}))

(lens3 :a) => 10
(lens3 :b) => 20
(lens3 :c) => 30

Lidor Cohen18:08:42

Were those helpful in illuminating what I looking for?

phronmophobic18:08:23

:thumbsup: ok, that makes sense.

phronmophobic19:08:11

I'm trying to remember all the libs I've seen in this space. The closest one I can think of is https://github.com/hoplon/javelin. There are some rules engines that are probably overkill for what you're trying to do: • https://github.com/oakes/odoyle-ruleshttps://github.com/cerner/clara-rules There's also an interesting an interesting relational database with materialized views called https://github.com/wotbrew/relic which is also probably overkill. I think development on relic is currently paused. There's also a discussion at https://clojurians.slack.com/archives/CQT1NFF4L/p1657482280025899, but most of those are dataflow in the large. Not sure any of these options are a perfect fit, but maybe you'll find some interesting ideas.

👍 1
🙏 1
skylize20:08:06

If you only need to get singular values out, and not realize the whole structure at once, then all you really need is to pass the same map back into the functions stored on it, right?

(require '[clojure.test :refer [function?]])

(def data {:a 1
           :b 2
           :c (fn [m] (+ (m :a) (m :b)))})

(defn derive' [k m]
        (let [v (get m k)]
          (if (function? v) (v m) v)))

(derive' :a data)  ;; => 1
(derive' :c data)  ;; => 3
You can still always just walk the tree, if you later need the whole structure realized at once.

Lidor Cohen12:08:44

That is actually a very good option 🙏 For general purpose data should look more like this:

(def data {:a 1
           :b 2
           :c (fn [m] (+ (derive' :a m) (derive' :b m)))})
can I somehow hide the derive function under map's protocols? something like overriding get for tagged maps?

chromalchemy21:09:56

Here is another cljs dataflow graph lib. Maybe a spiritual successor (and precursor) to Hoplon/Javelin https://github.com/kennytilton/matrix/tree/main/cljs/matrix

🙏 1