Fork me on GitHub
#clojure
<
2021-01-10
>
clayton00:01:21

Forgive me if this is the wrong channel to ask this, but I am having trouble understanding how to version my own library written in clojure using the clojure cli and deps.edn. When looking at other projects on github I notice that nowhere does a deps.edn file contain a project version, but instead the project version lives in a separate pom file where the project's dependencies are also listed. Can someone explain why that is? Is there a way to do this without a pom.xml file? Thanks!

seancorfield00:01:44

Clojure is a hosted language and leverages a lot of the Java and JVM ecosystem -- including Maven-style repositories for distributing libraries (e.g., http://clojars.org). That's why we're "stuck" with the pom.xml file.

seancorfield00:01:51

You can mostly ignore it. If you use depstar, you can tell it to generate a minimal pom.xml for you and keep it in sync with the dependencies in your deps.edn file. It uses the same machinery that underlies the Clojure CLI (`-Spom`) to do that.

seancorfield00:01:23

You can also tell depstar what your library version is for any given JAR you build, and it will update your pom.xml file for you.

clayton00:01:29

Does leiningen just do this behind the scenes then? I haven't seen pom files show up when looking through libraries built with lein.

seancorfield00:01:53

But it's also important to understand that Clojure libraries are nearly always packaged as source code, not compiled class files, and so you might just as well depend directly on their source code on GitHub, for example, which is something that deps.edn supports via :git/url and :sha dependencies (instead of :mvn/version).

👍 3
seancorfield00:01:18

Yes, Leiningen generates pom.xml files (and puts them inside the JAR file -- just as depstar does).

clayton00:01:38

I haven't heard of depstar before so that will be helpful! Thanks for the explanation Sean!

seancorfield00:01:20

Also https://github.com/seancorfield/dot-clojure/ for some helpful aliases for working with CLI tooling.

seancorfield00:01:26

As for Leiningen:

(! 525)-> lein new foobar
Generating a project called foobar based on the 'default' template.
The default template is intended for library projects, not applications.
To see other templates (app, plugin, etc), try `lein help new`.
(! 526)-> cd foobar/
(! 527)-> lein jar
Created /Users/sean/clojure/foobar/target/foobar-0.1.0-SNAPSHOT.jar
(! 528)-> jar tvf target/foobar-0.1.0-SNAPSHOT.jar |fgrep pom
  1948 Sat Jan 09 16:26:36 PST 2021 META-INF/maven/foobar/foobar/pom.xml
    56 Sat Jan 09 16:26:36 PST 2021 META-INF/maven/foobar/foobar/pom.properties

seancorfield00:01:19

The pom.xml file is used when deploying the JAR to http://clojars.org -- and also services like http://cljdoc.org lift a lot of information from it in terms of authors, version control links, etc. So the minimal file that clojure -Spom generates doesn't have any of that but if you use clj-new to create a new project, the pom.xml file is fully-populated with that information.

👍 3
emccue00:01:33

Also services like jitpack - which is the way to use github hosted dependencies seperate from :git/url. poms are everywhere and won't die anytime soon

clayton00:01:15

> "But it's also important to understand that Clojure libraries are nearly always packaged as source code, not compiled class files..." Why is this the case?

clayton00:01:37

Is there a situation when I'd want one over the other when using a library?

seancorfield00:01:03

AOT (compilation) can cause a lot of problems in libraries so we all avoid it. A library packaged as source code is likely to be compatible with a wide variety of Clojure versions. A compiled library may only be compatible with a specific version of Clojure.

clayton00:01:51

Oh ok - makes sense. Thanks again 🙂

clayton00:01:09

I'll get back to my repl

seancorfield00:01:14

In addition, AOT compilation tends to bring in transitive dependencies and compile them too (Leiningen goes to some lengths to try to clean up after AOT to avoid dragging in "the world" when you compile just a library so this problem is often unnoticed until further down the road).

seancorfield00:01:36

If you're building an application for deployment to production -- as an uberjar -- then it's fine to AOT everything as the last step in building that JAR but the only impact it really has is faster start up (because you're avoiding the initial compilation of Clojure source code when your app starts up).

👍 3
seancorfield00:01:52

(although, for years at work we didn't bother AOT compiling even our uberjars because startup speed was reasonable -- but over time as our apps gained more dependencies, it was worth AOT to speed things up for rolling restarts etc... but, along with direct-linking, it makes it harder to patch a running process via a remote REPL... which is something we still do in some of our production processes)

didibus03:01:21

Tools.deps and the Clojure CLI are not build tools, and don't manage versions, deployments, tests, etc. All it does is pull dependencies and launch your app with the correct dependencies passed to it for it to run.

didibus03:01:06

So if you want anything else, you need to use some other tool along with Tools.deps. That's where say depstar comes in (and there are others).

didibus03:01:22

Remember when they told you Clojure didn't like frameworks but favoured small composable libraries instead... Ya they were not joking ;)

kingcode03:01:30

is using varargs in protocol fns OK? Eg:

(defprotocol State 
  (update-state! [_ f & args])
  (reset-state! [_ v]))

didibus05:01:53

No, basically the & is treated as an ordinary symbol, and it is as if you defined:

(defprotocol State 
  (update-state! [_ f other args])
  (reset-state! [_ v]))
So the & act as a normal argument, like if you had wrote other in its place.

didibus06:01:25

A trick I've seen is to wrap the protocol function in a normal function:

(defprotocol State 
  (-update-state! [_ f args])
  (reset-state! [_ v]))

(defn update-state! [this _ f & args]
  (-update-state! this _ f args))

didibus06:01:05

You can also do it with a different namespace if you don't like having to change the name of the protocol:

(ns foo-protocol)

(defprotocol Foo
  (foo [this args]))

(ns foo
  (:require [foo-protocol :as foo]))

(extend-type String
  foo/Foo
  (foo [this args]
    (apply str this args)))

(defn foo
  [this & args]
  (foo/foo this args))

(foo "hello" "world" "lol" "hi")

didibus06:01:34

And I'd say that's the recommended approach for protocols. If you hadn't noticed, there's a lot that a protocol function doesn't support. You can't have have a spec, can't have a doc-string, etc. So actually providing normal functions that wraps the protocol functions is an idiom

Sam Ritchie21:01:21

Docstrings work, I think! (The point is of course valid)

kingcode12:01:58

Thanks @U0K064KQVand Sam for your much appreciated advice! I like the wrapping approach and will use that. Out of curiosity, where is the official documentation for this? I scanned http://clojure.org couldn’t find any thing.

didibus16:01:28

Seems that's mostly an undocumented quirk

didibus16:01:19

I highly recommend clojuredocs for a reference, it has user submitted examples and notes for functions which really help with edge cases

didibus16:01:38

There's a note about it there for example

didibus16:01:43

There's an open ticket to change the compiler so it errors if someone tries to use & but it's probably a case of too many fish to fry and it hasn't been merged in yet

kingcode19:01:43

Oh I missed that one! Cool many thanks 🙂

nivekuil09:01:51

is there a library for doing stuff with set semantics but over generic datatypes? something like

(defn difference [left right]   (when (set? right)     (cond (set? left)    (set/difference left right)           (vector? left) (filterv (complement right) left)           (seq? left)    (filter (complement right) left))))

Yehonathan Sharvit17:01:07

Has someone an idea how to walk a form (like clojure.walk does) but with passing to the function not only the value but also the path?

jjttjj17:01:07

I'm not 100% sure, but this reminds me of https://github.com/halgari/odin and I think it might have some similar functionality somewhere in the code

Sam Ritchie21:01:15

I have something for this!

Sam Ritchie21:01:55

Maybe not exactly what you want but give that docstring a look

Yehonathan Sharvit07:01:59

Looks very complicated :thinking_face:

delaguardo09:01:53

(defn walker
  ([f] (walker [] f))
  ([path f]
   (fn [v]
     (cond
       (map? v)
       (into {}
             (map (fn [[key val]]
                    [key ((walker (conj path key) f) val)]))
             v)

       (sequential? v)
       (map-indexed
        (fn [idx val]
          ((walker (conj path idx) f) val))
        v)

       :else
       (f path v)))))

((walker (fn [path val]
           (prn path)
           val))
 {:x [:0 :1 :2] :y {:v 3}})
something like this?

Sam Ritchie13:01:36

@U0L91U7A8 haha, maybe it needs a clearer treatment… but the idea is you keep a vector of the path as you walk, and then if you ever descend down into some indexed spot in the sequence, you carry the NEW path (ie the old path with the new index you dropped into conj-ed on)

Sam Ritchie13:01:00

my impl above was a little more complicated because I’m also carrying along the type of thing you descend into, instead if just the indices

Sam Ritchie13:01:10

(defn mapv-chain
  "Returns a new vector of equivalent shape to `v`, generated by applying `f` to
  two arguments:

  - the entry in the vector
  - a vector of its 'access chain', ie, the path you'd pass
    to [[clojure.core/get-in]] to access the entry

  For example:
  (doall (mapv-chain print [[1 2] [3 4]]))
  1 [0 0]
  2 [0 1]
  3 [1 0]
  4 [1 1]"
  [f v]
  (letfn [(walk [elem chain]
            (letfn [(process [i x]
                      (walk (nth elem i)
                            (conj chain i)))]
              (if (vector? elem)
                (into [] (map-indexed process elem))
                (f elem chain))))]
    (walk v [])))

Sam Ritchie13:01:14

here it is cleaned up for just vectors

Sam Ritchie13:01:39

I like @U04V4KLKC’s more general version 🙂

Yehonathan Sharvit13:01:31

Could you explain why you chose to implement a walker instead of a walk like this?

(defn walk
  ([f v] (walk f [] v))
  ([f path v]
   (cond
     (map? v)
     (into {}
           (map (fn [[key val]]
                  [key (walk f (conj path key) val)]))
           v)

     (sequential? v)
     (map-indexed
       (fn [idx val]
         (walk f (conj path idx)  val))
       v)
     :else
     (f path v))))

(walk (fn [path val]
          (if (coll? val)
            val
            [path val]))
        {:x [0 1]
         :y {:v 3}})

delaguardo13:01:02

Because returned walker function can be in composition of many other data processors.

delaguardo15:01:34

I’m more thinking about comp ) but I see no problems to use it in transducer