Fork me on GitHub
#clojure
<
2023-02-08
>
Nazral09:02:58

Hi, is there a way to do something similar to

:global-vars {*warn-on-reflection* true}
in deps.edn? Or is it better to just set it to true in all the files deemed necesssary?

delaguardo09:02:26

warn-on-reflection is a global var and when you (set! *warn-on-reflection* true) in one namespace it will be effective in any other namespace you load after this first one. I usually add it to the user.clj file located at the root of dev source path, add :dev alias to include this source path only for development and ensure this alias is used by the tooling. deps.edn

{,,,
 :aliases {:dev {:extra-path "dev"}
 ,,,}
dev/user.clj
(ns user)

(set! *warn-on-reflection* true)

,,,

Nazral09:02:33

Thanks ! How do you make sure the user namespace is loaded before the other namespaces with this strategy?

Nazral09:02:49

Thanks a lot, I didn't know about the user namespace, that's perfect

delaguardo09:02:21

btw, I made a mistake in my example. for the dev alias it should be :extra-paths ["dev"]

👍 2
borkdude10:02:50

I don't think this is accurate. If you have user.clj:

(set! *warn-on-reflection* true)
(prn (ns-name *ns*) *warn-on-reflection*)
and foo.clj:
(ns foo)

(prn *warn-on-reflection*)
and you require 'foo, then you will see that warn-on-reflection is false in the namespace foo. This is because that var is re-bound to its original value on every loaded file

borkdude10:02:39

Apparently doing (alter-var-root #'*warn-on-reflection* (constantly true)) in user.clj does have the desired effect

Nazral11:02:07

Thank you @U04V15CAJ I will try that

Nazral13:02:50

It works, thanks !

🎉 2
Joshua Suskalo17:02:48

warn-on-reflection is bound during loading of new files, so using a top-level set in one namespace affects that namespace only. I put it in most performance-sensitive namespaces I make these days

☝️ 2
Alex Miller (Clojure team)18:02:49

our rule of thumb in clojure itself is to put it in all namespaces that do interop (unless there is some active reason not to)

borkdude18:02:18

clj-kondo has an opt-in linter to "force" you to add (set! *warn-on-reflection* true) in namespaces where you do interop: https://github.com/clj-kondo/clj-kondo/blob/master/doc/linters.md#warn-on-reflection Note: it doesn't actually check if you get reflection warnings, it's just warning you about not having set the var

2
dpsutton18:02:06

Does anyone have a helpful gist laying around? I would love to use clojure.asm.* to modify a deftype to have a setter for its one field and a no-arg constructor. For dealing with interop to a migration library. Does anyone have a quick gist of how to interact with a deftype?

hiredman18:02:14

I wrote https://git.sr.ht/~hiredman/reagents/tree/master/item/src/com/manigfeald/reagents/struct.clj a while ago to get atomic updates to fields on a class. it just generates a class with some fields, no interface/protocol implementating.

ghadi18:02:57

"modify a deftype" meaning (f bytecode) -> new bytecode?

ghadi18:02:15

or generate a class fresh?

dpsutton18:02:35

yeah. would love to modify the deftype so it has a 0-arg constructor and a setter for its single arg

dpsutton18:02:57

doesn’t have to be generalizable, ie, hardcode to make a setChange of type String

ghadi18:02:45

is setChange part of an interface, or just a new public method?

dpsutton18:02:54

new public method

dpsutton18:02:21

or actually, i can use an interface to put it there. i just need to add a 0-arg constructor then in that case

ghadi18:02:46

who is calling the 0-arg ctor?

dpsutton18:02:02

liquibase, a migration library.

dpsutton18:02:33

it needs to instantiate a class with no args. and then will use reflection over fields to call .set<Field> with provided values from the yaml file

ghadi18:02:17

link to docs about that?

hiredman18:02:51

generating a class (from deftype) then rewriting the byte code of the class seems pretty heavy weight for that, just generate the class you need to start with

dpsutton18:02:26

yeah trying to not add a javac prep step. But i suppose that’s an option

hiredman18:02:02

or just generate it using clojure.asm (a javac prep step is going to be far simpler)

Joshua Suskalo18:02:13

you have to pass a class name string to it, right? If so then you might have to have a javac or compile-clj step anyway in order to have a pre-compiled version that can actually be sent. Or do they require you to just pass the class object?

Joshua Suskalo18:02:03

also if you want to do bytecode stuff I highly recommend insn, which has proper clojure support for large parts of the asm api

dpsutton18:02:09

- changeSet:
      id: v46.00-080
      author: dpsutton
      comment: Uppercases all Card names
      customChange:
        class: "metabase.db.custom_migrations.ChangeWrapper"
        change: metabase.db.custom-migrations/uppper-case
kinda like this. there’s a class and I provide a change parameter. It will instantiate the class (requires a 0 arg ctor), call setChange with the fqn there. And then I’ll have the class requiring-resolve the fqn for the migration

ghadi18:02:35

consider compiling a java proxy -> IFn

ghadi18:02:44

(by writing a java class)

hiredman18:02:01

but using clojure.asm is more fun

😂 2
ghadi18:02:09

agree with @U0NCTKEV8 that bytecode manip is... heavyweight

dpsutton18:02:39

yeah. team generally wants to stay away from a prep step if not needed. we removed the need for that so if a bit of asm interop and keep us away from that we’re happier.

dpsutton18:02:15

and i’ve never used clojure.asm so sounded fun and perhaps not super heavy weight. I was looking to see if i could modify deftype or make my own macro but its a special unfortunately

dpsutton18:02:24

so this

(defprotocol SetChange
  (setChange [_ x]))
(deftype Change [^:unsynchronized-mutable change]
  SetChange
  (setChange [_ x]))
gets me almost there if it could succeed with Class.forName(className).getConstructor().newInstance(); . I can succeed at that point

hiredman18:02:18

bytecode maniplation for deftype is going to require aot compilation so you can get the bytecode, then transform it, then write it out again, possibly leaving the clojure runtime where the aot compilation and the clojure runtime that loads the aot code is disagreement about the definition of the class (that disagreement sounds unlikely to matter in this context though)

hiredman18:02:39

you could always using a global identity map

Joshua Suskalo18:02:23

insn lets you make a clojure form that constructs a class at runtime with arbitrary bytecode, without needing to manipulate an existing class. You could pretty easily build a helper function to generate the bytecode for an arbitrary set-x and then you just build a macro that calls it for a fixed set of fields

hiredman18:02:38

so you have a https://docs.oracle.com/javase/7/docs/api/java/util/IdentityHashMap.html def'ed somewhere, maybe protected with a lock, and the deftype has no field

Joshua Suskalo18:02:40

I have examples of this sort of thing in coffi

Joshua Suskalo18:02:45

it just wouldn't use deftype

hiredman18:02:49

it stores stuff in the map

Joshua Suskalo18:02:57

the identityhashmap sounds like a reasonable idea too, although personally I'd have a little discomfort with that if you can't attach a cleaner to it that will remove the thing once it's destructed

hiredman18:02:08

so setChange becomes (setChange [this x] (.put global-map this x))

dpsutton18:02:38

that’s interesting.

hiredman18:02:44

maybe with a requiring-resolve for the namespace global-map is in

Joshua Suskalo18:02:04

actually I guess cleaner wouldn't work anyway since the map would maintain a strong reference

hiredman18:02:02

it depends on the context, like if this is just part of a migration process that the liquibase thing runs, then exits, who cares

dpsutton18:02:32

this runs at startup of the application, not a separate step. But don’t mind a reference to these classes hanging out

hiredman18:02:59

if you are in control of running liquidbase, you can clear the map once it is done too

dpsutton18:02:09

yes that is an option

dpsutton18:02:42

i’ll look at this and see if i can find a way to get the migration id. could just store a string and not worry about this

dpsutton18:02:18

ah, nevermind about this. I wasn’t thinking

dpsutton18:02:47

ah but this is just “leaking” a few instances of deftypes. and i don’t care about the lifetime of those

Alex Miller (Clojure team)18:02:48

just fyi - clojure.asm is considered internal implementation details subject to change without notice (and we have, when updating the shaded asm version)

dpsutton18:02:19

good point. I was hoping this would be a simple “asm, add a 0-arg constructor” and carry on.

Alex Miller (Clojure team)18:02:21

the last update included some breaking changes

👍 2
Alex Miller (Clojure team)18:02:38

you can of course, depend directly on the actual asm library and do whatever you like

Joshua Suskalo18:02:51

might sound like a broken record, but insn is a really good wrapper for asm and it's intended for public consumption

Joshua Suskalo18:02:31

I use it for generating upcall and downcall classes that handle boxing/unboxing to panama calls and c callbacks in coffi

Alex Miller (Clojure team)18:02:32

even better, don't do the thing you're trying to do :)

😂 2
Coby Tamayo21:02:26

Is there an established pattern or best practice for accepting k/v pairs of command line args to be zipped up into a map? I've seen a few tools where you type "keywords" (with leading :) on the CLI but I can't find any examples off the top of my head. Example command:

my-cli-tool :a b # -> {:a "b"}
This works but it seems a little verbose:
(defn -main [& args]
  (let [opts (into {} (map vec (partition 2 (mapv edn/read-string args))))]
    (prn opts)))
Is there something more specialized? Maybe I'm just being nitpicky. 🙂

seancorfield21:02:14

The -X / -T invocations on the CLI do that automatically, and invoke a function (not -main) that accepts a single hash argument.

seancorfield21:02:41

The -M invocation on the CLI calls -main with multiple strings.

seancorfield21:02:45

(and the example you gave would need to be something like clojure -X:my-cli-alias :a '"b"' -- b would read as a symbol, "b" is a string, but the shell needs extra quotes to prevent those " from being interpreted by the shell itself, hence '"b"' -- assuming :my-cli-alias is in your :aliases and has the :extra-deps for your tool and names a function to invoke via :exec-fn)

Coby Tamayo21:02:52

oh, ha, I just realized my vals are being parsed as symbols!

Coby Tamayo21:02:45

Thanks, sounds like I just need to revisit the deps/CLI docs in more detail.