Hacker News new | past | comments | ask | show | jobs | submit login

Note to non-Clojurians:

Most people just use Leiningen and don't mess with this stuff.

This post does not paint a good picture of Clojure development tools, but don't get turned off by it.

99% of Clojure projects I've worked on just have a declarative project.clj file and building it is as simple as:

    $ lein uberjar
I will never understand the core team's acute NIH Syndrome and tendency to fragment the community, but I guess Open Source Is Not About Me.



Hard agree here. As a full time Clojure dev, my experience is that lein "just works". I've also used the Clojure Tools professionally, and have seen the dev team lose countless days of what would be otherwise productive time putzing with it, building infrastructure that lein has had out-of-the-box for years, and trying to make sense out of its inscrutable docs.

Want a project with both Clojure and Java? Want tests that magically integrate with CIDER's test functions? Complex build profiles for different target environments? Lein makes this stuff as painless as possible, whereas these things are possible with Clojure Tools, but have fun wasting time messing with it and doing awkward stuff like inlining strings of eval-ed clojure code in your build file. With Leiningen these are solved problems.

I also find the core team's behavior perplexing. I'd argue the messy tooling they've been producing has probably been net negative value add to their community. Working downstream from this stuff has degraded the dev experience. Thankfully, it can be safely ignored.


I agree. To me tools.build is not a replacement for Lein at all. Lein is more intuitive to most beginners. It's much easier to tell someone to run `lein new some-app` and `lein run` / `lein uberjar`, than to tell them to configure their deps.edn file so they can run some alias with the `-X:...` / `-M` arg to create and run a new project (and who knows which dependency to use / configure to build an uberjar).

For experienced Clojure developers this might seem trivial, but new people (in my experience) are very impatient and want to get started ASAP. Honestly, who can blame them? Why does it have to be so difficult in this day and age?

Another thing I hear Clojure developers say is that beginners can start with Lein, but once you get more experienced with Clojure you can switch to tools.build. Why does a build tool need to be so unusable that you can only use it when you become more familiar with the language? You don't see that with Ruby's Bundler, or Elixir's Mix. They just work, just like Lein does.


> RICH HICKEY: I think that, collectively, we are infatuated with these two notions of easy. We are just so self-involved in these two aspects; it's hurting us tremendously. Right? All we care about is, can I get this instantly and start running it in five seconds? It could be this giant hairball that you got, but all you care is, can you get it.

Simple Made Easy is what attracted me to Clojure, and tools.build, tools.deps, tools.cli are aligned with this mission. Cognitect is doing exactly what they said they would do and have been doing all along.

Re-read the talk here: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...

(PS, a note to Cognitect comms – maybe start here with your next blog post and see if people react differently!)


You forget that on the ground-floor, where programming languages compete for mind-share, easy matters. In fact easy, in terms of getting something working, trumps difficult simplicity any day of the week. Easy is part of what makes an employer want to use your technology.


> In fact easy, in terms of getting something working, trumps difficult simplicity any day of the week. Easy is part of what makes an employer want to use your technology.

The whole point of the talk is that choosing "easy" (as in easy to get started) solutions while ignoring the complexity will be harder to maintain in the long run over choosing the "simple" solution that takes a bit more time to set up. Personally, this tracks pretty closely with my experience.

(Relevant slide from the talk: https://res.infoq.com/presentations/Simple-Made-Easy/en/slid...)


To get started with clj, you need 0 files. From a project directory, you can just type `clj` and get a REPL that includes Clojure and source files in src/. You can add new source files and immediately invoke them with `clj -X`. You can push that project to github and others can immediately use it from another project using a github url/sha. This is far "easier" than leiningen for getting started. The comparative experience there requires creating a project.clj with a bunch of keys, building a jar, getting an account and keys on clojars to deploy it, etc.

There's lots of other things you can do with clj - these are all additive. At some point, you might (possibly) need a build script. When you do, it's written in Clojure using a pretty straightforward set of functions and you can copy/paste in a starting point. You can grow that build file forever.


Bit bold to assume that the people making these decisions "forgot" anything. Design's about trade-offs.

The claim here is that the thing that makes your language easier to pick up initially nonetheless hobbles it for long-term use - refactoring, adding features. There is a business case for making the latter easy at the expense of the former.

Code can be easy or hard to write and/or change at various levels of complexity, and it's hard to solve for everything when designing a language.


that is completely true and maybe even clojure’s achilles’ heel, but you can’t come here and say they didn’t tell you


Rich can try to redefine the English language all he wants, but he'll find it a lot more difficult than redefining Clojure.

The fact is, the solutions the core team have been putting out for the last several years have been neither simple or easy, and in fact tend towards a weird and arcane complexity in service of problems no one seemed to have.

I remain baffled by Spec, by the clj tool, by deps, and now this. At every step it seems as if the team lives in their own world, and not in ours, and each new tool is as cryptic and convoluted as the last.

They frequently don't even seem to encourage code that is consistently idiomatic to how the rest of us even write Clojure. Who else was even using namespaced keywords for anything until Spec decided to not only use them, but link the entire mechanism to a mutable namespace registry? How is that "functional" in any way?


> and tools.build, tools.deps, tools.cli are aligned with this mission.

No, they are neither simple nor easy. They make new project setup un-necessarily hard, they make builds un-necessarily hard and complicate stuff for both newbies and intermediate programmers. Only experts with tools.* may be comfortable with this and I have seen these experts actually copy sample template `deps.edn` for new projects and use long complicated aliases.

For both simplicity and ease-of-use this should be done by a tool and not a human.

When I recommended some friends to Clojure, they stopped after getting stuck in deps.edn. Clojure setup feels more complicated than C++/Java/Kotlin/Python project setup nowadays.

I wish Clojure would use Rust's Cargo for inspiration.

`cargo new --bin projName` and open the folder in your editor/IDE of your choice.


We migrated from Leiningen to Boot and then to the CLI/deps.edn. Leiningen was too rigid and too much ceremony to write new tasks for. Boot was great from the "writing new tasks" point of view but the fileset abstraction and the pod pooling were both pain points for us. We've been very happy with the CLI and deps.edn and we're already using tools.build at work to streamline our (Clojure) build script.

We have a fairly large codebase (113K lines) in a monorepo with over a dozen artifacts built from about fifty subprojects. Leiningen really wasn't a good fit for us so we're very glad of alternatives.


Thank you for the perspective (and also for clj-new — it feels like it should be built into Clojure’s own tooling).


Now that tools.build is here, I think clj-new can be substantially simplified (copy-dir with text replacement is most of what clj-new does for built-in templates) and when add-libs gets to the main branch of t.d.a or Clojure itself, that takes care of a lot of the rest of what clj-new does.


> I'd argue the messy tooling they've been producing has probably been net negative value add to their community.

I feel the same way. It has definitely hurt the ecosystem.

Every Clojure IDE, for example, has to support a new deps/build system that is still a WIP and doesn't come with batteries.


Start a REPL; connect to it. That's identical for every single editor/IDE. The problem is that editor plugin maintainers seem reluctant to encourage developers to start REPLs manually so they have to deal with the whole issue of "how to start a REPL" -- which should be up to the developer, not the tool.

I run REPLs for weeks, sometimes months, so I don't want my editor to start my REPL since my editor is going to get restarted a lot more often than my REPLs!


Leiningen is over 10 years old and has tons of pain points. For example, I bet you're not using ClojureScript, which is a mess of plugin debugging in leiningen. The goal of tools.build is to let you code your builds as simple functions that run at the REPL, which is awesome. No more finding misplaced keys in soup like https://github.com/technomancy/leiningen/blob/master/sample.... . If leiningen works for you, great, keep using it!


What pain points are those? Last I checked, doing cljs on Clojure Tools wasn't even possible. If that's still the case, then that's not a valid comparison (complex vs impossible).

Also, that's the master version of project.clj, which is a superset of all possible features lein supports. Nobody's project.clj file looks like that, though I suspect you know this. Should we compare that complexity to a monster build.clj and this master deps.edn:

https://github.com/seancorfield/dot-clojure/blob/master/deps...

Lein is also a singular tool vs. a murky interaction between a suite of tools (tools.deps with deps.edn, Clojure CLI, and tools.build).


> Last I checked, doing cljs on Clojure Tools wasn't even possible.

I'm not sure what this is referring to here. The "Getting Started" guide for ClojureScript uses deps.edn and the clojure CLI to introduce ClojureScript. You can certainly use figwheel or shadow-cljs with clojure tools.deps and deps.edn. And since the new build.tools allows just running a program, you can of course build clojurescript from a `build.clj` build script. I cannot think of a step in this process that isn't amenable or simplified by the use of the clojure cli, tools.deps, and now build.tools.


That isn't a project deps.edn file though -- it's a compendium of tools that are available for the CLI/deps.edn and I put it together as a working example so folks coming to the CLI/deps.edn stuff could see the broad range of tools available and could copy'n'paste any useful bits they want.

The README explains that and even links to a better-documented and more carefully curated set of aliases that folks might want to use instead (the Practicalli repo).

In reality, I use very, very of those aliases from that dot-clojure file, because the projects I work with already have the ones I need for working on those projects. That can be as simple as a :test alias or it can be up to a dozen aliases used in variations combinations for a variety of tasks.

You can certainly use the CLI and deps.edn for ClojureScript, by the way -- check out Figwheel Main which uses deps.edn and provides a nice workflow.


Thanks for the reply. While you're here, I'll say that your resources for the CLI stuff made it possible to use those tools at all (for my current Clojure team) several years ago. depstar also is a great resource.

We had to scour the internet and engage in weeks of teeth grinding to get CLI/deps to do all the things our project needed.


Thank you. As someone who follows the (b)leading edge of Clojure and its tooling very closely -- and has run alphas of Clojure itself in production since 2011 -- it's very hard for me now to put myself in the shoes of someone coming new to this tooling (unfortunately).

I think perhaps the biggest issue for most people is discoverability: how do I learn about the tools I need to get "task X" done with the Clojure CLI, deps.edn, and associated "stuff".

Even though I created clj-new (and boot-new, on which it is based, which in turn was based on lein's new), I pretty much never use it myself, except for quick examples to test stuff and/or help beginners with. I always start a project from scratch with an empty deps.edn and an initial src/myns/myfile.clj or similar.

I use depstar heavily but I strongly suspect that it will ultimately go away as tools.build matures to the point where all the special cases that depstar handles today are folded into t.b.api/jar and t.b.api/uber -- I don't want to maintain something that duplicates what the core team have poured so much time, effort, and careful thought into!

I'm already contemplating a "reboot" of clj-new as a very simple wrapper around t.b.api/copy-dir with some simple templates -- since that's most of what clj-new does (even the more complex template-running stuff is really only a slight augmentation of t.b.api's create-basis + java-command + process).


That should read "I use very, very FEW of those aliases"


To be clear, you are pointing to this as an equally contrived example as what ‘dustingetz linked to, correct?


Correct.


That file says at the top:

  ;; This is an annotated reference of the options that may be set in a
  ;; project.clj file. It is fairly contrived in order to cover lots of
  ;; different options; it shouldn't be considered a representative
  ;; configuration.
The comment from ‘bm3719 makes tools.build sound like a less useful solution when compared to the practicality of leinengen, and your reply only has ClojureScript in its favor if I trust that the linked sample project.clj is indeed not representative (which matches my limited experience with my own “useful toy” projects and the other projects I have browsed). “awkward stuff like inlining strings of eval-ed clojure code in your build file” sounds really bad, but that could be equally contrived for all I know.

I also don’t understand the value of running builds from the REPL. What kind of developer process makes that a useful thing to have? What am I missing?

These are genuine questions, not advocacy for one thing or the other. I want to work professionally with Clojure(Script) enough that it is a prerequisite for me to consider changing jobs. I’ve only used lein thus far, so I don’t have enough experience with either tool to share a meaningful opinion here. I am extremely curious about the state of things though.


Running builds from the repl let’s you skip the startup cost associated with Clojure. It’s faster if you need to run more than build command, or one build command several times.

Lein is easy to get started with, and for simple projects does everything you need. tools.build is endlessly flexible. For some projects, tools.build/tools.deps was much easier for me to use than leiningen (easier to just write clojure code directly than figuring out the whole lein plugin stuff). Use whatever works for you.


I've tried deps several times, but I keep coming back to leiningen. Can anyone explain the advantage of using deps to me?

What I want from a build tool is: manage dependencies, starting a repl, running tests, and building a jar. Leiningen provides me this: lein repl, lein test, lein uberjar. Simple and exactly what I need, and importantly: i can expect that all leiningen projects use the same commands.

Deps can be made to do all these things, and more, but you need to configure it. Which test runner should I use for tests? Which library should I use to build an uberjar? Since it's all configurable, I risk that every deps project gets it's own special snowflake build setup, and I need to spend time avoiding this.

Until deps gets a standard way for building and testing, i'm sticking to leiningen.


Exactly. Lein is like maven, declarative and just works in a standard way for 95% of use cases. Deps is like ant, can do anything, super flexible, but you need to start from scratch actually programming your build for every project.


It's like we learned nothing from years of suffering with snowflake Ant builds.

Then again, gradle builds seems to be more popular than maven these days.


That is what happens when a new generation has grown without Ant and then a giant behemoth with endless cash for top hardware gets a deal to push Gradle as the official build tool for their mobile OS.


I would think that Gradle is a lot more pleasant to use than Ant...


Not for those that don't suffer from XML allergy, so given that, the outcome is almost the same, with the caveat that Gradles uses a ton of more resources than Ant, as it needs its background daemon sucking up 2GB to pretend being fast.


Same. I don't want to spend time learning build tools and configuring builds - I want to Get Shit Done.

Lieningen gets out of my way and lets me do the things that really matter, and I'm not switching till I see some clear advantages of the new tooling.


I first used deps when doing a project with Datomic Cloud.

I like deps, but I can’t quite articulate why.


After using it with Datomic Cloud, do you prefer it also for local projects? Is there something that happened with Datomic Cloud configuration that made it “click” for you elsewhere? Did you just have to use it there and that experience informed your preferences?

Data > functions > macros is a core concept of Clojure. Does that come into play here?

These are prompts from a curious outsider’s perspective.


I do.

I guess it boils down to the fact that I don't find it significantly harder to use than Leiningen, there are great libraries and tools that work well with it (shadow-cljs can source its deps from deps.edn for example), and I wrap the entire thing in a task system anyway (lately, Babashka tasks).

I guess it helps that its the "official" way to do things, but that's not a primary concern for me.


Thanks. I hadn’t even considered Babashka, which I’m quite intrigued by.


I found an old thread that examines some of the early usage of deps. Data > macros does indeed seem to be a driving force: https://clojureverse.org/t/combining-tools-deps-with-leining...

tools.deps makes better choices when dependency specs conflict, so I would suggest trying https://github.com/RickMoynihan/lein-tools-deps if you otherwise want to use lein, and get the best of both worlds.

By better, I mean:

> Leiningen and Maven, when there is a conflict always pick the version that is closest to the root of the dependency tree; where as tools.deps always picks the newest.


Leinengen vs Maven dependency resolution is pretty distant to my concerns. I may be in the minority there, but I don’t think I am. I guess the thing I’m struggling to understand in the build tool differences is:

Leinengen’s (defproject) takes a series of keys and values. It is declarative. You can probably get into dependency hell if your config is complex enough, and I’m almost certain it’s doing more than I understand under the hood. (like why doesn’t it just take a map instead of being a macro?)

tools.deps OTOH appears to need both a bunch of top-level definitions in a namespace (instead of a map literal) AND a hand-rolled build.clj file with custom functions to do most of the things that leinengen considers to be boilerplate. This does imply that you have much more flexibility with tools.deps, but it also raises the bar for entry, and it doesn’t appear to be as data-centric.

If I want to do ANYTHING meaningful with Clojure, I need a project environment to start with. I am not confident about how to do this with clj, even after reading https://clojure.org/guides/deps_and_cli#_writing_a_program Maybe I just haven’t found the right guide, or maybe clj wants me to commit more to working REPL-first than my dev environment (VS Code + Calva) has good support for.

What I’m wondering now is, if the gains that tools.deps provide are worth the entry price, does it make sense to have something like “create-react-app” to generate a project directory with sane defaults that can be customized if/when needed, if your project doesn’t fit the 90% case? Does this already exist?

‘dustingetz linked the Simple Made Easy talk. I get the distinction between the two, and I don’t want to swallow the hairball. I’m already sold on the Clojure paradigm.

But I ALSO still want to “get this instantly and start running it in five seconds,” and I don’t understand why I’m not allowed that if I choose tools.deps. I don’t think the two goals are more than accidentally orthogonal.


You keep saying tools.deps but I think you mostly mean tools.build here.

For the CLI -- which uses tools.deps under the hood (not the brand new tools.build) -- all you need is your deps.edn file: a declarative hash map describing your dependencies and, under aliases, any additional tools you want to use.

The build.clj file is something some folks have already been doing in one form or another for large, complex projects (and would be familiar to anyone using Boot instead of Leiningen).

tools.build provides some common tooling and structure that should help reduce the complexity out there by providing a standard way to do a number of tasks that folks were already doing a different way (yes, including some of the core Leiningen tasks) and to provide a composable, function-based set of tools to help you write your own build scripts.

The bottom line: if Leiningen does what you need, great! Keep using it. If you find Leiningen to be restrictive and not a good fit for your project -- as we did, years ago -- then tools.build might be better. We left Leiningen for Boot many years ago and if we hadn't started to run into bugs/issues with Boot at scale we'd probably still be using it. Instead we switched to the CLI/deps.edn back in 2018 and we've just adopted tools.build which has simplified some parts of the build script we already had.


i agree that it’s possible to have both, but you have to start with simple, and as he said in the talk which we just reread, easy is often relative to your local priors

also those custom hand rolled fns are … a library, which is way better than a build plugin


Thanks. I didn’t even know that leinengen has plugins.

If tools.* is the language maintainers’ preferred method, I think there is value in having a single command to set up a “simple/default Clojure project” in a folder, even if that only cuts the commands down from four to one.

  $ mkdir hello-world
  $ cp deps.edn hello-world
  $ cd hello-world
  $ mkdir src
I don’t understand where that deps.edn comes from in a fresh project. It’d be great to have “clj create hello-world” build one for me in ./hello-world, use my running Clojure version as a dependency (if that’s how “clj” works), create the child src/ (and /test if that’s a best practice) folders, etc. The idea being to generate a local library on-demand with sane boilerplate defaults and almost no knowledge. I can learn later what Else I might need if I can get it up and running quickly first.

Leinengen looks easier to me for my admittedly trivial use cases, because I don’t need a library or plugins at all. And again, I might be in the minority. But if folks keep saying “lein handles 90% and is easier,” and if we can make tools.* handle those same 90% cases trivially, it seems worthwhile.


If you're using the latest (prerelease) version of the Clojure CLI:

    # this is a one-off step to install the clj-new tool:
    (! 646)-> clojure -Ttools install com.github.seancorfield/clj-new '{:git/tag "v1.1.324"}' :as new
    Installed new
    # now you can run it anywhere to create new app or lib projects:
    (! 647)-> clojure -Tnew app :name myname/myapp
    Generating a project called myapp based on the 'app' template.
    (! 648)-> tree myapp
    myapp
    |____.gitignore
    |____.hgignore
    |____CHANGELOG.md
    |____deps.edn
    |____doc
    | |____intro.md
    |____LICENSE
    |____pom.xml
    |____README.md
    |____resources
    | |____.keep
    |____src
    | |____myname
    | | |____myapp.clj
    |____test
    | |____myname
    | | |____myapp_test.clj
This is already set up with testing and JAR building for you:

    (! 649)-> cd myapp
    (! 650)-> clojure -X:test
    
    Running tests in #{"test"}

    Testing myname.myapp-test

    FAIL in (a-test) (myapp_test.clj:7)
    FIXME, I fail.
    expected: (= 0 1)
      actual: (not (= 0 1))
    
    Ran 1 tests containing 1 assertions.
    1 failures, 0 errors.
    (! 651)-> clojure -X:uberjar
    [main] INFO hf.depstar.pom - Synchronizing pom.xml
    Skipping paths: resources
    [main] INFO hf.depstar.aot - Compiling myname.myapp ...
    [main] INFO hf.depstar.uberjar - Building uber jar: ./myapp.jar
    [main] INFO hf.depstar.uberjar - Processing pom.xml for {net.clojars.myname/myapp {:mvn/version "0.1.0-SNAPSHOT"}}
    (! 652)-> java -jar myapp.jar 
    Hello, World!


{:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.10.0"}}}

that's it


You don't even need that -- those are the defaults for the CLI.

    (! 639)-> mkdir fresh && cd fresh
    (! 640)-> mkdir -p src/myns
    (! 641)-> echo '(ns myns.myfile) (defn foo [{:keys [name]}] (println (str "Hello, " name "!")))' > src/myns/myfile.clj
    (! 642)-> clojure -X myns.myfile/foo :name "filoeleven"
    Hello, filoeleven!
No deps.edn file needed -- until you want to add extra dependencies etc.


I haven't used Datomic at all but I do prefer the CLI and deps.edn for all my projects. I really haven't used Leiningen for years at this point -- I mostly only use it now when I'm helping a beginner debug some problem they've run into with Leiningen plugsin.


deps is extremely simple, which means very little time spent debugging, and it never breaks


I have never stressed over or debugged Leiningen in my 6 years of Clojure experience.

Not sure what you're on about.


deps can't produce stable builds as it insists dependencies are not ordered. Yet on the classpath they are. That order shouldn't matter, but it very well does.

lein AFAIK produces the same classpath every time.

deps: Maybe put the paths first and then let's gamble what a seq on a map produces...



thanks, didn’t know that, will definitely be annoyed when i hit that someday


Ya, this accords with my thoughts when I first saw this bubble up, I don't understand the benefit over just using lein, which is very easy and straightforward (for my use case). I thought maybe this new approach is needed by people with complicated builds? My own builds have been quite simple so far, but I'm new to Clojure.


I have worked on pretty large Clojure projects, and found Leiningen to be perfectly adequate.


Although I prefer Lein at this point one of its disadvantages not mentioned here is that it requires its own JVM process which means you're running 2 JVMs during development.


but probably good and bad. pretty tough for one namespace to bleed into another due to a bug or incorrect config.

bad, obv, because all the RAM the JVM needs.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: