humbleui

JAtkins 2024-01-05T05:35:07.398289Z

IDK if y'all are still experimenting here, but using electric might be a nice way to avoid requiring a vdom. It's a compiler that basically lifts (in the monad sense) clojure code into streams with managed lifecycles for endpoints... a little hard to explain, but the CLJS dom library is about 300 LOC without any react vdom style state management. probably worth a quick look. but, if electric was used top-to-bottom for humble-ui, many state management issues might just disappear.

đź’ˇ 4
Niki 2024-01-06T01:22:39.942909Z

Is it substantially different from https://tonsky.me/blog/humble-signals/?

JAtkins 2024-01-06T01:27:08.685629Z

That looks super interesting. I’ll read it tomorrow. On first blush, no electric isn’t super different. It’s an incremental computation signal graph. However, electric is based on the missionary library, which handles both continuous time and discrete time events. The main difference between those is the system is ok with dropping continuous time signals for performance reasons. Anyway, electric is a clojure-like dsl that compiles to a missionary graph. Probably interesting to review at least, if not actually use.

JAtkins 2024-01-06T01:30:59.390359Z

This is likely the easiest entry point: https://youtu.be/xtTCdT6e9-0?feature=shared The video is solely about missionary and how it handles signal graphs over time.

JAtkins 2024-01-06T03:25:04.215819Z

One other thing I just thought of - it prevents visual glitches. If you have A and B (which depends on A), the UI is prevented from being in a state showing AB*, and waits until A*B* is shown.

leonoel 2024-01-07T11:35:47.449629Z

> Is there an explicit “unsubscribe” method? No. sub/unsub are not exposed to the user, they are managed by the operators. Each operator defines the lifecycle of its upstream subscriptions, in the case of *area it is the same as the parent but the logic can be more complex (e.g. with conditionals - when the predicate switches, signals in the previous branch are unsub and signals in the current branch are sub). > how do you know that “there are no active subscriptions”? Each signal has access to its active downstream subscriptions, so it's basically a refcount.

leonoel 2024-01-07T15:52:22.375969Z

@tonsky also feel free to correct me about your implementation, I've not read it just inferred it from your blog post

leonoel 2024-01-06T19:45:53.580169Z

;; humbleUI
(def *area (s/signal (* @*width @*height)))

;; missionary
(def *area (m/signal (m/cp (* (m/?< *width) (m/?< *height)))))
On surface it is no different, the syntax is similar and the semantics match. The difference is, in missionary the subscription lifecycle is managed and propagated by functional composition. Therefore, if at any point in time there is no active subscription on *area, the subscriptions to *width and *height are also discarded, removing the bidirectional references (the edge in the DAG), which solves the memory leak problem. Of course, it only works if all resources comply to this model, including event sources and effects (the inputs and outputs of the DAG). That's why missionary is an effect system that also provides operators for discrete streams, so you can treat any long-lived process as a managed resource and have it automatically cleaned up when it's not used anymore.

Niki 2024-01-07T02:04:04.671669Z

@jatkin How do signals help with vdom?

Niki 2024-01-07T02:04:48.726209Z

@leonoel how do you know that “there are no active subscriptions”? Is there an explicit “unsubscribe” method?

JAtkins 2024-01-07T05:49:33.664599Z

Re. VDOM: Here's an electric example using the built in js/dom library for usage reference. https://electric.hyperfiddle.net/user.tutorial-7guis-2-temperature!TemperatureConverter Nothing too exotic, it looks kinda like react. Here's the dom library: https://github.com/hyperfiddle/electric/blob/master/src/hyperfiddle/electric_dom2.cljc#L52 I've selected and commented a couple functions:

#?(:cljs (defn dom-listener [node typ f opts]
           (.addEventListener node typ f (clj->js opts))
           #(.removeEventListener node typ f))) ;; How to setup and teardown an event listener on js/dom

#?(:cljs (defn listen> ; Listen returns an event stream. 
           ([node event-type opts]
            ;; I'm not fully sure what relieve is. 
            (m/relieve {}
              ;; observe creates a new signal based on a subject. it expects a function (fn [new-value-supplied-f])
              ;; The function can also return a no arg fn for teardown (see dom-listener) 
              ;;   when the observe is cleaned up.
              ;; It's expected that `(new-value-supplied-f new-changed-value)` is called when the value changes.
              (m/observe #(dom-listener node event-type % opts)))))) 
(defmacro on
  "Run the given electric function on event.
  (on \"click\" (e/fn [event] ...))"
  ;; 
  ([typ F] `(let [[state# v#] ;; F is a signal processor. For each new value spawned by `listen>`, send it to F,
                              ;; BUT if that signal hasn't finished propagating and a new value comes along, discard
                              ;; the older process and continue only with the new one. 
                              ;; @leonoel will have to check me on this understanding
                  (e/for-event-pending-switch [e# (listen> node ~typ)] 
                    (new ~F e#))]

    ;; the real impl has more code handling `::pending` states but those aren't required to understand this
    )))

(defmacro element
  {:style/indent 1}
  [t & body]
  `(with (new-node node ~(name t))
     ; hack: speed up streamy unmount by removing from layout first
     ; it also feels faster visually
     (e/on-unmount (partial hide node)) ; hack
     ~@body))

(defmacro button {:style/indent 0} [& body] `(element :button ~@body))

;; This function creates a stream depending on the atom !hover-enabled?. When that value changes, the when branch 
;; is hit. If hover-enabled=false, then the consumer of the `listen>` stream is killed. Since no consumers are left,
;; the whole stream is closed and the consumer fn is un-mounted from the js/dom
(e/defn EnableOnHover []
  (let [!hover-enabled? (atom false) hover-enabled? (watch !hover-enabled?)]
    (button
      (when hover-enabled?
        (on "hover" (e/fn [_] (println "hovered!")))
      (on "click" (e/fn [_] (swap! !hover-enabled not))
      (dom/text (str "Hover enabled? " hover-enabled?)))
It's a rather long example, but it shows that electric/missionary are managing streams all the way down to the leaves. There's no need for a react-style vdom because it's streams all the way down, and create/change/teardown signals are maintained to the edges of the js/dom graph.

JAtkins 2024-01-07T06:07:25.433899Z

I'm super interested now after reading your article... I may try my hand at implementing your incremental-app example in electric