Clojure's on-ramp?

Just read: Paving the on-ramp

Where Java’s lead is talking about how they could make on-ramp for beginners to Java experts easier by possibly making entry into using Java easier, and making it you can learn concepts one by one with an easier linear curve.

It made me wonder what a similar thought experiment would yield for Clojure…

Thoughts?

2 Likes

That’s really interesting to see – and it would make so many “small” Java programs easier to get up and running with few, if any, downsides.

What does Hello, World! look like for Clojure at this point?

Start with an empty directory. Add hello.clj with just this line:

(println "Hello, World!")

and now run it with:

clojure hello.clj

I’m not sure it could get much simpler.

OK, so right now it complains that you need -M (but it still works without it) so perhaps that could be made cleaner.

If you need more than the “standard library” – for either Java or Clojure – then you need to deal with some sort of dependency specification and I think Clojure has Java beat there (just add deps.edn with {:deps {whatever/library {:mvn/version "something"}}} and run clojure hello.clj again – voila!).

2 Likes

That initial hello world might be very straightforward, though I think the online tutorials don’t make it sound this straightforward.

But I think the install process to get to that point, and also the steps after to say go from a hello world to a more involved program could be where Clojure could have better on-ramp.

In the Clojurescript side, even the hello world is quite difficult I’d say.

Anyways, I’ll have to think more about it, but I like this idea of on-ramp better then just talks of making a language easier, instead making a language have easier step by step to it’s full potential and mastery seems a much better approach, the language still ends up being what it is once you’ve mastered it, it’s not compromised to make it easier, but it allows a more linear progression from beginner to that fluency.

1 Like
npx create-cljs-project foo
cd foo
npx shadow-cljs node-repl # or browser-repl
(println "Hello, World!")

I think the CLJS Quick Start is reasonable, but I’m biased. Sure, it does get complicated if you want to add a full frontend with something like re-frame/reagent, CSS, backend servers. That is just the nature of frontend development.

2 Likes

You missed a step:

  1. Install npx (and explain why you need a Node.js tool installed in order to use Clojure/Script)

You linked to the Shadow-cljs Quick Start (which is fair since you’re promoting Shadow-cljs) but the first thing most beginners are going to see is the official ClojureScript - Quick Start and the first thing they’re confronted with there is a project structure and notes about downloading some JAR file for Windows.

For macOS/Linux, I don’t think installing clojure/clj is any more complex than installing Ruby/Python/etc – although I guess on Linux folks might lean toward apt/yum and that’s a much harder path to walk given the “everything must be compiled from source” mentality of most of the Linux package managers?

For Windows, yes, installation is still a fraught process (and discussions continue in #clj-on-windows on Slack about that today!). For developers on Windows, I always suggest WSL2 so that you’re back in the “macOS/Linux” world and most Clojure tutorials/books etc will “just work” but now you’re drifting further away from how to install “a language”.

As for going to a more involved program, you can “grow” your Hello, World! one step at a time – I just don’t think tutorials really cover this.

So we have hello.clj and we run it with clojure -M hello.clj (to avoid the warning).

Now we can add deps.edn with, say, {:deps {org.clojure/data.json {:mvn/version "0.2.6"}}} and update hello.clj to have:

(require 'clojure.data.json)

(println (clojure.data.json/write-str {:hello "world"}))

and you can still run it with clojure -M hello.clj and it will fetch data.json and run the code.

Then you could explain ns and change that to:

(ns hello
  (:require [clojure.data.json :as json]))

(println (json/write-str {:hello "world"}))

And when you finally need multiple namespaces, you can explain the src tree convention and move to something like:

src
|- hello.clj
+- world.clj

Now, that’s all just the files side of things – but it’s a lot more like how most other language tutorials work I think? The next step is the most important for beginners, IMO, and that’s to get an editor up and running with a REPL and learn to eval code from source files via the REPL rather than typing into it. I think we do a poor job there.

And then testing is, I guess, the next step which adds aliases and more dependencies/tooling, and then learning how to run tests via your editor and/or from the command line.

1 Like

npx is part of the node install. Don’t know why that needs to be explained, when you didn’t explain how to install clojure, or the original article java?

You can also add shadow-cljs to deps.edn and run clj -M -m shadow.cljs.devtools.cli browser-repl without ever touching node. It is entirely optional and was never a requirement, just convenient and something most CLJS devs actually want access to.

5 Likes

FWIW, as someone started on Clojure with a JS background, I would say the Shadow-cljs Quick Start is easier to comprehend and more useful than the official ClojureScript - Quick Start.

5 Likes

I think ‘deps.edn’ is a bit more cryptic then it really needs to be

Once ‘add-libs’ lands in clojure, you’ll be able to add dependencies straight from your .clj file which should streamline minimal examples.

I think making ‘:paths’ defaulting to ‘src’ was mistake. It’s not obvious at a glance that ‘src’ is some special directory name that’s baked into the language. It’d have been cleaner if it defaulted to local directory ‘.’ and didn’t impose a directory structure on you.

You could then easily make one-word namespaces (which maps to the ‘.clj’ file names) and make flat projects with no folders. So in intro material you also skip all the added “complexity” of the dot notation mapping to file hierarchies. It just makes everything look much simpler and cleaner. The most basic examples in a language shouldn’t give you the feeling like you’re setting up a visual studio project…

4 Likes

The benefit of deps.edn over add-libs is tooling – the former is declarative and lots of tooling can be built around being able to read the standard file and perform a variety of tasks, including things like cljdoc.org for example. Using add-libs in code means that tooling really doesn’t have a chance to figure out what dependencies are used. This was a problem for Boot and why 3rd party tooling never really supported it properly.

src isn’t “baked into the language”. It’s a convention in Java and many other JVM languages for project structure and it was the default structure for Leiningen when that first appeared (over a decade ago). As I noted above, you can have a single segment name in the top-level folder and still run code without needing any project structure (or even a deps.edn file). You can go quite a long way with that.

When you want to add tool support to your simple, flat project, you can add deps.edn with {:paths ["."]} and whatever :deps you need and you’re ready to go.

I agree that intro material and tutorials could start that way (I specifically said that).

1 Like

I think making ‘:paths’ defaulting to ‘src’ was mistake.

This was something we talked at length about and there were really three concerns - getting started, evolving a project, and integrating with existing projects.

For getting started, yes - making . the default path would have been the easiest possible thing. But what about the next step after that? We wanted something that was both easy to get started (no deps.edn file is required at all) but that led you in the direction of a real project. With namespaces mapping to directories you will quickly be making directories regardless, and every typical project will have a directory for src (which is also pretty typical for many other language communities too if you’re coming from those). So requiring you to make that directory seems a) not hard and b) pretty normal for most devs. And then additionally, using src/ as the default directory meant many existing projects automatically worked as well, which was important when introducing a new tool.

All in all, I have no regrets about this choice.

On the learning ramp, the deps guide Clojure - Deps and CLI Guide does attempt to take you through a process with knowing about a) repl, b) deps, and c) project structure (and even local deps and how this works with git). There are some things I’d like to improve here and we have plans to integrate this better with the getting started pages (which were also reorganized this year), but still have a lot of planned worked to do.

3 Likes

Just btw, I am actively working on add-libs stuff right now for 1.12 timeframe. It will likely morph a bit and likely move closer to core in part. One of the main things we want to discourage though is adding libs directly from your .clj file and eschewing the use of deps.edn. When you do that, you lose all the benefits of storing your deps in a data file.

We have talked about some pretty suprising extensions to Clojure that would support this kind of thing, but again, we’re worried about encouraging that use case too much in lieu of deps.edn etc. There are places where it’s interesting though so you never know.

7 Likes

Wait a second, you mean that you just drop in any folder, create a src dir, put some code and run Clojure and it’ll run it?

I guess I never thought about that.

But I do feel this seems weird, as opposed to creating a file and doing clj -X ..., putting it in src here seems counterintuitive.

Anyways, I don’t mind the src default, mostly because I think it’s good to have a widespread convention for what the name of the source folder in a project should be, and I hate when I go to some project and I’m having to look through many folder and try and figure out which one is the source. So I think this default makes sense.

In practice I find the beginner ramp-up seems to actually be:

  1. Create a .clj file anywhere, write some code, run it as a script: clj myfile.clj
  2. Want a dependency, modify the ~/.clojure/deps.edn, and run your script again: clj myfile.clj (it seems beginners do better starting with global deps first, especially if coming from Python)
  3. Outgrew the single file? Install deps-new tool, and use it to create a project, copy/paste code from myfile.clj into new project main source file.

So ya, don’t think default src dir is really an issue.

Do we really need safeguards? This seem like it can just be a best practice.

In scripts I’ll want to put deps in the .clj, that’s half the point. Obviously I’d not do that for a lib or app. The other use of it being for Repl development.

2 Likes

Thanks for taking the time to explain the rational. Its always really nice to understand the thought behind things

With namespaces mapping to directories you will quickly be making directories regardless

if you don’t use dot notation it doesn’t right? Or maybe I’m missing something

I have several small utility libraries that have single word namespaces and everything is in a top level directory. If they don’t use external libraries I end up having to always still add a deps edn that reset the src path

I guess on reflecting on things more, what feels off-putting is that the ‘src’ folder sort of starts to imply a “project” (in the big bloated IDE sense) with doc folders, license files and whathaveyous. While you kinda want the code parts to only deal with code and the extras being separate. You can keep everything in one big git repo if you want, but it feels like the code shouldn’t inherantly care about that.

As didibus points out, the idea of having a project folder with just a bare src folder and no ‘deps.edn’ - and it magically works! also feel weird. If anything that’s even a bit confusing

Anyway, it’s just a vague feeling probably colored by my experience working in c++ :))

Super happy it’s going to come to core! Thank you so much for making this great tool

As I think we talked about elsewhere, my main usecase is to have code that can be copy-pasted and run either by the reader of some tutorial or in a GitHub issue

Especially since we’re running in the JVM you can get to a point where you construct a minimal example of something that uses external libraries and you can be pretty confident everyone can copy-paste-run it in their REPLs and get the same exact behavior

It significantly lowers the friction when talking about code

In more step by step notebook-like or literate code, it’s also helped me introduce libraries/dependencies incrementally - which has helped in the narrative

Thank you once again :))

1 Like

If you’ve got a top level script with no dependencies, can’t you just: clojure myscript.clj to run it?

1 Like

Right, if it’s a one file script and you wanna execute it then that definitely works. But if you have multiple files/namespaces then does it? I don’t think it does - but I might be wrong. Can your myscript.clj require/use a file right next to it? I’ll test next time I’m at my computer :))

I guess I’m usually not running scripts. What Im typically doing lately is i break out components into separate small libraries. So for instance I needed to write a little wrapper to read in GeoTIFF files, but it’s not really project-specific and I can reuse it. So Id just open a new file ‘mygeotiff.clj’ in a new top level directory ‘mygeotiff’ and I’d write the code there in a ‘mygeotiff’ namespace. In my main project’s ‘deps.edn’ I then just import it using the library-path method and restart my repl. I have a dozen or so small libraries of that nature - each with like a couple namespaces. It’s really quick and seemless to work this way and almost as smooth as adding another namespace in the main project (the only drawback is restarting the repl)

Sometimes I reuse the code in another project but even if I don’t - it keeps me honest and forces me to keep the code decoupled :slight_smile:

Obviously it’s not a huge inconvenience to have to drop in an additional ‘deps.edn’ with a ‘.’ path (often I’ll have an external dependency I’m wrapping as well - so I need one anyway) - but running against the default src directory and dot notation - so I do feel like I’m “holding it wrong”