clojure 2023-12-18

what are some recommendations for doing browser automation in clojure?

I see. So advantage of playwright is that it’s more in-browser?

I'd say it's webdriver vs CDP, different protocols with different capabilities (CDP is more featureful but Chrome-specific)

https://github.com/clj-commons/etaoin is one such library.

👆 2
👆🏻 1

Depends on the use case, if it happens to be more on the scraping side of things, playwright is most suitable (available on pure java via https://github.com/pfeodrippe/wally, or use a nbb/squint type of thing)

Lately I've preferred just using https://chromedevtools.github.io/devtools-protocol directly. you start chrome with a debug flag and then you get an http/ws api to do whatever you want in the browser. I think I always hit some limitation or another when trying to use webdriver for non-testing automation purposes. Though if it meets your needs etaoin is a great wrapper for it

💡 2

Playwright is essentially that, it uses CDP

We use Playwright at work.

Consider also whether you might like to subscribe to a browser test automation service. You can then choose a tool that supports both your local browser and the service you have in mind.

➕ 1

do we have a comment (;) reading/preserving clj/edn reader? I have some big edn files with comments all over the place which I'd like to rewrite automatically? Does clj-rewrite support this?

also https://github.com/borkdude/rewrite-edn sugars a few things on top

❤️ 3

edamame doesn't preserve comments though

ok, then I will look into rewrite-edn first. 😀

I think that is about comment , not ;;

some comment forms had a ridiculous amount of children forms which caused a problem, apparently

(also, please use 🧵 for replies in future)

Question that’s been bothering me today. Is there any way to get a tagged literal to recursively eval the same as regular Clojure collection does? For example, I’ve got a custom record for RDF triples, with an associated tagged literal.

(def foo "")

;; when evaluating a vector, symbol `foo` is evaluated & resolved
(eval (read-string "[foo]")) ; => [""]

;; when evaluating a triple record, symbol `foo` is captured as a literal symbol.
(eval (read-string "#rdf/triple [1 foo 2]")) ; => #rdf/triple [1 foo 2]
I’ve read the source code and it appears that the answer is no, since the conditional for a IType/IRecord is baked into the analyzer and returns a ConstantExpr, but just wanted to check…

Alex Miller (Clojure team) 2023-12-18T21:38:24.838059Z

tagged literal reader functions are a read-time construct to create a value - they take read (but unevaluated) data and return an object (in your case an instance of record or type)

Alex Miller (Clojure team) 2023-12-18T21:39:05.838489Z

records or types are like any other Java object and evaluate to themselves

Right.. but in this case they’re also (sorta) like a map or a vector, which eval recursively. Curious if any way to make them behave that way.

Alex Miller (Clojure team) 2023-12-18T21:39:47.885819Z

so, in short - no this is not how it works (and you probably shouldn't actually want read-time eval)

well, it’s eval-time eval, not read time 🙂

Alex Miller (Clojure team) 2023-12-18T21:40:23.689679Z

the tagged literal reader is read time

it reads just fine

just like a vector reads just fine (and reads the symbol)

but the way the compiler will compile it is as a constant without evaluating any internal fields

Alex Miller (Clojure team) 2023-12-18T21:41:09.609059Z

functions are cool things that do eval :)

if you eval in the literal you can end up getting double evaled

Alex Miller (Clojure team) 2023-12-18T21:41:31.193779Z

there even happens to be a positional record constructor that does exactly this :)

Alex Miller (Clojure team) 2023-12-18T21:42:56.554789Z

(rdf/->triple 1 foo 2)

Yep, @alexmiller that’s the fallback. I was just hoping for a construct so that writing literals embedded in code would look the same as when writing them in an EDN file. Would be a bit preferable IMO to have a way to do:

(let [foo ...]       
   #rdf/triple [1 foo 2])

but the record constructor fn will work

Alex Miller (Clojure team) 2023-12-18T21:45:01.328829Z

tagged literals are about data and values. people constantly try to do trickier things with them, and mostly just get into trouble doing so

👍 fair enough, thanks for the rationale

I kinda feel like this in particular is a justified attempt though since this is how you'd make e.g. a queue and I think it's perfectly reasonable to want a queue literal which can have elements be references to locals.

Alex Miller (Clojure team) 2023-12-18T21:46:27.252259Z

that's all fine, but why do you need to use a read literal for it?

this is why, for example, if you use a clojure set literal with (+ 1 2) in it, that will evaluate, but if you use an ordered set literal you get the unevaled list

Mostly just ergonomics, it's not really absolutely necessary.

It’s a way in which Clojure data structures (lists/maps/vectors) are privileged syntax… which is fine, that part of the language just isn’t extensible in this respect

➕ 1
Alex Miller (Clojure team) 2023-12-18T21:47:28.835869Z

correct :)

I can argue that it should be, but am fully aware that would take a well articulated problem statement and a full stack of spreadsheets, haha

Alex Miller (Clojure team) 2023-12-18T21:48:58.811539Z

tagged literals are a part of edn to be exchangeable with other languages which may not have eval

Alex Miller (Clojure team) 2023-12-18T21:49:59.045369Z

having eval in your data means you're back to programs, not data

Alex Miller (Clojure team) 2023-12-18T21:50:14.898039Z

you shouldn't want that for edn :)

I would argue the actual issue is orthogonal to tagged literals… it happens even if you construct forms by hand out of symbols and then call “eval” on them. eval in the reader fn for a tagged literal is 100% evil I agree

clojure sets exist, and are part of edn

they also evaluate their members when passed through clojure's eval function

this is not "these types should automatically evaluate stuff", this is the behavior of these types when passed through clojure's eval

the thread I linked above ends with https://clojurians.slack.com/archives/C03S1KBA2/p1698768489212979?thread_ts=1698763672.356679&cid=C03S1KBA2 which is a pretty simple protocol that if the clojure compiler used it would allow you to define new set literals that would have their members evaled if passed through eval

Yeah this is the behavior that I would want, and I've talked about desiring this before e.g. when I was writing my ropes library

➕ 1

I see the distinction Alex is making, though. Clojure code is data, but not all data can be code. Right now, lists/maps/sets are special because they are data that can be eval’d as Clojure code, and that mechanism is not designed to be extensible in Clojure as it exists. Tagged literals are solving a different problem. You COULD design a “polymorphic eval” for arbitrary data structures but that would be a design change with significant ramifications.

when serializing objects to bytecode, which the compiler has to do to, if the object isn't something the compiler already has hard coded cases for (including IType and IRecord) the compiler calls printString

it is already the case that the compiler is attempting to be polymorphic eval

Alex Miller (Clojure team) 2023-12-18T22:05:11.849299Z

(the pr thing is kind of a hack, which is maybe something we could remove with constant dynamic in Java 11)

This is what I ran into when I was working with ropes, where it has support to be emitted into code, but it can't be evaluated recursively.

Alex Miller (Clojure team) 2023-12-18T22:08:07.374249Z

functions are a fine way to make custom data structures, which can act as Clojure data by implementing the right interfaces. I think it's a non-goal (or even an anti-goal) to make new syntax with custom eval semantics.

no new syntax, it is the same evaluation semantics as core data structures, so I would argue no new semantics

for my strawman, if the type satisfied the protocol (interface would maybe be better in the compiler) when passed through eval, eval would eliminate it, and eval the result of that

and it wouldn't break any existing code, because it would introduce a new interface for this, and none of the existing code implements it so they keep the same non-evaling behavior

Alex Miller (Clojure team) 2023-12-18T22:13:34.261009Z

the principle I've heard Rich espouse about is being able to read anyone else's program, which is why the read table is not open for extension. in this case, you would need the lib creating the data structure and the semantics its applying at read time to understand how it is to be evaluated. that seems like a step too far, but don't know.

you don't have to evaluate it though

Alex Miller (Clojure team) 2023-12-18T22:15:11.916019Z

?

it is already the case with, again, the built in clojure collection types where those get used in contexts where evaluation is expected and where it isn't

Alex Miller (Clojure team) 2023-12-18T22:16:43.358859Z

I don't understand what you're proposing then

Alex Miller (Clojure team) 2023-12-18T22:17:27.691399Z

what is the objective?

so with a clojure set #{(+ 1 2)} is read in as a set with a list in it, then when eval'ed, it generates a set #{3}

the goal is the same behavior for #ordered [(+ 1 2)]

Alex Miller (Clojure team) 2023-12-18T22:19:28.416129Z

so your objective is to ... [say words]

when #ordered [(+ 1 2)] is read in, it would be some ordered set type containing the list (+ 1 2) and then after calling clojure.core/eval on it, if the ordered set type implements the right interface, you get an instance of the ordered set type containing 3

honestly this could be implemented without a protocol as long as you can call seq and empty on it

Alex Miller (Clojure team) 2023-12-18T22:20:46.069909Z

forget the mechanism for a moment, I'm still unclear precisely what the goal is

(into (empty coll) (map eval) (seq coll))

the goal is to allow source code to have literals which have the same semantics as core literals

same interior evaluation semantics

Alex Miller (Clojure team) 2023-12-18T22:22:33.014459Z

to have custom literals that have the interior evaluation semantics of core collections

if (-> #{(+ 1 2)} eval first) is 3, then (-> #my/custom-set-type [(+ 1 2)] eval first) resulting in the list (+ 1 2) is kind of a drag

Alex Miller (Clojure team) 2023-12-18T22:26:09.608729Z

well again, functions

Alex Miller (Clojure team) 2023-12-18T22:26:32.674999Z

but I see the point

Alex Miller (Clojure team) 2023-12-18T22:27:53.607419Z

there are lots of ways custom colls that implement the necessary interfaces/protocols can participate in Clojure with identical semantics, this is one place where they don't have the same level of access

👍 1

This is the best description of the problem statement and issue I’ve seen yet. Might be worthwhile to capture in an ask/jira ticket

Looking at some multi threaded code which might show some race condition, I’m wondering (to eliminate some possibilities) is binding thread safe, ie if I have two threads which do (binding [name (.getName thread)] …) do I risk a race condition or will this work as expected, ie each thread only having name contain the value of thread it’s running in?

binding is thread-safe, yes.

✅ 1

(Assuming the things on the right hand side of the bindings are thread-safe).