clojure

New to Clojure? Try the #beginners channel. Official docs: https://clojure.org/ Searchable message archives: https://clojurians-log.clojureverse.org/
Michael Lan 2021-03-25T00:18:27.417300Z

how do I use third-party dependencies in a single-file clojure execution

seancorfield 2021-03-25T00:32:33.417800Z

@michaellan202 How are you running the Clojure file?

seancorfield 2021-03-25T00:33:16.418700Z

(there’s a trick where you can turn a Clojure file into a shell script that can run on Linux/macOS and it can contain its dependencies and code to reinvoke itself via the Clojure CLI)

seancorfield 2021-03-25T00:36:26.420100Z

Here’s an example I posted the other day in #tools-deps showing how a Clojure “shell script” could have functions invoked via the CLI -X option:

(! 962)-> ./example.clj -X example/test :foo '"bar"'
WARNING: test already refers to: #'clojure.core/test in namespace: example, being replaced by: #'example/test
[+] test successful {:foo bar}
(! 963)-> cat example.clj 
#!/bin/sh
#_(
   #_DEPS is same format as deps.edn. Multiline is okay.
   DEPS='
   {:deps {} :paths ["."]}
   '
   #_You can put other options here
   OPTS='
   -J-Xms256m -J-Xmx256m -J-client
   '
exec clojure $OPTS -Sdeps "$DEPS" "$@"
) ;; code goes below here
(ns example)
(defn test [arg-map]
  (println "[+] test successful" arg-map))

seancorfield 2021-03-25T00:39:28.420500Z

And here’s another possibility:

(! 991)-> bin/time.sh 
Time is now 2021-03-25T00:38:56.623Z
Java version is 15
(! 992)-> cat bin/time.sh 
#!/usr/bin/env clojure -Sdeps {:deps,{clj-time/clj-time,{:mvn/version,"0.14.2"}}} -M
 
(require '[clj-time.core :as t])
(println (str "Time is now " (t/now)))
(println (str "Java version is " (System/getProperty "java.version")))

Michael Lan 2021-03-25T01:04:21.421700Z

Wow thats interesting. the second looks a lot better. The command line options confuse me, like I don’t get how to execute the main method, or what -M or -X do exactly, and the docs I have found aren’t comprehensive

Michael Lan 2021-03-25T15:25:51.426800Z

I had a lot of trouble and still can’t figure out how to get https://github.com/davidsantiago/hickory working with -Sdeps.

clojure -Sdeps '{:deps github-davidsantiago/hickory {:git/url "<https://github.com/davidsantiago/hickory.git>" :sha "ea248a6387f007dc4c4e8fcbbafbb1b9cbc19c78"}}' -M
gives the error:
Error while parsing option "--config-data {:deps github-davidsantiago/hickory {:git/url \"<https://github.com/davidsantiago/hickory.git>\" :sha \"ea248a6387f007dc4c4e8fcbbafbb1b9cbc19c78\"}}": java.lang.RuntimeException: Map literal must contain an even number of forms
I’d appreciate if you could give some pointers as to how to use this.

Michael Lan 2021-03-25T15:34:45.427100Z

I’ve also changed the spaces to commas as you describe in this video https://www.youtube.com/watch?v=CWjUccpFvrg but I do get this error:

Error building classpath. Don't know how to create ISeq from: clojure.lang.Symbol
java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol

seancorfield 2021-03-25T16:33:21.448800Z

@michaellan202 -Sdeps takes a hash map but yours is not valid: you have three items in it. It should look like this:

clojure -Sdeps '{:deps {github-davidsantiago/hickory {:git/url "<https://github.com/davidsantiago/hickory.git>" :sha "ea248a6387f007dc4c4e8fcbbafbb1b9cbc19c78"}}}' -M
However, that library is not set up to be used via a git dep as it is a Leiningen project (it has no deps.edn file). You can use it via it’s Clojars coordinates:
clojure -Sdeps '{:deps {hickory/hickory {:mvn/version "0.7.1"}}}' -M
The Hickory docs are outdated, it seems, and the coordinates for the dependency are buried way down in the README at https://github.com/davidsantiago/hickory#obtaining and it looks like the project is abandoned anyway.

Michael Lan 2021-03-25T16:34:42.449100Z

Thank you for the detailed response!

seancorfield 2021-03-25T16:38:34.449300Z

Also, if you are using :git/url to depend on a library that has Clojars (or Maven) releases, it is better to use the actual lib name of the released version so that the dependency machinery will know that you’re using a different version of, say, hickory/hickory, rather than a completely different library.

seancorfield 2021-03-25T16:39:42.449500Z

That way, if you are working with code that also depends on that library, your explicit :git/url version choice will override the transitive dependency, rather than try to load both versions of the library and then having a conflict because you’ll have the same set of namespaces in two places.

Michael Lan 2021-03-25T16:48:01.449700Z

Ah got it. That makes sense

seancorfield 2021-03-25T17:07:08.450Z

You can use non-`deps.edn` projects via :git/url by the way but you need to tell the CLI that you’re overriding the project type by adding :deps/manifest :deps into the coordinate map and then you also need to specify the project’s dependencies (from project.clj) yourself — which would be several additional things for Hickory, which is why I didn’t show that. If project.clj only has Clojure as a dep, you can use it via :git/url just by specifying the manifest (which is true for quite a few simple libraries out there). Most Leiningen-based projects tend to deploy to Clojars so it’s rare that you would need to depend on them via :git/url.

seancorfield 2021-03-25T17:07:46.450200Z

Also worth noting that if a project contains any Java code, even if it is a deps.edn project, you can’t use it via :git/url because there’s no way to compile the Java code.

Michael Lan 2021-03-25T17:13:30.450400Z

I’m struggling with figuring out how to fix this error:

Error building classpath. Don't know how to create ISeq from: clojure.lang.Symbol
java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol
my shebang line is as so:
#!/usr/bin/env clojure -Sdeps '{:deps {cheshire/cheshire {:mvn/version "5.10.0"}}}'

Michael Lan 2021-03-25T17:13:57.450600Z

oh, the quote is being interpreted as a symbol

devn 2021-03-25T20:36:33.451500Z

I have kind of a gross version because there are multiple calls to clojure, but: given a file foo.clj:

#!/usr/bin/cljit
'{:deps {cheshire {:mvn/version "5.8.0"}}}
(ns foo (:require [cheshire.core :as json]))
(println (json/parse-string "{\"a\": 1}"))
chmod +x foo.clj Drop this in /usr/bin as cljit
#!/bin/sh

set -e

if [ $# -eq 0 ]; then
    echo "usage: doclj &lt;file&gt; [args ...]"
    exit 1;
fi

FILE=$1

shift

/bin/clj -Sdeps "$(clj --eval '(fnext (read-string (slurp "'"$FILE"'")))')" -i $FILE -- "$@"
Then, ./foo.clj

Michael Lan 2021-03-25T22:48:01.456900Z

ah that’s very interesting, i like your approach

seancorfield 2021-03-25T01:16:55.421800Z

Happy to answer any specific questions you have about the Clojure CLI and its options.

seancorfield 2021-03-25T01:17:15.422Z

Have you read https://clojure.org/guides/deps_and_cli ?

seancorfield 2021-03-25T01:18:08.422200Z

TL;DR: -M means “execute clojure.main” and -X means “execute this specific function”

seancorfield 2021-03-25T01:20:05.422400Z

clojure.main can invoke a Clojure script directly — the time.sh case is almost the same as this command-line:

$ clojure -Sdeps '{:deps {clj-time/clj-time {:mvn/version "0.14.2"}}}' -M time.clj
if the file was time.clj and did not include the #! line.

seancorfield 2021-03-25T01:21:09.422600Z

The first example — with multiline deps and opts — is much more flexible but a little bit more setup.

Michael Lan 2021-03-25T01:34:20.422800Z

Ah, thank you! Yea I found that link but it seemed to have glanced over the details. Thanks so much for your detailed reply, I will try this!

nilern 2021-03-25T12:43:03.426200Z

Wow. The first one is quite the trick but isn't the second one just posix #! (and an unregistered reader tag I guess).

restenb 2021-03-25T15:38:53.428800Z

i have this long-running process that starts two go-loops to manage state while it runs. it's supposed to be started on demand by any client. is there a point to somehow "closing" the go-loops when each process completes?

restenb 2021-03-25T15:40:11.429300Z

i honestly don't even know how one would "exit" a go-loop.

nilern 2021-03-25T15:41:20.430300Z

Just stop looping. You can also request it to stop via a channel.

restenb 2021-03-25T15:42:12.431500Z

slaps forehead so just (if finished? nil (recur))

ghadi 2021-03-25T15:42:36.432500Z

lots of times people pass an input channel and a cancel channel to a go loop

borkdude 2021-03-25T15:42:42.432800Z

(when-not finished? (recur))

ghadi 2021-03-25T15:42:58.433300Z

if I signal is received on the cancel channel -> perform cleanup and exit

ghadi 2021-03-25T15:43:31.434500Z

sometimes you arrange these processes in trees, so that a process can signal to subprocesses the need to clean up and exit

nilern 2021-03-25T15:43:40.434700Z

But if the OS process is exiting there is not much that actually needs cleaning up. Threads, memory, even file handles will be wiped out by the OS anyway.

ghadi 2021-03-25T15:44:00.435100Z

It depends on the use-case, can't make a generalization

ghadi 2021-03-25T15:44:36.435700Z

e.g. you grabbed a lock, need to relinquish

nilern 2021-03-25T15:45:05.436500Z

Some C++ programs take a looong time to exit because they are running a bajillion pointless destructors

mpenet 2021-03-25T15:45:23.437200Z

it's not only destructors, sometimes it just matters to finish what you are doing cleanly

mpenet 2021-03-25T15:45:36.437700Z

like draining some job queue or whatever

1
dpsutton 2021-03-25T15:46:04.438900Z

(when-not finished? (recur)) this pattern requires a message after the "quit" signil though right? It's probably better to alt or some other mechanism that looks for a quit message in addition to a normal message

mpenet 2021-03-25T15:46:06.439Z

exit-ch is a good candidate for a promise-chan + alts!

mpenet 2021-03-25T15:47:01.439900Z

inside your loop just alts! over your input-ch + exit-ch and do the right thing from there

nilern 2021-03-25T15:47:13.440300Z

(when-not (&lt;! quit-chan) ... (recur))

ghadi 2021-03-25T15:47:33.441500Z

not that ^ because if your quit-chan doesn't signal anything you'll block

🤦 1
mpenet 2021-03-25T15:47:35.441600Z

that would park over your quit-ch

borkdude 2021-03-25T15:47:39.441800Z

(when-not was just a syntactic rewrite of the (if finished? nil ..) expression, not an answer as such to handling core.async loops)

mpenet 2021-03-25T15:47:49.442Z

hence the alts!

ghadi 2021-03-25T15:48:25.443100Z

closing the input channel is another implicit signal to quit

mpenet 2021-03-25T15:48:30.443400Z

true

alexmiller 2021-03-25T15:48:37.443900Z

when-some is probably better for most reading from channel cases btw

restenb 2021-03-25T15:48:53.444400Z

(poll! quit-chan)?

mpenet 2021-03-25T15:48:59.444600Z

or closed output-ch 🙂, there many ways to skin a cat

🙀 1
restenb 2021-03-25T15:49:00.444700Z

assuming it will signal at some point

mpenet 2021-03-25T15:49:12.445300Z

you can check the ret val of >! or <!

ghadi 2021-03-25T15:49:33.445700Z

@restenb (alts! [input cancel])

mpenet 2021-03-25T15:50:08.446100Z

(when-let [task (&lt;! input-ch)] ... (recur))

➕ 2
mpenet 2021-03-25T15:51:11.447400Z

"exit-ch" can be useful when you have to propagate that exit to other places, otherwise it's true than just checking ret vals of put/take can be enough

nilern 2021-03-25T15:51:14.447600Z

when-some as alex said

➕ 3
🙌 1
🎉 1
2021-03-25T16:02:22.448300Z

you can also propagate the close, by closing all the chans you have for writing - then you get a nice modular protocol for shutdowns, and it's flexible enough that you can shut down a sub-tree of your forest of processes

mpenet 2021-03-25T16:04:13.448500Z

yeah I meant more if you have various "components" that are not cooperating via chans otherwise

raspasov 2021-03-25T21:41:14.455500Z

I wrote this small lib to deal exactly with stopping/restarting go-loops. Uses a combination of atoms, poll! , and a loop-in-a-loop; Been using it happily for UI stuff in ClojureScript/React. Should work on JVM Clojure as well. https://github.com/saberstack/loop Note: A ss.loop/go-loop always exits on the very next (recur ...) call. It does not "die" automagically in the middle of execution.

raspasov 2021-03-25T21:42:44.456700Z

It is very nice for the REPL specifically, when you have some go-loops that you’re writing and once in a while you’ll get a “runaway” go-loop and you need to restart your process in order to stop it.