Fork me on GitHub
#clojure
<
2024-05-02
>
didibus05:05:04

Where does libs get preped? I'm trying to like "Clean" the prep, to see if prep is still needed.

seancorfield05:05:54

It depends on the library. Look in the deps.edn file for the prep key and see what it "ensures" -- that's usually the folder (or file) that prepping will cause to be created, which is how the prep machinery knows whether it is needed or not.

seancorfield05:05:02

The folders will be created in the checked out repo in ~/.gitlibs

didibus06:05:17

Know if its safe for me to delete ~/.gitlibs ? Should get recreated right

seancorfield06:05:30

It's not safe. It's like ~/.m2 -- if you delete it then any .cpcache folder entries already created by the CLI will point to none-existent files.

seancorfield06:05:18

You'd need to use -Sforce on every CLI invocation that might reference a git dep, so that it would recompute the dependencies.

seancorfield06:05:45

You can just remove the single folder that :deps/prep-lib specifies in :ensure from ~/.gitlibs/libs/<org>/<repo>/<sha>/deps.edn

seancorfield06:05:12

(that's where you'll find the checked out repo -- so there will be a copy for every sha)

didibus06:05:36

Cool, good to know

Alex Miller (Clojure team)11:05:03

It is safe, .gitlibs is a cache. The CLI will notice the dir is missing and re acquire

Alex Miller (Clojure team)11:05:52

You should not really need -Sforce

seancorfield16:05:48

Oh... tools.deps hadn't used to notice missing artifacts in .cpcache tho', right? So that got fixed at some point?

Alex Miller (Clojure team)16:05:05

well looking at the script again, it may be a little more subtle. if the gitlib dir itself is missing, that will be reobtained and if the prep dir is missing, that's the trigger for prep. other changes may not be noticed

Alex Miller (Clojure team)16:05:35

missing .jar files (say in .m2) or stale manifest files in local or git all trigger the staleness check

seancorfield16:05:25

Thanks. I ran into the "missing .jar" stuff a lot because I often blew away ~/.m2/repository while testing/trying to repro weird bugs to help people here on Slack ๐Ÿ™‚ I haven't experimented too much with blowing away parts of ~/.gitlibs -- I have blown away the whole directory a couple of times (I just checked and I have over 7,000 deps.edn files under ~/.gitlibs/libs/ so it's probably time to blow it away again ๐Ÿ™‚ )

Alex Miller (Clojure team)16:05:20

there are also two parts to gitlibs - the git object dirs and the worktrees (effectively checkouts at a commit, which are added to the classpath and where builds happen)

seancorfield16:05:28

Apparently, I have 21G of stuff in ~/.gitlibs/ ๐Ÿ˜

flowthing06:05:02

Typoing the name of a method-as-value in Clojure 1.12-alpha10 yields a somewhat confusing error message:

ฮป clj -Srepro -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.12.0-alpha10"}}}' -M -r
Clojure 1.12.0-alpha10
user=> (map String/.toUppercase ["foo" "bar"]) ; bad lower-case "c"
Syntax error macroexpanding clojure.core/fn at (REPL:1:1).
() - failed: Insufficient input at: [:fn-tail]

โœ… 1
flowthing07:05:21

Adding param-tags helps:

user=> (map ^[] String/.toUppercase ["foo" "bar"])
Syntax error (IllegalArgumentException) compiling String/.toUppercase at (REPL:1:1).
Could not find method toUppercase in class java.lang.String

oyakushev07:05:56

This is fixed in alpha11

oyakushev07:05:07

Clojure 1.12.0-alpha11
user=> (map String/.toUppercase ["foo" "bar"])
Syntax error (IllegalArgumentException) compiling fn* at (REPL:1:1).
Error - no matches found for instance method toUppercase in class java.lang.String

flowthing07:05:57

Oh, crikey. I mixed up the alphas. Thanks!

๐Ÿ‘ 2
Noah Bogart14:05:40

hey @seancorfield, i saw your Ask about deps.edn aliases-as-data. what is the purpose of those? how do you use them?

Alex Miller (Clojure team)15:05:53

the basic idea behind this in deps.edn is that conveying arbitrary edn on the command line is fraught and given that we have a configuration file, you should be able to convey arbitrary data there by giving it a name (the alias). this can be used by tools, or even your app, in particular via the new clojure.java.basis api added in 1.12

Alex Miller (Clojure team)15:05:17

alex.miller@alex ~ % clj -Sdeps '{:aliases {:foo ["hi" "there"]} :deps {org.clojure/clojure {:mvn/version "1.12.0-alpha11"}}}'
Clojure 1.12.0-alpha11
user=> (require '[clojure.java.basis :as basis])
nil
user=> (-> (basis/current-basis) :aliases :foo)
["hi" "there"]

Noah Bogart15:05:51

oh cool, that makes sense.

Alex Miller (Clojure team)15:05:56

tools that wish to leverage this as a config mechanism can do so, but are strongly encouraged to use namespaced alias keys

Alex Miller (Clojure team)15:05:16

the more typical use of aliases as information for command execution by the CLI is a self-serving use of this - these are aliases conveying data for use by tools.deps in resolving the classpath

๐Ÿ‘ 2
Noah Bogart15:05:37

i see that a section has been added to the docs: https://clojure.org/reference/clojure_cli#_using_aliases_for_custom_purposes - that's helpful

Noah Bogart15:05:04

once 1.12 releases, your example above would be a good demonstration

Alex Miller (Clojure team)15:05:04

that said, I don't know how Sean is using them, so I'm sure he will add that context :)

๐Ÿ‘ 1
seancorfield16:05:23

Mornin' ๐Ÿ™‚ My external test runner for Polylith lets you use aliases-as-data for passing JVM options to the subprocess: https://github.com/seancorfield/polylith-external-test-runner/?tab=readme-ov-file#passing-jvm-options and here's how we use that at work:

:poly-test-jvm-opts ["--enable-preview"
                       "-client"
                       "-Dclojure.core.async.go-checking=true"
                       "-Dclojure.spec.check-asserts=true"
                       "-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory"
                       "-Djdk.httpclient.allowRestrictedHeaders=host"
                       "-Dlog4j2.configurationFile=/var/www/worldsingles/development/resources/log4j2-silent.properties"
                       "-Dlog4j2.formatMsgNoLookups=true"
                       "-Dlogged-future=synchronous"
                       "-Duser.timezone=UTC"
                       "-Dws.pubsub.buffer-size=10000"
                       "-XX:-OmitStackTraceInFastThrow"
                       "-XX:+TieredCompilation"
                       "-XX:TieredStopAtLevel=1"]
  :poly {;...
                polylith/clj-poly {:mvn/version "0.2.19"
                                   :exclusions [org.slf4j/slf4j-nop]}
                io.github.seancorfield/polylith-external-test-runner
                {:git/tag "v0.4.0" :git/sha "eb954fe"
                 :deps/root "projects/runner"
                 :exclusions [org.slf4j/slf4j-nop]}
   :jvm-opts ["--enable-preview"
              ...
              ;; for the subprocess that poly test invokes:
              "-Dpoly.test.jvm.opts=:poly-test-jvm-opts"]

๐Ÿ‘ 1
seancorfield16:05:59

...and then in the runner itself:

java-opts      (or (System/getenv "POLY_TEST_JVM_OPTS")
                           (System/getProperty "poly.test.jvm.opts"))
        opt-key        (when (and java-opts (re-find #"^:[-a-zA-Z0-9]+$" java-opts))
                         (keyword (subs java-opts 1)))
        java-opts      (if opt-key
                         (into [] (remove nil?) (chase-opts-key (get-project-aliases) opt-key))
                         (when java-opts (str/split java-opts #" ")))
where chase-opts-key is copied from tools.deps:
(defn- get-project-aliases []
  (let [edn-fn (juxt :root-edn :project-edn)]
    (-> (deps/find-edn-maps)
        (edn-fn)
        (deps/merge-edns)
        :aliases)))

(defn- chase-opts-key
  "Given an aliases set and a keyword k, return a flattened vector of
  options for that k, resolving recursively if needed, or nil."
  [aliases k]
  (let [opts-coll (get aliases k)]
    (when (seq opts-coll)
      (into [] (mapcat #(if (string? %) [%] (chase-opts-key aliases %))) opts-coll))))
(this has to work on older Clojure versions so so it doesn't use the new basis function)

Noah Bogart16:05:24

wow, that's quite a bit of code. makes sense tho, you're juggling a lot of stuff.

seancorfield16:05:02

sean@sean-win11-desk:~/workspace/wsmain$ find . -name deps.edn|wc
    210     210    7226

Noah Bogart16:05:32

210 deps.edns? holy shit

seancorfield16:05:13

Our top-level deps.edn is > 600 lines and our build.clj is > 400 lines. Polylith has a deps.edn file per "brick"" (`bases` for applications, components for reusable pieces) plus one per "project" (the actual thing you build from those bricks). 145K lines of code total.

๐Ÿ˜… 1
3
chef_kiss 1
Noah Bogart17:05:04

that's very cool

Brett18:05:00

What are the call stack limits of JVM Clojure ? Do you ever feel stack empathy ? Sometimes I am considering moving a piece of code in a separate function and I feel a slight discomfort because I don't like growing my call stack. Am I silly to even have this thought ?

Alex Miller (Clojure team)18:05:44

the limits are really stack memory, not a fixed limit. it's pretty unusual to hit a stackoverflow unless you're in some recursive process

Alex Miller (Clojure team)18:05:02

in general, I wouldn't make refactoring decisions based on stack size

dpsutton18:05:23

note that assoc-in and other core functions are coded in a way that they could stack overflow.

setting=> (some? (assoc-in {} (repeat 50000 :a) :hi))
Execution error (StackOverflowError) at (REPL:1).
null
I doubt anyone has hit this in a non-synthetic manner.

didibus18:05:39

Depends. It's silly in small cases, for largely recursive algorithms that you don't actually need a stack for, it wouldn't be silly. I would say though what's not silly is designing the code so it's not as deep. It's easier to move things horizontally then up/down. It's also easier to follow a series of steps, than lots of inner branching.

didibus19:05:46

To answer your question more directly. You can set the stack memory as a JVM arg when you launch the app. I think default is 1mb, but it depends on JVM, their version, what operating system it runs on, etc. they might all have different default.

didibus19:05:54

I think the option is -Xss for stack size

didibus19:05:19

Each thread gets a stack. So it's per-thread. And if you max it out, you get StackOverflow

vemv21:05:51

Misc well-known Clojure libraries tend to need a higher stack (`-Xss` as mentioned above) to properly work Some people have a hard time accepting that. Personally I have it clear - Java algos and Clojure algos are vastly different, the JVM / its defaults were made primarly with Java in mind > I feel a slight discomfort because I don't like growing my call stack. Considering even the simplest function in Clojure has a quite high stack depth already, it barely makes a difference What I mean is, if I hit Ctrl+\ in my repl I can easily see many layers like:

at clojure.core.protocols$fn__8244.invokeStatic(protocols.clj:136)
	at clojure.core.protocols$fn__8244.invoke(protocols.clj:124)
	at clojure.core.protocols$fn__8204$G__8199__8213.invoke(protocols.clj:19)
	at clojure.core.protocols$seq_reduce.invokeStatic(protocols.clj:31)
	at clojure.core.protocols$fn__8236.invokeStatic(protocols.clj:75)
	at clojure.core.protocols$fn__8236.invoke(protocols.clj:75)
	at clojure.core.protocols$fn__8178$G__8173__8191.invoke(protocols.clj:13)

Alex Miller (Clojure team)21:05:20

I've never heard of a Clojure library needing a different -Xss

vemv21:05:15

clojure.algo.monads, Meander (iirc)

didibus22:05:47

Ya same, never had to change the Xss config on any prod system or personal stuff either. I've never used those libs though, but, it sounds like they have unbounded recursion that consumes the stack? If so, no Xss setting will work, you just have to rewrite the implementation. If they just have a need for higher Xss but it doesn't grow with input size, that's weird. I can't imagine what they're doing that's so deep

didibus22:05:02

Like, to stack overflow you'd need to go beyond 1000 depth, that's quite large.

Brett06:05:13

Thanks all for the interesting replies

oyakushev06:05:01

To @U47G49KHQ: it is important to understand that by default, without https://clojure.org/reference/compilation#directlinking, Clojure functions cannot be inlined because of Var indirection. So, function calls in Clojure are not as "free" as they are in Java.

oyakushev06:05:32

Second point: A well-written Clojure program usually has a much cleaner call stack than a framework-driven Java program. Case in point:

๐Ÿ”ฅ 2
didibus17:05:04

Generally when people release their app though, they will enable direct linking, and others I feel.

dpsutton17:05:44

Is direct linking a common option set during AOT? Iโ€™m not sure weโ€™ve ever done it and I donโ€™t hear about it much.

โ˜๏ธ 1
oyakushev17:05:33

Generally when people release their app though, they will enable direct linking, and others I feel.To be honest, I really doubt that. It is a great feature, but it is still not default, obscure, and comes with its own set of tradeoffs.

didibus17:05:04

Oh well, might just be me then. I always AOT, elide-meta and direct-link for release. Not for libraries, but for apps/services yes.

โž• 1
didibus17:05:36

Only downside as far as I know is you can't redefs which prevents repl usage.

didibus17:05:21

Well, it doesn't prevent it. Just limits your ability to redef and have what was already calling that function be updated to call the new function

dpsutton17:05:22

have you checked how much performance you get from direct linking? I wonder if the benefits outweigh the loss of repl functionality

didibus17:05:55

No I haven't. Just kind of did it cause free boost. We never redef in production by policy. We even try to rarely REPL-in, but when we do we use it to debug. The risk of breaking the node more than it is in prod is too high with redef, you can fat finger easily and all.

dpsutton17:05:27

yeah. i would never open a repl against a real running instance, but i like being able to debug the actual production jar

dpsutton17:05:06

java "$(socket-repl 6006)" -jar real-jar.jar is lovely

vemv17:05:48

It's a good idea to do all REPLing in a sidecar process. It shouldn't be too hard to create two artifacts and two processes (one direct-linked, the other not)

oyakushev17:05:45

> Only downside as far as I know is you can't redefs which prevents repl usage. Yes, it is pretty much the only downside, but still massive for me. I asked before for the ability to control what's direct linked: https://ask.clojure.org/index.php/12488/finer-grained-control-over-direct-linking

didibus17:05:09

You do have the ability to control it. You can mark it with ^:redef to avoid direct linking.

didibus17:05:26

Or you meant like, opt-in instead of opt-out ?

oyakushev17:05:43

I mentioned that in the question.

didibus17:05:09

Ah ok, ya I just read it

oyakushev17:05:50

In my backed-only-by-intuition assumption, enabling direct linking just for clojure.core functions and nothing else would give 80-90% of the performance benefits.

didibus17:05:00

> However, if the developer has DL disabled, then the calls to clojure.core functions would go through Var resolution > Is this true? I thought it would use the precompiled classes first, and those are direct linked.

didibus17:05:53

Honestly, this can be a lib. Pretty sure you can walk your code and add the annotations to all vars as a build step. Then you could specify namespaces to include/exclude.

oyakushev17:05:59

> Is this true?

user=> (decompile (not (empty? [])))

...
    public static void load() {
        ((IFn)cjd__init.__not.getRawRoot()).invoke(((IFn)cjd__init.__empty_QMARK_.getRawRoot()).invoke(PersistentVector.EMPTY));
    }

    public static void __init0() {
        __not = RT.var("clojure.core", "not");
        __empty_QMARK_ = RT.var("clojure.core", "empty?");
    }
...

oyakushev17:05:37

> Honestly, this can be a lib. Pretty sure you can walk your code and add the annotations to all vars as a build step. Then you could specify namespaces to include/exclude. I'd rather patch the compiler if I wanted to go that route. Would be more robust.

didibus17:05:09

Ya, but that type of PR will just linger forever lol. I'm saying it more from a practical standpoint, not that I think it's better as a lib ๐Ÿ˜›

oyakushev17:05:41

I meant as in "patch the compiler locally and use it instead of mainline".

didibus17:05:38

Oh I see. Well, sure. But then you can't offer it to the world and you need to keep rebasing

didibus17:05:48

I feel using rewrite-clj it should be not too hard to do

oyakushev17:05:43

It won't work for libraries then, or you have to be content with not being able to redefine functions in libraries. Which may be fine, but then, perhaps the blanket :direct-linking true will be fine.

didibus17:05:45

Ya, if the lib is direc-linked, like Clojure it will. You could also, more annoyingly, do it for libs. You'd need to unjar and rejar though.

oyakushev17:05:40

I see, don't even have to rejar, just unpack onto the filesystem and compile from there.

๐Ÿ‘ 1
didibus17:05:55

Why would you want libs not direct-linked though?

didibus17:05:23

I can't remember the last time I redefed an inner lib function that my code doesn't call

oyakushev17:05:41

You have to debug weird stuff sometimes:). It doesn't happen often, but when it does, you sure as hell want that ability. But again, really depends on what you are doing. In a really serious type of application where data corruption may be critical, I'd stay away from redef for sure

didibus17:05:33

Ah, I think I'm wrong about the Clojure core lib being AOTed

didibus17:05:51

That's why direct-linking depends on if you have it on or off

didibus17:05:06

But then, what does it mean for Clojure lib to be direct-linked?

oyakushev17:05:43

It means that functions in clojure.core invoke direct-linked variants of other functions in clojure.core

didibus17:05:52

> As of Clojure 1.8, the Clojure core library itself is compiled with direct linking. This is from: https://clojure.org/reference/compilation#directlinking

didibus17:05:49

If I set direct-link to true when I compile, won't everything, including all my dependencies, be direct-linked? So like, it's not like Clojure has to explicitly do anything for it to be direct-linked if I set direct-link to true no?

oyakushev17:05:49

Yes. But if you do not enabled direct-linking (which is the default state), clojure.core functions calling other clojure.core functions will still be direct linked.

oyakushev17:05:52

Here's clojure/core$frequencies.class (from the Clojure jar) decompiled:

public static java.lang.Object invokeStatic(java.lang.Object coll) {
        java.lang.Object object = coll;
        coll = null;
        return core.persistent_BANG_.invokeStatic((java.lang.Object)core.reduce.invokeStatic((java.lang.Object)new fn__8614(), (java.lang.Object)core.transient.invokeStatic((java.lang.Object)clojure.lang.PersistentArrayMap.EMPTY), (java.lang.Object)object));
    }

didibus17:05:53

So Alex's answer to your question is outdated? He said that fastmath for example wants to redef core functions no? So does that mean you can no longer do that?

oyakushev17:05:39

No, he's correct. You can redefine clojure.core functions for your code (or any code that doesn't use direct linking)

didibus17:05:07

Ok, but it won't redefine the function that clojure.core functions call on each other

๐Ÿ‘ 1
oyakushev17:05:19

You can redefine clojure.core/persistent! so that your code uses the new version, but clojure.core/frequencies will still use the old version of persistent!

didibus17:05:58

Interesting limitation. So you could not have fastmath detect boxed calls a clojure function makes by calling other math operations internally

didibus17:05:10

Unless you deleted the class files from the clojure jar

oyakushev17:05:42

Theoretically, you can put a sources-only clojure.jar onto the claspath, with file change date that is later that in the real clojure.jar. Not really robust, but that will force a clojure.core recompile.

๐Ÿ‘ 1
didibus17:05:03

I actually feel like, it would be nice to lean into direct-linking even more. I would wrap more things in a Var indirection for better REPL reloads. And then rely more on direct-linking to elide. And then it could also make sense to have better support for granular config of what to direct-link or not. Like, hum, now I forgot haha. What are the things you can't redef. Protocols?

Bailey Kocin19:05:32

Does anyone know how I would add a custom encoder and decoder in Cheshire for a java.time.Duration type so I can convert it to and from that type. I see add-encoder but I do not see a way to add custom decoding? I am a little confused

Bailey Kocin20:05:54

Found a comment

;; Decoding remains the same, you are responsible for doing custom decoding.

jgomez20:05:57

Looking at what I did in a similar situation here is only add-encoder, once you encode something you lose type information, so it only becomes a String/Number/JSObject so you would lose the ability to know what to decode it to.

Bailey Kocin20:05:57

I got my answer!

๐Ÿ™Œ 1
jgomez20:05:55

for Java 8 time classes I think you can just do something like this for encoding:

;;; add simple string json encoders for Java 8 classes
(json/add-encoder Duration json/encode-str)
(json/add-encoder Instant json/encode-str)
(json/add-encoder LocalDate json/encode-str)
(json/add-encoder LocalDateTime json/encode-str)
(json/add-encoder LocalTime json/encode-str)
(json/add-encoder MonthDay json/encode-str)
(json/add-encoder ZoneOffset json/encode-str)
(json/add-encoder ZoneId json/encode-str)
(json/add-encoder OffsetDateTime json/encode-str)
(json/add-encoder OffsetTime json/encode-str)
(json/add-encoder Period json/encode-str)
(json/add-encoder Year json/encode-str)
(json/add-encoder YearMonth json/encode-str)
(json/add-encoder ZonedDateTime json/encode-str)
where json is the cheshire.core alias

Bailey Kocin21:05:11

I was able to do something a lot simpler with clojure.data.json

(defn encoder-fn
  "Custom encoder on java.time.Duration types"
  [key val]
  (cond
    (= (type val) Duration) (str ":java.time.Duration" "[" val "]")
    :else val))

(json/write-str {:a 1 :b (java.time.Duration/ofSeconds 1)} :value-fn encoder-fn)

(defn decoder-fn
  "Custom decoder on java.time.Duration types"
  [key val]
  (cond
    (str/starts-with? val ":java.time.Duration")
    (Duration/parse (second (str/split val #"\[|\]")))
    :else val))

(json/read-str "{\"a\":1,\"b\":\":java.time.Duration[PT1S]\"}" :key-fn keyword :value-fn decoder-fn)
It works just fine

๐ŸŽ‰ 1
Max03:05:34

Fwiw if youโ€™re dealing with structures of a known shape, malli is great for this sort of thing. Adding these sorts of tags isnโ€™t all that different from what transit does too

Bailey Kocin13:05:57

Oh good to know! I can look into that too