Fork me on GitHub
#clojure
<
2023-10-31
>
lsenjov01:10:49

Is there a way using deps to make your entry point a java function instead of a clojure function? Using -M expects a namespace, and using -X it can't seem to find the namespace

lsenjov01:10:28

Alright, will play. Thanks!

Alex Miller (Clojure team)01:10:13

Actually I guess it is a class, so not

Alex Miller (Clojure team)01:10:42

It is possible to use java directly and just use the CLI to generate the classpath with -Spath

lsenjov01:10:34

We're just working around it: realised we could make a clj namespace that wraps the other class' main function

lsenjov01:10:13

Thank you though, will look into the straight java route later if we need it

Alex Miller (Clojure team)01:10:19

You can also pass a .clj script which can do whatever

Alex Miller (Clojure team)01:10:44

Or pass via -e or stdin if you want to make that on the fly

👍 1
seancorfield01:10:53

To call a Java function via -X would imply that it can take a single hash map argument and it would be passed a Clojure hash map so, even if you could do it directly, it would be a pretty unusual Java function (unless you were really careful about the command line arguments you passed). Having a Clojure function as a wrapper seems a much safer approach since you could perform argument validation/adapt the Java function's signature etc. I'm curious as to your use case @U24QP2D4J?

lsenjov02:10:32

Playing with compiling a SOAP service into classes with jaxb and calling from clj. Our builds had fun time yesterday when the enonic maven decided to have a hiccup, so looking at options for moving it to a more reliable place. (I'm not entirely across it, a different engineer is looking at options)

dpsutton14:10:52

I have a question on how to ingest data in reader macros. We have a reader macro #hawk/malli that will take a malli schema in our tests. We call eval on the form in the reader macro, but this is leading to problems in resolving functions in clojure core. It seems to be resolving clojure.core/map? to a new object. It behaves correctly when called, but malli has a registry based on function identity and it’s not finding it because it has a new location in memory. More details in thread

dpsutton14:10:48

(let [schema #hawk/malli [:map [:k [:maybe map?]]]]
  (println {:schema (.schema schema)
            :map?   map?})
  (malli.core/validate (.schema schema)
                       {:k {}}))
{:schema [:map [:k [:maybe #object[clojure.core$map_QMARK___5489 0x49cfc60c clojure.core$map_QMARK___5489@49cfc60c]]]],
 :map? #object[clojure.core$map_QMARK___5489 0xe0847a9 clojure.core$map_QMARK___5489@e0847a9]}
Execution error (ExceptionInfo) at malli.core/-exception (core.cljc:138).
:malli.core/invalid-schema
note that the schema references a clojure.core/map? at location 0x49cfc60c whereas map? is at location 0xe0847a9

dpsutton14:10:35

so i’m wondering what the proper way to read “complicated” structures are in data-readers

dpsutton14:10:27

I noticed that ordered/set #'flatland.ordered.set/into-ordered-set, seems to sidestep this issue by not evaling it’s arguments, but it leads to some nonsense like

#ordered/set [1 2 (+ 1 3)]
#{1 2 (+ 1 3)}

Noah Bogart14:10:26

this is a slightly different way to handle it but could you use the :map keyword-style of schemas in malli? and then move any custom schemas to the global schema registry?

dpsutton14:10:26

it is for use in tests. we need some way to dispatch to a malli validation and we have a deftype wrapper. But the fundamental question remains, why do i get a different version of clojure.core/map? and what is the proper way to use data-readers

👍 1
Noah Bogart14:10:10

instead of eval, could you use resolve? maybe postwalk plus resolve?

Noah Bogart14:10:52

i'm not sure why eval results in different identity for core functions

dpsutton14:10:02

walking over the form and resolving sounds strange

thheller15:10:16

safe yourself a lot of pain and do not use reader tags in source code. (hawk/malli [:map [:k [:maybe map?]]]) works just fine with no undefined behavior or quirks 😛 in data this is fine, but for source code you have to deal with a lot of quirks and pain, especially if you want to add AOT compilation

dpsutton15:10:38

the suggestion from our work slack was

`(->Malli ~form)
seems to work. Again this is just for test code for dispatch in our test matchers. Would you still stay away from data readers there?

thheller15:10:49

everywhere in any .clj(c|s) file, absolutely without question should never contain any reader tags in my view yes

hiredman15:10:56

How are you running tests?

hiredman15:10:13

Are you using something like polylith?

dpsutton15:10:15

example usage is

(is (=? {"Rome"   {:pk_ref         (mt/$ids $municipality.id)
                                 :name           "Rome"
                                 :model_id       (:id model)
                                 :model_name     (:name model)
                                 :model_index_id #hawk/malli :int}
                       "Tromsø" {:pk_ref         (mt/$ids $municipality.id)
                                 :name           "Tromsø"
                                 :model_id       (:id model)
                                 :model_name     (:name model)
                                 :model_index_id #hawk/malli :int}}
                      (into {} (comp relevant (map (juxt :name normalize)))
                            (search! "rom"))))
so we have our =? test form which asserts equality and we can mixin #hawk/malli on a malli schema. Seems like it works with self-evaluating (numbers, keywords, etc) stuff

dpsutton15:10:20

no polylith

dpsutton15:10:20

another example

(is (=? [{:id   #hawk/malli Tab-Id-Schema
                        :name "A look at Reviews" :position 0}
                       {:id   #hawk/malli Tab-Id-Schema
                        :name "A look at Orders" :position 1}]
                      (:tabs dash)))
and
(def Tab-Id-Schema
  "Schema for tab-ids. Must be integers for the front-end, but negative so we know they do not (yet) exist in the db."
  [:fn neg-int?])

thheller15:10:08

same, this is clojure code. so why not just :model_index_id (hawk/malli :int)? I mean this might work if this is your reader function, just called as a regular function

hiredman15:10:00

It sounds sort of like multiple clojure runtimes isolated from each other by class loaders

dpsutton15:10:07

it seems that

(defn read-malli
  "Data reader for `#hawk/malli`."
  [schema-form]
  `(->Malli ~schema-form))
is a good fix from the older version
(defn read-malli
  "Data reader for `#hawk/malli`."
  [schema-form]
  (->Malli (eval schema-form)))
I’m quite open to getting rid of reader macros. But first I want to understand the right way to do it and understand why i’m getting a different version of clojure.core/map?

dpsutton15:10:48

i’m wondering if eval in the reader macro causes a double eval? I’m a bit scrambled by how many times that gets evaluated

hiredman15:10:21

You could take each map?, get the class, and walk up the class loaders for each to see if they are all the same

dpsutton15:10:57

what would that look like (excited to check though)

dpsutton15:10:44

ok. .getParent on the classloader for each clas

Alex Miller (Clojure team)15:10:32

the value passed to the reader function has been read but not evaluated

Alex Miller (Clojure team)15:10:02

if you think eval needs to be involved, I would suggest this is probably a bad usage of tagged literals

Alex Miller (Clojure team)15:10:23

have you heard about functions? they are pretty cool and do eval for you :)

😎 1
dpsutton15:10:51

haha i agree. i think this was a syntax preference in our test code and we didn’t realize some of the tradeoffs involved

Alex Miller (Clojure team)15:10:22

the reader value will get evaluated when it's returned so that is likely the source of the double eval

hiredman15:10:35

I bet it is because the form you are evaluating has the fn object for map? Embedded in it, not the symbol

Alex Miller (Clojure team)15:10:49

probably something like that

Noah Bogart15:10:09

if the data reader is meaningful to you, maybe check out the keyword schemas in malli. those will pass through to malli easily

hiredman15:10:07

Because the map? doesn't close over anything it gets the generic embed object bytecode treatment of "call no arg constructor of its class"

hiredman15:10:21

So multiple instances of the class

dpsutton15:10:44

ok. so for the flatland ordered set reader

ordered/set #'flatland.ordered.set/into-ordered-set,
which has source
(defn into-ordered-set
  [items]
  (into empty-ordered-set items))
which yields this
#ordered/set [1 2 (+ 1 2)]
#{1 2 (+ 1 2)}
Do you consider this GIGO or is there a proper fix of their function? (perhaps (into empty-ordered-set ~items)` ?

hiredman15:10:09

It is complicated, but in general that is correct

thheller15:10:15

I consider that working as intended and nothing to fix. reader tags are meant for data, not "code yet to be evaluated"

dpsutton15:10:36

ok. so it is intender for literals and using non-literals is abusing it?

dpsutton15:10:22

(thank you both. just wanting to fully understand it before jumping into a fix (replacing with raw functions, fixing the reader macro function, only using self-evaluating data in the macro call sites)

pithyless15:10:32

Another example:

user=> #inst "2023"
#inst "2023-01-01T00:00:00.000-00:00"

user=> #inst (str 2023)
Syntax error reading source at (REPL:4:17).

dpsutton15:10:16

yes. but i was kinda keying off of

#{1 2 (+ 1 2)}
#{1 3 2}
which does the right thing. So was wondering how to do the right thing with code that might be evaluated

👍 1
pithyless15:10:52

Also echoing @UEENNMX0T - I'd also recommend favoring keyword over functions when defining malli schemas. It just sidesteps some future headaches that may or may not come up.

thheller15:10:13

if you want to make things even more complicated add CLJS into the mix, which handles reader tags in source files entirely different 😛

1
dpsutton15:10:31

so this is interesting. Double evaling doesn’t necessarily give me a new instance. But it does if it’s inside a vector

(let [evaled (eval 'map?)
      double (eval evaled)]
  {:evaled evaled
   :double double
   :original map?
   :identical? (identical? map? double)})
{:evaled #object[clojure.core$map_QMARK___5489
                 "0xe0847a9"
                 "clojure.core$map_QMARK___5489@e0847a9"],
 :double #object[clojure.core$map_QMARK___5489
                 "0xe0847a9"
                 "clojure.core$map_QMARK___5489@e0847a9"],
 :original #object[clojure.core$map_QMARK___5489
                   "0xe0847a9"
                   "clojure.core$map_QMARK___5489@e0847a9"],
 :identical? true}
here the double evaled form is identical? to map? Note all locations are 0xe0847a9 But if we put it inside a vector
(let [evaled (eval '[:stuff map?])
      double (eval evaled)]
  {:evaled evaled
   :double double
   :original map?
   :identical? (identical? map? (last double))})
{:evaled [:stuff
          #object[clojure.core$map_QMARK___5489
                  "0xe0847a9"
                  "clojure.core$map_QMARK___5489@e0847a9"]],
 :double [:stuff
          #object[clojure.core$map_QMARK___5489
                  "0x506ba0ae"
                  "clojure.core$map_QMARK___5489@506ba0ae"]],
 :original #object[clojure.core$map_QMARK___5489
                   "0xe0847a9"
                   "clojure.core$map_QMARK___5489@e0847a9"],
 :identical? false}
function identity breaks. location on the single evaled and original are 0x0847a9 (as before) but evaling [:stuff map?] twice resolves map? to location 0x506ba0ae

hiredman15:10:33

Yeah, you are basically seeing the difference between eval via the interpreter and eval via the compiler

👍 2
dpsutton15:10:16

so the tl;dr would be that tagged literals should be reserved for self-evaluating literals. Using the syntax quote solution is working around some of the pitfalls from not using self-evaluating literals but isn’t necessarily clear of all of them and there could be other things lurking.

hiredman15:10:57

The issue with reader literals and eval is once you start having your reader literals produce non-core collections at read time, which is a popular usage for them, the evaluator doesn't know how to traverse them

hiredman16:10:09

% clj -Sdeps '{:deps {org.flatland/ordered {:mvn/version "1.15.11"}}}'
Clojure 1.11.1
user=> (use 'flatland.ordered.set)
nil
user=> (ordered-set 4 3 1 8 2)
#ordered/set (4 3 1 8 2)
user=> #ordered/set (4 3 1 8 2)
#ordered/set (4 3 1 8 2)
user=> #ordered/set (4 3 1 8 (+ 1 2))
#ordered/set (4 3 1 8 (+ 1 2))
user=> (defprotocol IEliminate (-eliminate [obj]))
IEliminate
user=> (extend-protocol IEliminate (type #ordered/set ()) (-eliminate [x] `(ordered-set ~@(seq x))))
nil
user=> (eval #ordered/set (4 3 1 8 (+ 1 2)))
#ordered/set (4 3 1 8 (+ 1 2))
user=> (eval (-eliminate #ordered/set (4 3 1 8 (+ 1 2))))
#ordered/set (4 3 1 8)
user=> 
% 

hiredman16:10:18

if eval would call something like IEliminate internally on unknown objects, you could teach it how to traverse custom collection types and make them behave more like core collections

diego.videco15:10:32

Why is it possible to sort this:

(sort [[1 3] [1 2]])
But not this:
(sort ['(1 3) '(1 2)])

p-himik15:10:31

As the exception says, lists are not comparable. But vectors are.

dpsutton15:10:15

presumably boils down to random access. You can access the Nth item in a vector in constant time. For a list you have to walk the list N times. So if you compared a bunch of things that were long you would have a drastic slowdown in code execution

dpsutton15:10:07

you can of course build a comparator that behaves this way. so it is “possible” to sort them. But you are forced to be aware of this

dpsutton15:10:37

an example would be

(sort (fn [a b] (compare (vec a) (vec b))) ['(1 3) '(1 2)])

p-himik15:10:44

Vector comparison is item by item. You don't need random access to be able to implement that with lists.

p-himik15:10:33

No clue why lists themselves are not comparable. Perhaps it was never seen as practical, maybe because it's very easy to confuse lists with lazy seqs, and those can be infinite.

dpsutton15:10:48

of course you don’t. but that’s the default way it is implemented

public int compareTo(Object o){
	IPersistentVector v = (IPersistentVector) o;
	if(count() < v.count())
		return -1;
	else if(count() > v.count())
		return 1;
	for(int i = 0; i < count(); i++)
		{
		int c = Util.compare(nth(i),v.nth(i));
		if(c != 0)
			return c;
		}
	return 0;
}
relying on the constant access

p-himik15:10:39

Because it's the implementation for the vector class. :)

dpsutton15:10:48

vector’s compare uses constant time access for count and positional comparisons. Both things lacking for lists

p-himik15:10:14

Lists do have O(1) count.

dpsutton15:10:14

ah right you are. sorry about that

dpsutton15:10:26

(i am conflating infinite seqs with concrete lists)

diego.videco16:10:50

Thanks to you both, very informative discussion, too

Eugen17:10:11

How can I conditionally define a fn ? I'm probably missing someting:

;; needed for Clojure 1.10 compatibility
(if (function? (symbol 'clojure.core/parse-long))
  (println "parse-long defined")
  (defn parse-long [s]
    (Long/valueOf s)))

hiredman17:10:34

But defn has compile time effects, so you cannot do what you want and with a runtime conditional

hiredman17:10:53

So you need to use a macro or not use def

Eugen17:10:09

how can I not use def ?

Eugen17:10:28

a macro can work as well I think

Eugen17:10:36

thanks, I learned so much today 😄

Eugen17:10:00

;; needed for Clojure 1.10 compatibility
(when-not (resolve 'clojure.core/parse-long)
  (intern 'migratus.cli 'parse-long
          (fn [s] (Long/valueOf s))))
now I need to test this with clojure 1.10 and see if it works

hiredman17:10:11

Long/valueOf isn't entirely complete, parse-long will return nil for an unparsable string instead of throwing an exception

Eugen17:10:35

thanks, for my use-case I parse a cli arg to long - don't need more

oyakushev18:10:39

If your goal is just avoiding the warning, then you can either: • rename your function so it doesn't clash anymore • In your ns form, do this:

(:refer-clojure :exclude [parse-long])

👍 2
Eugen18:10:05

I renamed the fn 🙂

Eugen18:10:15

thanks, that is a better solution for my case

Eugen17:10:26

I want to avoid the warning in a CLI app

Eugen19:10:53

does tools.cli allow me to pass in edn maps as strings? Trying to parse this

./migratus.sh -vvv --config-data "{:store database}" create 
gives me :
{:options {:verbosity 3, :config-data {:store}, :arguments [database} create] REDACTED}
which seems wrong. Does anyone know if this is a bug or a feature? I might be wrong but "normal" cli parsing (bash at least) should not ignore quotes .

p-himik19:10:45

Try printing with prn instead of println to see the double quotes around strings. I assume that something in that second block is a string because otherwise that data structure isn't readable.

p-himik19:10:48

Also, it depends on what you have in your migratus.sh. You have to be careful with double quotes in scripts - otherwise, you'll end up treating a single argument like "1 2" as two arguments, 1 and 2, which is, seemingly, what's going on here.

seancorfield19:10:27

user=> (require '[clojure.tools.cli :refer [parse-opts]])
nil
user=> (require '[clojure.edn :as edn])
nil
user=> (def cli-options [["-c" "--config-data EDN" "Configuration Data" :default {} :parse-fn edn/read-string]])
#'user/cli-options
user=> (parse-opts ["-c" "{:store database}"] cli-options)
{:options {:config-data {:store database}}, :arguments [], :summary "  -c, --config-data EDN  {}  Configuration Data", :errors nil}
user=>

Eugen19:10:41

thanks, I solved it. I think it was caused by how I was passing args in the shell script. I added quotes around "$@"

clojure -J-Dclojure.main.report=stderr -M:migratus "$@"

1
👍 1