Fork me on GitHub
#missionary
<
2022-06-23
>
Richie22:06:35

[:renderer] Compiling ...
[:renderer] Build failure:
------ ERROR -------------------------------------------------------------------
 File: C:\Users\richie\Documents\org\projects\electric-clj-play\src\app\renderer\ui09.cljs:192:3
--------------------------------------------------------------------------------
 189 | (defn poll
 190 |   "A discrete flow running given task repeatedly forever."
 191 |   [task]
 192 |   (mi/ap (loop [] (mi/amb (mi/? task) (recur)))))
---------^----------------------------------------------------------------------
Encountered error when macroexpanding cloroutine.core/cr.
null
Can't recur here at line 192 app/renderer/ui09.cljs
copied from https://gist.github.com/leonoel/c5e32b65ec7b6ab4b5b45772082a3d85#file-pump-clj-L146 How do I make a flow (I think a signal) out of an input field? In the pump.clj example, it makes a mailbox beforehand and then posts the input to the mailbox when there's input. Is that the best way? How else would I do it? Also, why doesn't this work? Thanks!

Richie22:06:36

I haven't tried it in clojure; only clojurescript with shadow-cljs.

Richie22:06:30

I found something in the slack chat history that should work.

(defn forever [task]
  (m/ap (m/? (m/?> (m/seed (repeat task))))))

leonoel05:06:33

check your version, this gist was written for b.27 where amb> is renamed to amb

leonoel06:06:57

To build a continuous flow from an input field, the idiom is to listen for changes with observe, then process event with eduction, then provide an initial value with reductions {}, then discard stale values with relieve {}

Richie03:06:44

Thank you for those hints about reductions {} and relieve {}. I'm using observe now instead of mbx. I have another question that's not related to my previous question.

(defn help-attributes
  [node attributes]
  (doseq [[k v] (s/unform ::hiccup-attributes attributes)]
    (let [k (if (keyword? k) (name k) k)]
      (when (contains? did-you-mean k) (println (<< "Setting '~{k}'. Did you mean '~(did-you-mean k)'?")))
      (if (= "on-" (subs k 0 3))
        #_(mi/stream!
           (mi/eduction (map v)
                        (mi/observe
                         (fn mount [send!]
                           (.addEventListener node (subs k 3) send!)
                           (fn unmount [] (.removeEventListener node (subs k 3) send!))))))

;; Uncaught Error: Can't process event - consumer is not ready.
;;     at G__34260 (Observe.cljs:33)
;;     at HTMLInputElement.G__35068__1 (core.cljs:4318)
;;     at HTMLInputElement.G__35068 (core.cljs:4321)

        (mi/stream!
         (mi/ap
          (v
           (mi/?<
            (mi/observe
             (fn mount [send!]
               (.addEventListener node (subs k 3) send!)
               (fn unmount [] (.removeEventListener node (subs k 3) send!))))))))
        (set-property! node k v)))))
I have this function that adds things to a dom node. node here is a dom node and attributes is a map. It's really properties not attributes. If the key is on-click then it will add an event listener for click events and expect the value to be a function that should get called on each event. When I use the commented form (commented with #_) then clicking my button prints the commented error (Can't process event - consumer is not ready) to the browser console. It works fine when I use the uncommented code. i.e. It doesn't work with eduction (map v) ... but it works with (ap (v (?< ... I failed to recreate it in a self contained example:
(let [my-fun #(println "fun!" %)
      node (gdom/createDom "input" (clj->js {:type "button"}))
      dispose! ((mi/reactor
                 (mi/stream!
                  (mi/eduction (map my-fun)
                               (mi/observe
                                (fn mount [send!]
                                  (println "adding event listener")
                                  (.addEventListener node "click" send!)
                                  (fn unmount [] (.removeEventListener node "click" send!)))))))
                prn prn)]
  (.dispatchEvent node (js/Event. "click"))
  (.dispatchEvent node (js/Event. "click"))
  (.dispatchEvent node (js/Event. "click"))
  (dispose!))

(let [my-fun #(println "fun!" %)
      node (gdom/createDom "input" (clj->js {:type "button"}))
      dispose! ((mi/reactor
                 (mi/stream!
                  (mi/ap
                   (my-fun
                    (mi/?<
                     (mi/observe
                      (fn mount [send!]
                        (println "adding event listener")
                        (.addEventListener node "click" send!)
                        (fn unmount [] (.removeEventListener node "click" send!)))))))))
                prn prn)]
  (.dispatchEvent node (js/Event. "click"))
  (.dispatchEvent node (js/Event. "click"))
  (.dispatchEvent node (js/Event. "click"))
  (dispose!)) 
These two example work fine. Hmm. Now I'm seeing a different error... I wrote some code that builds dom elements from a hiccup-like syntax and my hiccup-like example is not working. I implemented the same app without my hiccup-like code in order to help me find what's going wrong. This is what I'm trying to get:
((mi/reactor
  (let [db (atom nil)
        dispatch! (fn [event]
                    (m/match event
                             [:field ?s] (reset! db ?s)
                             ?x (throw (ex-info "unmatched" {:value ?x}))))
        >field (->> (mi/observe (fn mount [send!]
                                  (let [id (random-uuid)]
                                    (add-watch db id (fn [k r o n] (send! n)))
                                    (send! @db)
                                    (fn unmount [] (remove-watch db id)))))
                    (mi/reductions {} nil)
                    (mi/relieve {}))
        >valid? (mi/eduction (map seq) >field)
        button-node (doto (gdom/createDom "input" (clj->js {:type "button"
                                                            :value "Submit"}))
                      (as-> node (mi/stream!
                                  (mi/ap
                                   (let [disabled (mi/?< (mi/eduction (map not) >valid?))]
                                     (gdom/setProperties node (clj->js {:disabled disabled})))))))
        input-node (gdom/createDom "input" (clj->js {:type "text"}))]
    (mi/stream!
     (mi/ap
      (mi/?<
       (mi/observe (fn mount [send!]
                     (.addEventListener button-node "click" send!)
                     (fn unmount [] (.removeEventListener button-node "click" send!)))))
      (println "field:" (mi/? (first-or >field)))))
    (mi/stream!
     (mi/ap
      (let [x (mi/?<
               (mi/observe (fn mount [send!]
                             (.addEventListener input-node "input" send!)
                             (fn unmount [] (.removeEventListener input-node "input" send!)))))]
        (dispatch! [:field (.. x -target -value)]))))
    (gdom/replaceNode
     (gdom/createDom "div" (clj->js {:id "app-container"})
                     (gdom/createDom "div" nil input-node)
                     (gdom/createDom "div" nil button-node))
     (gdom/getElement "app-container"))))
 prn prn)
And this is the hiccup-like syntax that I want:
(binding [*in-reactor* true]
  ((mi/reactor
    (let [db (atom nil)
          dispatch! (fn [event]
                      (m/match event
                               [:field ?s] (reset! db ?s)
                               ?x (throw (ex-info "unmatched" {:value ?x}))))
          >field (observe db)
          >valid? (mi/eduction (map seq) >field)]
      (gdom/replaceNode
       (as-element [:div#app-container
                    [:div [:input {:type "text"
                                   :on-input #(dispatch! [:field (.. % -target -value)])}]]
                    [:div [:input {:type "button"
                                   :value "Submit"
                                   :disabled (mi/eduction (map not) >valid?)
                                   :on-click #(println "field:" (mi/? (first-or >field)))}]]])
       (gdom/getElement "app-container"))))
   prn prn))
Here's the bit of code to support it:
(def ^:dynamic *in-reactor* false)

(def re-tag #"([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?")

(s/def ::hiccup-leader
  (s/and keyword?
         (s/conformer #(if (namespace %)
                         ::s/invalid
                         (let [[match tag id class] (re-matches re-tag (name %))]
                           (if-not match
                             ::s/invalid
                             (into {}
                                   [(when (seq tag) [:tag tag])
                                    (when (seq id) [:id id])
                                    (when (seq class)
                                      [:class (string/replace class #"\." " ")])]))))
                      #(let [{:keys [tag id class]} %]
                         (keyword
                          (str tag
                               (when (seq id) (str "#" id))
                               (when (seq class) (str "." (string/replace class #" " ".")))))))))

(s/def ::hiccup-attributes
  (s/map-of (s/or :keyword keyword? :string string?)
            (s/or :fn fn? :string string? :number number? :boolean boolean?)
            :conform-keys true))

(s/def ::hiccup
  (s/or :text string?
        :number number?
        :flow fn?
        :hiccup (s/cat :leader ::hiccup-leader
                       :attributes (s/? ::hiccup-attributes)
                       :children (s/* ::hiccup))))

(defn observe
  [a]
  (->> (mi/observe (fn mount [send!]
                     (let [id (random-uuid)]
                       (add-watch a id (fn [k r o n] (send! n)))
                       (send! @a)
                       (fn unmount [] (remove-watch a id)))))
       (mi/reductions {} nil)           ; provide initial value
       (mi/relieve {})                  ; discard stale values)
       ))

(defn set-property!
  [node k v]
  (if (and *in-reactor* (fn? v))
    (mi/stream!
     (mi/ap
      (gdom/setProperties node (clj->js {k (mi/?< v)}))))
    (gdom/setProperties node (clj->js {k v}))))

(def did-you-mean {"onclick" "on-click"
                   "click" "on-click"
                   "readonly" "readOnly"})

(defn help-attributes
  [node attributes]
  (doseq [[k v] (s/unform ::hiccup-attributes attributes)]
    (let [k (if (keyword? k) (name k) k)]
      (when (contains? did-you-mean k) (println (<< "Setting '~{k}'. Did you mean '~(did-you-mean k)'?")))
      (if (= "on-" (subs k 0 3))
        #_(mi/stream!
         (mi/eduction (map v)
                      (mi/observe
                       (fn mount [send!]
                         (.addEventListener node (subs k 3) send!)
                         (fn unmount [] (.removeEventListener node (subs k 3) send!))))))

        (mi/stream!
         (mi/ap
          (v
           (mi/?<
            (mi/observe
             (fn mount [send!]
               (.addEventListener node (subs k 3) send!)
               (fn unmount [] (.removeEventListener node (subs k 3) send!))))))))
        (set-property! node k v)))))

(defn as-element
  [form]
  (when-not (s/valid? ::hiccup form)
    (throw (ex-info (s/explain ::hiccup form) (s/explain-data ::hiccup form))))
  (let [conformed (s/conform ::hiccup form)]
    (m/match conformed
             [:text ?text]
             (gdom/createTextNode ?text)

             [:number ?number]
             (gdom/createTextNode (str ?number))

             (m/and (m/guard *in-reactor*) [:flow ?flow])
             ;; use mi/reductions instead
             (let [old (volatile! nil)]
               (mi/stream! (mi/ap (let [elem (as-element (mi/?< ?flow))]
                                    (do (when @old
                                          (gdom/replaceNode elem @old))
                                        (vreset! old elem)))))
               @old)

             [:hiccup {:leader {:tag ?tag
                                :id ?id
                                :class ?class}
                       :attributes ?attributes
                       :children (m/seqable (m/cata !children) ...)}]
             (doto (gdom/createDom ?tag)
               (as-> node (do
                            (when ?id (set-property! node "id" ?id))
                            (when ?class (set-property! node "class" ?class))
                            (help-attributes node ?attributes)
                            (doseq [child !children]
                              (.appendChild node child)))))

             ?x (throw (ex-info "non exhaustive pattern match" {:value ?x})))))
I'm just making a dom node and then I have some special cases when I add properties. When the key is "on-click" I listen for click events. Otherwise, if the value is a function then I treat it like a missionary flow and run it for the effect of setting a dom node property. Instead of the "consumer is not ready" error from before, I'm now getting #object[Error Error: Unsupported operation.]. If I use (ap (v (?< ... instead then I get field: #object[missionary.impl.Ambiguous.Branch]. I'm sorry that I don't have a clear question or sample problem. What's the difference between eduction (map f) vs (stream! (ap (f (?< ...? I'm running flows for the effect of setting a property on a dom node. When would I use stream! vs signal!? I think the value for the property is actually a signal since it's a continuous thing. Button clicks would be a stream since those are discrete events.

Richie03:06:49

I'm using stream! everywhere since it doesn't seem to work with signal! . My reasoning is that signals are lazy and I need the effect so I can't have a lazy thing.

Richie03:06:15

I just learned that slack has a limit on the size of a single post. Haha.

Richie03:06:38

Thank you. Thank you so much.

Richie14:06:17

Hey, I have a more focused example. My problem is something to do with where I put the function. IIUC ?< forks the program so I should have the println "field" (? (first-or ...)) after the fork. If it were before the fork then it would only run once; I want it to run again every time the program forks. I figure it should work in the other cases since the println ? first-or is inside of a function. It's ok if it builds that function once as long as it runs it in each fork. Thanks!

Richie14:06:43

I'm using b.27-SNAPSHOT

Richie14:06:59

Here's a complete and self contained example program with the problem.

Dustin Getz18:06:40

Can you start a new thread with the question in two sentences for me to read and the gist?

👍 1
leonoel09:06:51

> What's the difference between eduction (map f) vs (stream! (ap (f (?< ...))))? (eduction (map f) <x) is semantically equivalent to (ap (f (?< <x))). If you observed a difference I suspect you're in undefined behavior land, look at the definition of f and check for dangling parking operators. > When would I use stream! vs signal!? I think the value for the property is actually a signal since it's a continuous thing. Button clicks would be a stream since those are discrete events. > I'm using stream! everywhere since it doesn't seem to work with signal! . My reasoning is that signals are lazy and I need the effect so I can't have a lazy thing. Yes. stream! is discrete and eager, signal! is continuous and lazy. Use signal! when you need to share a continuous value among multiple consumers, use stream! when you want to perform an effect in reaction to a change.

Richie14:06:17
replied to a thread: [:renderer] Compiling ... [:renderer] Build failure: ------ ERROR ------------------------------------------------------------------- File: C:\Users\richie\Documents\org\projects\electric-clj-play\src\app\renderer\ui09.cljs:192:3 -------------------------------------------------------------------------------- 189 | (defn poll 190 | "A discrete flow running given task repeatedly forever." 191 | [task] 192 | (mi/ap (loop [] (mi/amb (mi/? task) (recur))))) ---------^---------------------------------------------------------------------- Encountered error when macroexpanding cloroutine.core/cr. null Can't recur here at line 192 app/renderer/ui09.cljs copied from https://gist.github.com/leonoel/c5e32b65ec7b6ab4b5b45772082a3d85#file-pump-clj-L146 How do I make a flow (I think a signal) out of an input field? In the pump.clj example, it makes a mailbox beforehand and then posts the input to the mailbox when there's input. Is that the best way? How else would I do it? Also, why doesn't this work? Thanks!

Hey, I have a more focused example. My problem is something to do with where I put the function. IIUC ?< forks the program so I should have the println "field" (? (first-or ...)) after the fork. If it were before the fork then it would only run once; I want it to run again every time the program forks. I figure it should work in the other cases since the println ? first-or is inside of a function. It's ok if it builds that function once as long as it runs it in each fork. Thanks!