Fork me on GitHub
#clojurescript
<
2022-05-21
>
pez06:05:27

I'd like to understand promesa.core/run! better. Just spent some time chasing a bug of mine that boils down to me thinking that ”promise aware” means it works a similar p/do!. Like so:

(comment
  (p/let [n 100
          !xs (atom [])]
    (p/run! (fn [x]
              (vscode/env.clipboard.writeText (str x))
              (p/let [cb-x (vscode/env.clipboard.readText)]
                (swap! !xs conj cb-x))
              (p/create (fn [resolve]
                          (js/setTimeout resolve 0))))
            (range n))
    (println
     (str "n: " n ", number of xs: " (count @!xs))))

  ;; Prints things like:
  ;;  n: 100, number of xs: 98
  ;;  n: 100, number of xs: 99
  ;;  n: 100, number of xs: 22
  ;;  n: 100, number of xs: 96
  ;;  n: 100, number of xs: 67
  ;;  ...

  (p/let [n 100
          !xs (atom [])]
    (p/run! (fn [x]
              (p/do!
               (vscode/env.clipboard.writeText (str x))
               (p/let [cb-x (vscode/env.clipboard.readText)]
                 (swap! !xs conj cb-x))
               (p/create (fn [resolve]
                           (js/setTimeout resolve 0)))))
            (range n))
    (println
     (str "n: " n ", number of xs: " (count @!xs))))

  ;; Consistently prints:
  ;;  n: 100, number of xs: 100
  ;;  n: 100, number of xs: 100
  ;;  ...
  )
The difference being that in the fixed version I wrap the three promises with p/do!. Can someone help me understand this better? 🙏

pez06:05:33

Forgot to say: The first version behaves well, without the setTimeout promise, afaict. This made me think I had understood p/run! for a while. Then I introduced the timeout wait...

alpox08:05:27

run! Executes the callbacks as microtasks, meaning it delays the execution slightly to make the calls asynchronous. It also only runs each callback after the previous one finished and also the possibly returned promise finished. The difference between your version is that do! Makes the single promises inside the callback themselves also wait for the previous ones to finish while without do! They are all running concurrently and finish in their own time. Thus its also possible that run! By itself finishes before all readText are done as nothing is waiting for readText to finish. run! Only waits for the returned promise to finish (here the setTimeout promise)

🙏 1
❤️ 1
pez08:05:03

Thanks! That makes perfect sense.

pez08:05:14

Am I the only one wishing for p/doseq? 😀

alpox08:05:02

That would probably also good to have, sure :lächeln: maybe it wouldnt be too hard to make a new macro for it in userspace but it would probably be a good addition in promesa.

alpox09:05:47

@U0ETXRFEW

(defmacro p-doseq [bindings & body]
  (let [syms (vec (take-nth 2 bindings))
        gen-syms (mapv gensym syms)]
    `(let [values# (for ~bindings ~syms)]
       (p/run! (fn [~gen-syms]
                 (-> (p/all ~gen-syms)
                     (p/then (fn [~syms] (p/do! ~@body)))))
               values#))))

(defn deliver-late [x]
  (p/create (fn [resolve] (js/setTimeout #(resolve x) 0))))

(p-doseq [x [(deliver-late -1) 0 1]
          y [1  2 3 (deliver-late 4)]]
         (prn x y))

alpox09:05:49

Seems to be working

alpox10:05:19

Or even simpler if the collection arguments cannot be promises as well:

(defmacro p-doseq [bindings & body]
  (let [syms (vec (take-nth 2 bindings))]
    `(let [values# (for ~bindings ~syms)]
       (p/run! (fn [~syms] (p/do! ~@body))
               values#))))

(p-doseq [x [-1 0 1]
          y [1  2 3 4]]
         (delay 1000)
         (prn x y))

alpox11:05:31

As I just fell into a small rabbit-hole I also made a p/for 😄

pez11:05:09

Fantastic! You should PR them to promesa, me thinks.

pez11:05:16

I was wishing for p/for last night as well. 😀

alpox11:05:53

@U0ETXRFEW I'll see if I'll make the PR 😉

(defmacro p-for [bindings & body]
  (let [syms (vec (take-nth 2 bindings))]
    `(let [values# (for ~bindings ~syms)]
       (p/loop [result# (list)
                value# (first values#)
                rest# (rest values#)]
         (p/let [exec-result# (->
                               (p/promise value#)
                               (p/then (fn [~syms] (p/do ~@body))))
                 next-result# (conj result# exec-result#)]
           (if (empty? rest#)
             (reverse next-result#)
             (p/recur
              next-result#
              (first rest#)
              (rest rest#))))))))
Here the p/for if you want it in user-space 🙂

🙏 1
alpox11:05:08

There is probably a better solution to the p/for though. I will probably rewrite it if I PR

alpox12:05:55

(defmacro p-for [bindings & body]
  (let [syms (vec (take-nth 2 bindings))]
    `(let [values# (for ~bindings ~syms)]
       (->
        (reduce (fn [results# ~syms]
                  (p/let [prev-results# results#
                          result# (p/do! ~@body)]
                    (conj prev-results# result#)))
                (p/promise (list))
                values#)
        (p/then reverse)))))
This may be better :thinking_face:

pez13:05:05

Looks simpler. Can you elaborate a bit on why one would be better than the other? Only if you have the time. And for the sake of me learning some macro author things. 😃

lilactown15:05:44

one thing to note is that this p-for isn't lazy

lilactown15:05:59

(there isn't a way to do it lazily with promises, since promises are eager)

🙏 1
alpox18:05:04

I wouldnt say one is strictly better than the other. But the second one is more readable which is good enough for me :-) Yes, none of them are lazy. One thing that I noticed is that there are too many possible versions of those macros so im not sure in what forms they would best come. It would be possible to make them execute all concurrently, sequential (as now) and for both versions it would also be possible to await promises that would be coming in from the sequences as values.

Oliver Marks14:05:48

["ol" :as ol]
["ol/View" :refer [View]]
["ol/layer/Tile" :refer [TileLayer]]
["ol/source/XYZ" :refer [XYZ]]

(new XYZ {:url "https://{a-c}.})
I am translating this code https://jsrepos.com/lib/openlayers-openlayers-javascript-maps to clojurescript I have the above import but XYZ seems to fail with ".node$module$ol$source$http://XYZ.XYZ is not a constructor" not even sure if this is a js issue but strange that the previous imports and there contructors worked but not the XYZ one am I missing something ? tend to avoid using node libraries as I always hit issues like this 😞

thheller14:05:48

@olymk2 see the translation table in this section https://shadow-cljs.github.io/docs/UsersGuide.html#_using_npm_packages. your requires are translated incorrectly if the package is actually ESM code. might also just be ["ol/source/XYZ" :as XYZ] in case it is not

Oliver Marks15:05:08

Thanks I will read through that and give it a try 🙂