Fork me on GitHub
#hyperfiddle
<
2023-04-30
>
Lidor Cohen09:04:29

Hello again, I'm looking to convert a self-ref map that represents a state graph to a DAG structure that electric will be able to consume easily, I know you've worked with something like this under the hood, except for the self-ref part but that's on me, I'm just looking for some target representation of a DAG, can you recommend anything?

Dustin Getz10:04:04

tell me more about the problem you’re solving so i can understand the use case?

Lidor Cohen10:04:03

We have nested structures that looks like this:

{:name ""
                           :greeting (str "Hello, " (this :name))}
*this is a simple example of course. It can be compiled to something like this:
(let [!name (atom "")
         name (e/watch !name)
         greeting (str "Hello, " name)]
...)
This will require us to do ourselves the traversals and manage the dependencies. So I was wondering if you know and data-structure in clojure that can handle this kind of self-ref or at least a good target to compile our data to it and from there to electric

Dustin Getz11:04:40

i need to think about it. we have pretty much this exact structure inside HFQL, we analyze it to datascript and then query datascript to emit electric code

Dustin Getz11:04:27

perhaps HFQL itself solves your business problem, if you zoom out a level or two what are we trying to accomplish with these maps?

Lidor Cohen11:04:35

it's a bit high level, and out of context it might lose its meaning, but I guess it comes down to expressive and reusable state management

Lidor Cohen11:04:02

this aspect (self-ref) resembles mobx a bit

Lidor Cohen11:04:49

datascript was one of my first hunches too, I started experimenting with it.

Dustin Getz11:04:22

like are you rendering forms, or what

Lidor Cohen12:04:41

My POC is a simple form, yes:

(e/defn Input-example []
  (e/client
   (let [!name (atom "")
         name (e/watch !name)
         greeting (str "Hello, " name)]
     (dom/input (dom/on "input" (e/fn [e] (swap! !name #(-> e .-target .-value))))
                (dom/props {:value name}))
     (dom/p (dom/text greeting)))))
This is my first target from the state in the example above

Dustin Getz12:04:28

HFQL may be the exact thing you need, we haven’t published it yet but there are some screenshots here https://hyperfiddle.notion.site/Hyperfiddle-progress-update-Feb-2023-8cc45f9da47c4719bb16851d129e3a3d

Lidor Cohen12:04:47

I think HFQL is too high level, I'm trying to handle only state (abstract data) for now. How did you break arbitrary maps into datascript? if I'm not specifying a ref in the schema it just takes the maps as a whole value. is there a way to tell datascript to break arbitrary maps to EAVs?

xificurC15:04:08

Roughly speaking HFQL extends EQL. It doesn't try to manage state but express a view

Dustin Getz18:04:04

In the example you gave

{:name ""
 :greeting (str "Hello, " (this :name))}
IMO the "map" here seems to be the desired result {:name "Lidor" :greeting "Hello, Lidor"} , which is a projection/query over an underlying graph

👍 2
Dustin Getz18:04:40

I hear that you think HFQL is too high level, but if I may show you one more thing

Dustin Getz18:04:06

This is how I'd produce basically the above map using HFQL and a datascript graph:

(defn greeting [name_]
  (str "Hello, " name_))

(e/defn Form [id]
  (hf/hfql
    [:person/name
     (greeting person/name)]
    id))

(comment
  (Form. 1) :=
  {:person/name "Alice"
   '(greeting person/name) "Hello, Alice"})

Dustin Getz18:04:34

you can process it from there, we have an :alias directive as well if you need the key to be :greeting and not '(greeting id)

Lidor Cohen18:04:56

That looks very much like the target I had in mind for that code, I still need to workout the details and how to work with datascript

Lidor Cohen18:04:50

But basically: Traverse graph Primitives turn to values in ds Computations turn to functions with pointers to dependants

Dustin Getz16:05:46

edited the gist to be simpler and slightly more powerful with a sideways lookup

Lidor Cohen09:05:37

(defn greeting [id]
  (str "Hello, " (d/entity db id)))

(e/defn Form [id]
  (hf/hfql
    [:person/name
     (greeting id)]
    id))

(comment
  (Form. 1) :=
  {:person/name "Alice"
   '(greeting id) "Hello, Alice"})
This one?

Dustin Getz13:05:51

yeah i edited that, it uses ‘(greeting person/name) now

markaddleman14:04:58

It seems like the value of electric-lang vars is nil is clojure-land.

(e/defn E [])
(def m {"k" E})
(println m)
Is this expected?

2
markaddleman14:04:13

This came up as I was trying to invoke an Electric function from a Plotly event callback. Very roughly, the callback code looks like

(fn [e] (new ElectricFn e))
Since this code lives in clojure-land, ElectricFn is nil and, thus, the code fails

markaddleman14:04:33

Well, I’m a bit of a dummy. I just noticed the clojurescript compiler complaining {k #object[clojure.lang.Var$Unbound 0x63bc661f Unbound: #'app.plotly-interop/E]}

Dustin Getz15:04:16

this is internals and could change. today, e/def quotes the body and saves it as metadata for a later compiler phase. electric things are only visible from inside/under the electric entrypoint

Dustin Getz15:04:18

in (fn [] (new ElectricFn)), new is clojure new not electric new because it’s in a clojure context

markaddleman15:04:37

For interop reasons, I think it would be helpful to have access to the Electric new

Dustin Getz15:04:25

post the whole plotly integration pls, we will find a clean way to bridge them

markaddleman15:04:57

My strategy to send plotly events into electric-land is by dispatching events to the surrounding div where I’ve added dom/on reactions

Dustin Getz15:04:01

if you look at our codemirror integration in the repo you might be able to figure out the pattern, the idea is to bridge the event to a discrete missionary flow immediately and then adapt to electric from there

markaddleman15:04:49

cool. Can you point me to that? I don’t remember seeing a codemirror integration in the tutorials

Dustin Getz15:04:03

on mobile you’ll need to grep the main repo

markaddleman15:04:11

no worries. thanks!

Dustin Getz17:04:43

this constructs a codemirror editor, registers a value change listener, and redirects change events into a missionary discrete flow >cm-v with m/observe. >cm-v which is then parsed (e.g. string to edn) and given an initial value before bridging to electric with new. https://github.com/hyperfiddle/electric/blob/faab6602ee84361d81f24e31609f01b5748a3d6f/src/contrib/electric_codemirror.cljc#L73-L83

Dustin Getz17:04:35

m/observe is the key primitive that adapts an event listener callback interface to a discrete flow, note m/observe also forces you to define the cleanup code in the same place to unregister the callback and destroy the codemirror (which missionary will call during teardown)

markaddleman13:05:22

I stared at this code for a while and I have low confidence I understand about 30% and high confidence that I don’t understand 70% of it 🙂 My current approach is working: reflect events onto a div on which I can place an electric on and use e/fns from there.

markaddleman13:05:49

Is there an issue for improving the ergonomics of javascript->electric interop?

Dustin Getz13:05:12

I think the real issue here is that the codemirror API is more complex than it seems which is causing contorted code to functionalize it

👍 1
Dustin Getz16:05:17

Based on my understanding of the plotly API (which is redirecting events through the div it attaches to) i think this is sufficient:

(e/defn Plotly [plotly-config]
  (let [plotly-id (str (random-uuid))]
    (dom/div (dom/props {:id plotly-id})
      (plotly/newPlot plotly-id (clj->js plotly-config))
      (dom/on! "plotly_click" (fn [data]
                 (let [curve-number ^int (. (aget (. data -points) 0) -curveNumber)
                       point-number ^int (. (aget (. data -points) 0) -pointNumber)]
                   [curve-number point-number]))))))

(comment
  (let [plotly-click-data (Plotly. plotly-config)]
    ; electric body
    ...))

👀 1
Dustin Getz16:05:40

We could tighten it up by using m/observe to separate the plotly lifecycle from the div's lifecycle (allowing you to remove the plotly but keep the div) but it doesn't seem to matter here

Dustin Getz16:05:49

As to making this easier to figure out, its unclear what's possible, i need to see a lot more of these and maybe there is some sort of common pattern, or also possibly each JS integration is a unique snowflake. This guy was a bit of an oddball as well

markaddleman19:05:20

Your code snippet doesn’t work because (I believe) of some plotly wierdness. From what I can tell, plotly events are not real javascript events. Plotly attaches an on function to its div and the client must call that to attach listeners. Making it more complicated, plotly/newPlot returns a JS promise which, on resolution, returns the div to which its on function is attached.

markaddleman19:05:52

thus, my entire Plotly-Chart electric function looks like:

(e/defn Plotly-Chart [plotly-config event-Reactions]
  (e/client
    (let [id (str (random-uuid))]
      (dom/div
        (doseq [[event Reaction!] event-Reactions]
          (dom/on event Reaction!))
        (dom/props {:id id}))
      (.then (plotly/newPlot id (clj->js plotly-config))
             (fn [div]
               (doseq [[event _] event-Reactions]
                 (.on div event
                      (fn [data]
                        (let [event (new js/Event event)
                              curve-number ^int (. (aget (. data -points) 0) -curveNumber)
                              point-number ^int (. (aget (. data -points) 0) -pointNumber)]
                          (set! (.-data event) [plotly-config curve-number point-number])
                          (.dispatchEvent div event))))))))))

Dustin Getz19:05:18

ok i’ll look closer at that .on synthetic thing. we already have the div, do we need to wait again?

markaddleman19:05:15

> do we need to wait again? Not sure what you mean by this. Right now, I’m reflecting the plotly data (available from its weird callback) as a real javascript event to the electric div. My ideal would be to take the plotly data object and pass it directly to a electric fn. Something like the following code:

(e/defn Plotly-Chart [plotly-config event-Reactions]
  (e/client
    (let [id (str (random-uuid))]
      (dom/div
        (dom/props {:id id}))
      (.then (plotly/newPlot id (clj->js plotly-config))
             (fn [div]
               (doseq [[event Reaction] event-Reactions]
                 (.on div event
                      (fn [data]
                        (new Reaction data)))))))))

markaddleman19:05:11

Obviously this doesn’t work because (as you pointed out earlier), the (new Reaction data) is not an electric new.

markaddleman19:05:19

It occurs to me that a real implementation would also have to convert data to edn before creating the Reaction object because it might have to be sent over the wire.