Fork me on GitHub
#clojurescript
<
2023-03-06
>
M J12:03:48

in clojurescript, I have a r/atom that starts as false and represents a universal 'button-disabled', that is sent to a map of components, and in each component based on some conditions, it changes the r/atom to true. What I want, is that the button-disabled is only true when all the components reset it to true, and what is happening is that only when the last component is true, it is true, and vice versa. Any ideas?

p-himik12:03:25

In case you're using Reagent, #C0620C0C8 would be a better place for this question. Create as many ratoms as there are components that change them, and replace the top-level ratom with a reaction that checks that all those other ratoms are true. Alternatively, if the number of buttons is known, replace the boolean ratom with a counter. Increment it when something "enables" it, decrement on "disabling". Consider the final state to be "enabled" when the counter is equal to the number of buttons. Note that this approach requires you to be rather careful.

M J12:03:58

how do i do the first approach with a reaction? any documentation or examples anywhere?

p-himik13:03:35

Uhm... In the Reagent docs? :) Reagent is extensively documented (except for its internals).

M J13:03:08

I am a beginne so I dont know where to look for this specific example exactly

p-himik13:03:09

It'll end up as something

(let [enabled? (r/reaction (every? deref [enabled?-1 enabled?-2 enabled?-3]))]
  ...)

p-himik13:03:39

Alternatively, given that the body of the reaction is so simple, you can avoid it altogether and just go with (every? ...). Unless you have other components that expect a single atom-like thing to deliver the "enabled" state.

M J13:03:05

Thing is Its a dynamic number of components

p-himik13:03:42

Have them all have unique IDs, create a single ratom that contains a set of disabled IDs. When that set is empty it means that everything is enabled. There are many solutions to this problem.

Jakub Šťastný15:03:05

Hey. How do I create a custom JS exception class in CLJS?

class ValidationError extends Error {
  constructor(message, type) {
    super(message)
    // ... whatever
  }
}
Context: the project I'm working on is a JS library, I don't want to throw a CLJS-native error, but rather that is normal JS without the gobject wrapper (as in #js error) I know I can (throw js/Error ...), but I'd really like to have a custom error class.

p-himik15:03:29

If you use shadow-cljs in that project, then it has shadow.cljs.modern/defclass.

👍 2
Jakub Šťastný15:03:01

Cheers! I'll check it out.

borkdude15:03:29

Does anyone know how to speed up dynamic access like this, under the contraint that you can't use macros and can only rely on pure JS tools:

(def dyn-accessor (fn [s] (let [path (.split s ".")] #(.reduce path Reflect.get %))))

 (let [x (clj->js {:a {:b {:c 3}}}) accessor (dyn-accessor "a.b.c")] (prn (inc (accessor x))) (time (dotimes [i 10000000] (inc (accessor x)))))
4
"Elapsed time: 421.886125 msecs"
One approach is to use js/eval but I understand that this is prohibited on some websites:
(defn compile-dyn-accessor [s] (let [path (.split s ".")] (js/eval (str "(x) => x." s))))

(let [x (clj->js {:a {:b {:c 3}}}) accessor (compile-dyn-accessor "a.b.c")] (prn (inc (accessor x))) (time (dotimes [i 10000000] (inc (accessor x)))))
4
"Elapsed time: 10.560125 msecs"
So maybe there is a better way?

2
p-himik15:03:11

A macro? Assuming paths are static. Then they can also become keywords. And then... you got yourself cljs-oops. :D But it also has dynamic accessors, maybe there's some good code in there, dunno.

borkdude15:03:28

macros aren't an option here, for reasons

borkdude15:03:58

Suppose you are in pure JS land, no other tools

p-himik15:03:01

const dynaGet = (obj, path) => path.split('.').reduce((obj, key) => obj[key], obj);

delaguardo15:03:47

maybe you can use Map.prototype.get() somehow

p-himik15:03:16

IME replacing reduce with a for loop will win you some tens of percents of performance in at least Chrome.

p-himik15:03:24

@U04V4KLKC Objects aren't maps.

p-himik15:03:38

There's 0 reason to use any functions when you have [] in JS.

borkdude15:03:05

That's even slower:

cljs.user=> (def dynaget (js/eval "(path) => (obj) => path.split('.').reduce((obj, key) => obj[key], obj)"))
#'cljs.user/dynaget
cljs.user=> dynaget
#object[Function]
cljs.user=> (let [x (clj->js {:a {:b {:c 3}}}) accessor (dynaget "a.b.c")] (prn (inc (accessor x))) (time (dotimes [i 10000000] (inc (accessor x)))))
4
"Elapsed time: 702.958042 msecs"

p-himik15:03:21

Weird. Definitely not what I would've expected.

p-himik15:03:54

I get opposite results in Chrome.

djblue15:03:59

Can you eval it once and reuse the helper?

borkdude16:03:12

yes, that's what it already does

djblue16:03:36

Ohh, I missed that :thumbsup:

p-himik16:03:56

@U04V15CAJ Seems like the performance here is largely dictated by the platform. In that case, it's kinda hard to figure out what the best approach is - even if your platform is fixed, it might have different performance under different OSes or when updated to a newer version. In any case, I would still try replacing reduce with an explicit for loop.

borkdude16:03:39

I was testing in planck (self-hosted on webkit) so yes, different host. still 400ms vs 10ms... there must be a way to get that 40x speedup somewhere without eval?

p-himik16:03:42

Not necessarily - there might be special handling for . that's for some reason missing for [] (perhaps because the former cannot accept integer indices whereas the latter can).

thheller16:03:56

beware micro benchmark such as that one. it is basically useless to tell you anything about actual performance

thheller16:03:25

JS engines work on "shapes", and if this function is called with multiple shapes its performance profile is going to change drastically

thheller16:03:17

so at the very least test with objects of different shapes and depths

borkdude16:03:31

It seems lodash has a kind of function like that. Maybe worth looking into: https://lodash.com/docs/4.17.15#get

p-himik16:03:05

It's just a while loop in there.

borkdude16:03:20

yeah, pretty similar to what you had

borkdude17:03:39

Loosely translated from goog.base, eliding the null check

cljs.user=> (defn getObjectByName [cur parts] (loop [cur cur i 0] (if (< i (.-length parts)) (recur (unchecked-get cur (aget parts i)) (inc i)) cur)))
#'cljs.user/getObjectByName
cljs.user=> (let [x (clj->js {:a {:b {:c 3}}}) arr (.split "a.b.c" ".")] (prn (inc (getObjectByName x arr))) (time (dotimes [i 10000000] (inc (getObjectByName x arr)))))
4
"Elapsed time: 103.103084 msecs"

Sam Ritchie17:03:05

@U04V15CAJ js/Function is often available where eval is not

💡 2
borkdude17:03:38

@U017QJZ9M7W Do you happen to know if many sites restrict that or not?

borkdude17:03:35

http://twitter.com seems to block both:

Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'

Sam Ritchie17:03:19

I found out about it reading posts on the function constructor as a “safer alternative” to eval but I don’t know anything about what’s more available in the wild unfortunately

👍 2
hifumi12320:03:35

Would it make sense for strings to implement IStack? While one can peek a vector for accessing its last element in O(1) time, JS interop is needed for strings, but we can extend-type anyway to provide a nice interface for this, e.g.

(extend-type string
  IStack
  (-peek [^string s] (.slice s -1))
  (-pop  [^string s] (.slice s 0 -1)))
What would be some objections to doing this?

p-himik21:03:20

It will work only for string literals and not for string objects. And I'd say that extending built-in types for convenience is discouraged. There was a nice article on that that I can't find right now but I have definitely been bitten by it before.

hifumi12321:03:03

Then covering both string and js/String should suffice, no? I believe String in JVM Clojure is extended to implement IStack I guess implementing IStack for strings in CLJS is a bad idea anyway because CLJ doesn’t do this. For some reason I had thought it did

hiredman22:03:56

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/IPersistentStack.java clojure doesn't have IStack, it has that, which extends IPersistentCollection which makes implementing it more involved

hiredman22:03:28

Also on the jvm the interfaces a class implements are part of the definition of a class and cannot be changed after the fact

phill23:03:27

Be not discouraged! This is an excellent use-case for which you could (I'm sure it would take only a weekend) implement the much-talked-about "Clojure-in-Clojure" based on the protocols developed as part of ClojureScript. And then do your experiment extending String to Stack. Let's defer discussion of whether that extension is a good idea until after you're done with Clojure-in-Clojure.