Fork me on GitHub
#clojurescript
<
2022-03-17
>
BorisKourt12:03:38

Hey, just wrote up a question on Clojureverse, and was wondering if any of you have tips on how to use Google’s <model-viewer> with Reagent. https://clojureverse.org/t/googles-model-viewer-via-reagent/8737. Thanks 🙂

p-himik12:03:27

As far as I'm aware, what you describe are one of the reasons why custom elements aren't really taking off. It might be easy to just write something with them, but it's far from easy to develop with them, especially if you have to change the code of such a component.

p-himik12:03:49

You also write: > without having to go with a custom solution via something like react-three-fiber But how is using a custom element while requiring a custom <script> better than using a library? Conceptually, it's exactly the same. You will end with a React element that does something.

BorisKourt12:03:06

Ah what I mean by that is that <model-viewer> provides all the key functionality that I need. If I went with writing a custom solution I would have to write all of the functionality that I am getting already from <model-viewer> (ie, poster images, sane interface, AR compatibility and a great deal of edge case checks) It is possible for me to write all of those features on top of threejs but I would end up with an inferior version of all of them with the time that I have.

BorisKourt12:03:09

It is sad that they decided to so tightly couple this to the web component. Rather than have the web component be one way to interface with the broader feature set.

p-himik12:03:33

Indeed. Also, just as an additional piece of info against using it - it doesn't support Safari and Firefox.

BorisKourt12:03:06

Only for web-xr features

BorisKourt12:03:23

Which is an issue with the larger ecosystem

BorisKourt12:03:40

I’ve been developing this in firefox. Safari doesn’t support webxr at all on the mobile side.

p-himik12:03:48

Ah, alright.

BorisKourt12:03:08

Are there any other ways to try to integrate this beyond the two I mentioned?

p-himik12:03:15

An <iframe>, unless that's what you mean by "a separate html element from the Reagent controlled application".

p-himik12:03:58

Maybe it's possible to tear apart the library and use it without having to rely on the custom elements machinery.

BorisKourt12:03:31

By the separate element I meant a manually created element outside of the “app” hierarchy mounted by reagent. Thanks for the tips.

👍 1
Pepijn de Vos12:03:11

Dumb question... when writing an async loop, do I have to use loop/recur or for/doseq is also fine? I found one random SO answer that claims doseq is fine but for is not because it's lazy, but my code did not work with doseq so I'm just making sure that I can rule that out as a problem

p-himik12:03:21

Anything should be fine if you use it correctly and in the right context. It's better to ask a more concrete question here. Like "why does my async code not work with doseq", while also providing the code itself. :)

Pepijn de Vos12:03:30

Yea I'll post it if there is anything I can't solve. For now it's probably simple bugs that are more work to provide the needed context than to debug. Just wanted to know what constructs are fine to use in theory.

p-himik12:03:03

As far as I'm aware, the only things that can definitely result in problems are interop-related. In particular, ^js disappears in go blocks. But there are workarounds.

Pepijn de Vos14:03:01

Works! Indeed turns out my bug was some destructuring, and then some thing that wasn't sorted, etc. Doseq works fine.

👍 1
ahungry14:03:19

for a reframe electron app, does any jvm/java code run, or is OS io done through node / cljs calls?

ahungry14:03:30

could the internal api be luminus and front end reframe I guess is what im asking

emccue19:03:46

the 2nd one

thanks3 1
fadrian14:03:42

Are there any good tutorials out there for using clojurescript interop with javascript promises? I'm trying to use it with showOpenFilePicker() which returns a JS promise, but all i seem to be able to do with the return so far is to generate console errors.

fadrian15:03:10

Thank you. I had found that earlier, but had not been able to get it to work. I finally got it working with the code:

(defn ld-file
  []
  (-> (. js/window showOpenFilePicker)
      (.then #(.getFile (first %)))
      (.then #(.text %))
      (.then #(println %))))
Thank you again.

wombawomba15:03:00

I have a situation where some dead code isn't being eliminated with :optimizations :advanced. Specifically, a whole lot of code ends up getting pulled in because of a situation like D below:

(def A {"This is eliminated" "This is eliminated"})
(def B (f "This is eliminated"))
(def C {nil (f "This is eliminated")})
(def D {(f "This is _NOT_ eliminated") nil})
The resulting code doesn't contain any references to D, but it does contain a 'bare' statement that creates the map (`{(f "This is NOT eliminated") nil}`). It doesn't seem to matter what f is — I get the same result if I replace it with identity. Any ideas what might be causing this? Is there anything I can do to help the compiler get rid of code like this?

dnolen16:03:59

@wombawomba probably not, it might be a case that Closure Compiler can't handle

dnolen16:03:11

assuming f is free of side effects, you could probably create an example where this fails for plain JavaScript and request an enhancement

dnolen16:03:17

by free of side effects, also means no logging, printing etc.

wombawomba16:03:56

Alright, thanks. Yeah it doesn't seem to matter if there are side effects or not (I'm assuming identity doesn't have any side effects even when compiled)

wombawomba16:03:52

Let's say I want to boil this down to a minimal JS example. Is there a way to compile with advanced optimizations but without dead code elimination, and then run the code elimination pass separately?

dnolen16:03:18

I don't think so

dnolen16:03:46

but this seems like it should not be so hard to reproduce

dnolen16:03:11

var d = new CustomDataType(foo("bar"));

dnolen16:03:25

that's effectively what we generate

wombawomba16:03:09

hmm... I wouldn't think it's that simple

dnolen16:03:15

it definitely is

wombawomba16:03:33

I mean sure, the code generation is, but this issue might not be.

dnolen16:03:50

I'm very skeptical about that having tuned DCE for ClojureScript

dnolen16:03:53

it always trivial stuff

wombawomba16:03:09

Right, yeah. I'm thinking more about that this issue only seems to arise in some cases (`{(identity 1) 2}` but not {1 (identity 2)}). The code gets compiled to:

PersistentArrayMap$createAsIfByAssoc(["This is _NOT_ eliminated", null]);
(which doesn't get eliminated), vs:
PersistentArrayMap$createAsIfByAssoc([null, "This is eliminated"]);
(which does get eliminated)

wombawomba16:03:05

The problem seems to only (and consistently) occur when there's a key in a map that gets created via a (any) function call.

wombawomba16:03:35

...so I'm assuming that this is due to something specifically with how createAsIfByAssoc is defined

Ferdinand Beyer16:03:12

Is there some caching/interning involved for map keys maybe?

Ferdinand Beyer16:03:37

(i.e. a side effect)

dnolen16:03:27

that is true, for strings, keywords, symbols I think for hashing we may intern (if we can't figure out the hashes at compile time)

👍 1
wombawomba16:03:56

Alright, so assuming that intering is what's causing this, could there be a way to work around it?

dnolen16:03:27

not that I can think of, it's a runtime hashing performance optimization

dnolen16:03:04

but I'm not 100% sure this is the problem - you'd have to audit

wombawomba17:03:10

Hmm... audit how?

dnolen17:03:39

like look at what createAsIfByAssoc does in the standard library - I scanned it but I didn't see anything particular suspicious

wombawomba17:03:56

yeah that's what I'm doing right now

wombawomba17:03:18

I also can't see that it's doing anything especially weird.

dnolen17:03:23

yeah, so then I would try to make something minimal - it's not obvious to me why the values work but not the keys

dnolen17:03:36

you also might want to try master, a few things were changed to align w/ Clojure 1.11 in this code path

wombawomba17:03:56

looking at the generated JS, the only thing that seems to differ between keys and values is that it calls cljs$core$array_index_of$ for the keys

wombawomba17:03:35

...although that function is a bit more complicated, so perhaps it's what's causing problems for Closure

dnolen17:03:37

right I don't think that's in the path in anymore

wombawomba17:03:50

Is there a nightly build or something I can use to test master, or do I need to build it myself?

dnolen17:03:02

if you use tools deps there nothing to do - it supports git deps

dnolen17:03:16

you can point at a sha and it will work

wombawomba17:03:30

I'm not, but either way I went ahead and compiled it on my own

wombawomba17:03:50

Unfortunately, the changes on master don't seem to help.

dnolen17:03:04

hrm that seems harder to explain then, since I don't think array-index-of is in the path

dnolen17:03:18

also if there are no dupes, then the code path is even more simple

wombawomba17:03:29

yeah it doesn't seem to be there in the compiled code

dnolen17:03:40

yes probably dead

dnolen17:03:05

and the earlier theory is even less believable

dnolen17:03:25

because there is no difference between keys and values

dnolen17:03:51

the emitted code is literally new PAM([k0, v1, k1, v1, ...]) w/o further processing

wombawomba17:03:22

hmm... afaict the emitted createAsIfByAssoc is actually pretty complex

wombawomba17:03:28

it seems to walk through the code and call cljs.core/key-test to check for identical keys

dnolen17:03:19

key test is uninteresting and free of effects

dnolen17:03:31

the code is not complex in the normal path

dnolen17:03:52

it checks for dupes, if there are none - effectively what is done is the above

dnolen17:03:38

it maybe something about the key testing is thwarting Closure - but then this seems to point at a Closure issue

wombawomba17:03:25

I'll try removing the key check and see if that fixes it.

wombawomba17:03:23

Yup, replacing createAsIfByAssoc with a direct call to PersistentArrayMap. fixes the problem.

dnolen18:03:15

right only option is to request some kind of enhancement to Closure - there's really nothing else we could do

dnolen18:03:27

since we don't know what the keys are we resort to a dynamic construction

dnolen18:03:40

which is the correct thing to do

wombawomba18:03:34

Makes sense. I'll try to see if I can trick Closure into doing the right thing, and if not, I guess I'll have to try to repro in plain JS and file a ticket with Closure

thheller18:03:10

pretty sure this comes down to equality semantics that are too complex for closure to "understand". there is also a path that may throw so that alone probably stops the elimination

thheller18:03:38

IIRC Closure marks everything that may throw as not pure

thheller18:03:40

although this might warrant some investigation. the only path that may throw is the cljs.core/missing-protocol. but since it has a default implementation it will never reach that, so in theory could become somewhat more pure?

thheller18:03:55

but I doubt it'll understand protocols such as IEquiv fully so it probably ends there

wombawomba18:03:41

I'm trying to see if that could be it. Is there any way to do a regular js equality check in cljs to confirm?

dnolen18:03:38

you can just replace the key-test with identical?

wombawomba18:03:43

Yeah that does the trick.

wombawomba18:03:44

So I guess the conclusion then is that there's not really anything that can be done about this.

wombawomba18:03:29

...if, in theory, I wanted to work around this for my project, would it be possible for me to alter-var-root! or set! createAsIfByAssoc before any of my code gets compiled? 😇

thheller18:03:23

what are you trying to do in the first place? I mean what is this map that you are trying to get rid of and why?

thheller18:03:53

seems like an easier strategy to eliminate that map before it even gets to closure?

dnolen18:03:12

@wombawomba you going to have be very careful - honestly I don't see how you wouldn't just break everything

dnolen18:03:42

eliminating these maps also doesn't seem interesting unless you're getting rid of more than 10% of your code by doing so

wombawomba18:03:53

@thheller So I'm using https://github.com/plumatic/schema — and I'm defining a whole bunch of schemas in different places (like (def MySchema {(schema.core/optional-key :foo) schema.core/Str})) — and I want to compile a release build that's completely stripped of all schema definitions except those I actually use (I do (schema.core/check MyOtherSchema x) in a few places).

wombawomba18:03:20

@dnolen this stuff seems to account for ~30% of my build size

dnolen18:03:57

then you have to go back to the first point - I don't see how you won't break your deps

dnolen18:03:03

you cannot use identical?

wombawomba18:03:44

(by "those I actually use", I mean in this specific build — the schemas are mostly there to check fn inputs/outputs in development)

wombawomba18:03:29

@dnolen why not? 🙂 Duplicate keys normally cause an error, no? So if I'm not seeing any such errors, then shouldn't I be able to just get rid of the duplicate key check?

dnolen18:03:40

there are no checks for duplicate keys if you switch to identical?

dnolen18:03:44

you will just have broken maps

dnolen18:03:13

any 3rd party lib that assumes the usual behavior won't work

wombawomba18:03:47

I'd only have broken maps if I actually try to create maps with duplicate keys though?

dnolen18:03:10

and any of your dependencies

wombawomba18:03:01

...actually, I'm creating all these schemas using a macro anyway — would it be possible to disable the check just for the scope of that particular macro?

dnolen18:03:31

I think a better alternative might to use ObjMap

dnolen18:03:47

your macro could rewrite the schema emission to use that instead

dnolen18:03:20

or write your own super simple map implementation etc.

dnolen18:03:24

and emit that instead

dnolen18:03:45

more likely to avoid the dragons

wombawomba19:03:00

Yeah I suppose

wombawomba19:03:55

Although, I'd rather avoid having to do a 'deep' rewrite of the code in the macro. Would is be possible to use something like with-redefs for this purpose?

wombawomba19:03:15

(also, if I wanted to go down that path, could I do something like that for createAsIfByAssoc?)

dnolen19:03:25

nothing like that will work

dnolen19:03:54

or rather I'm super skeptical that Closure will make sense of these redefinitions - effectively runtime mutation

dnolen19:03:05

but visible to Closure

dnolen19:03:23

just make a DCE friendly map - that is the best way

thheller19:03:25

@wombawomba instead of (def MySchema {(schema.core/optional-key :foo) schema.core/Str}) you could (dev-only-def MySchema {(schema.core/optional-key :foo) schema.core/Str}) macro that just emits the code in a way that can be eliminated. eg (def MySchema (when ^boolean js/goog.DEBUG {(schema.core/optional-key :foo) schema.core/Str}))

dnolen19:03:22

yeah, that's even simpler if this is really about eliminating dev build stuff

wombawomba19:03:39

@thheller yeah I've actually built something like already, but it ends up being kind of clunky having to keep track of which schemas to annotate

thheller19:03:56

another option is moving all the dev only schemas to a dedicated namespace that is only included via preloads. ie. only in dev

thheller19:03:53

depending on how you use all the schemas delay may also work. (def MySchema (delay {(schema.core/optional-key :foo) schema.core/Str})). just requires using @MySchema whereever you would actually use it

wombawomba20:03:11

I can't really move the definitions — a lot of them are part of shared libraries used in multiple clj and cljs projects

wombawomba20:03:11

delays are potentially interesting... are you saying that anything wrapped in a delay won't be included in the compilation output unless it's actually used?

wombawomba20:03:01

I'm already defining my own versions of schema.core/def, schema.core/defn etc, so I suppose I could just make them work with delays transparently

thheller20:03:23

last time I checked delay is eliminated if never deref'd regardless of what is in it

wombawomba19:03:37

could there be some automated way to figure out which schemas I need to emit?

wombawomba19:03:28

...actually, it seems like there's a Closure annotation (https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler#nosideeffects-modifies-thisarguments) that could be used to mark functions as side effect-free. Couldn't this be used to help the Closure compiler eliminate the problematic code?

dnolen19:03:41

it cannot - only supported in externs

fadrian20:03:40

I'm attempting to read and write files in clojurescript by transliterating the code I found in https://web.dev/file-system-access/. The translteration of the file read code was somewhat simple, and using the pointers in https://clojurescript.org/guides/promise-interop, I was able to get it working fairly easily. However, writing seems more involved. I am translating the following code:

async function writeFile(contents) {
  const handle = await window.showSaveFilePicker();
  // Create a FileSystemWritableFileStream to write to.
  const writable = await handle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write(contents);
  // Close the file and write the contents to disk.
  await writable.close();
}
According to the advice in the promise-interop guide, this should translate to (something like):
(defn save-file
  [data]
  (let [writeable (-> (. js/window showSaveFilePicker)
                      (.then #(.createWritable %)))]
    (.then (fn [writeable]
             (-> (.write writeable data)
                 (.then #(.close writeable)))))))
However, when I try to run this, I get the following error:
./cljs-runtime/atlas.complexes.js:472 Uncaught TypeError: (intermediate value).then is not a function
    at Object.atlas$complexes$save_file [as save_file]
I'm pretty sure that something in my translation of the close is messed up. Anyone have any ideas as to where my translation is going wrong and how to right it?

p-himik21:03:39

Your second .then is not called on a promise with a function as an argument. Instead, you call it on the (fn [writable] ...) function, with no arguments.

p-himik21:03:54

(-> (.showSaveFilePicker js/window)
    (.then (fn [^js handle]
             (.createWritable handle)))
    (.then (fn [^js writable]
             (-> (.write contents)
                 (.then (fn [_]
                          (.close writable)))))))
or something like that.

1
fadrian23:03:11

Thank you. I'll give it a try.

fadrian08:03:39

I messed about with it a little bit. It turns out that this is the magic formula:

(-> (.showSaveFilePicker js/window)
    (.then (fn [^js handle]
             (.createWritable handle)))
    (.then (fn [^js writable]
             (-> (.write writable contents)
                 (.then (fn [_]
                          (.close writable))))))))
Thanks for your help!

p-himik08:03:30

Ah, right, missed that writable in there. NP!