Clojure promises unprecedented productivity. Its devs boast top salaries.
It's true! But before you ever get to that point you'll face unprecedented confusion.
Understanding Clojure's tooling is very challenging.
Most guides push you straight into writing Clojure and don't bother explaining its complex foundations.
This is different.
I will NEVER tell you to go do something yourself. You'll be able to complete this guide 100% uninterrupted on any recent Windows machine. ZERO knowledge required! Nothing will be installed permanently!
We'll get java, discuss java stuff (JARs, the classpath, Maven), play around in Clojure's REPL, install lein, explain what lein really does, and make a simple Clojure program that counts most frequent words in dev.to headlines.
Follow along carefully, don't skip any parts, faithfully execute every command as I do!
Copy-and-paste PowerShell commands1, but when evaluating Clojure please retype yourself!
In only 10 minutes you'll be spared weeks of frustration.
Open up a PowerShell (press
Windows key, type
powershell then press
Enter). Please, don't close it till the end.
Ok, let's get started!
What is Clojure
Clojure's homepage has this to say:
Clojure is a dynamic, general-purpose programming language, combining the approachability and interactive development of a scripting language with an efficient and robust infrastructure for multithreaded programming. Clojure is a compiled language, yet remains completely dynamic – every feature supported by Clojure is supported at runtime. Clojure provides easy access to the Java frameworks, with optional type hints and type inference, to ensure that calls to Java can avoid reflection.
Clojure is a dialect of Lisp, and shares with Lisp the code-as-data philosophy and a powerful macro system. Clojure is predominantly a functional programming language, and features a rich set of immutable, persistent data structures. When mutable state is needed, Clojure offers a software transactional memory system and reactive Agent system that ensure clean, correct, multithreaded designs.
I hope you find Clojure's combination of facilities elegant, powerful, practical and fun to use.
author of Clojure and CTO Cognitect
But what is it really?
On the most fundamental level Clojure is just a Java program.
Before we begin we need to get
We will get ourselves an OpenJDK implementation of Java 11.
Even if you already have
java, please follow along. Not going to affect your existing installation.
PS C:\Windows\system32> cd ~ PS C:\Users\adas> mkdir clojure PS C:\Users\adas> cd clojure # 187MB download PS C:\Users\adas\clojure> wget https://download.java.net/java/GA/jdk11/13/GPL/openjdk-11.0.1_windows-x64_bin.zip -OutFile java11.zip # might take a minute or two to download. OpenJDK is almost exactly like Oracle's JDK, # for our purposes there's no real difference. # The JDK (Java Development Kit) includes all the necessary binaries to work with java, # like java, javac, javah, javap and so on... Writing web request ... .... # load up .NET stuff for extracting Zips into PowerShell (no need to understand) PS C:\Users\adas\clojure> Add-Type -AssemblyName System.IO.Compression.FileSystem # we extract the zip we just downloaded PS C:\Users\adas\clojure> [System.IO.Compression.ZipFile]::ExtractToDirectory((Join-Path $PWD "java11.zip"),$PWD) # rename the extracted directory to java11 PS C:\Users\adas\clojure> mv jdk-11.0.1 java11 # we don't need the zip anymore PS C:\Users\adas\clojure> rm java11.zip PS C:\Users\adas\clojure> java11\bin\java -version # make sure the binary works openjdk version "11.0.1" 318-1-16 # it does ... # temporarily update our environment PATH variable so we can access java everywhere PS C:\Users\adas\clojure> $env:path+=";" + (Join-Path $PWD "java11\bin") PS C:\Users\adas\clojure> java -version # did it work? openjdk version "11.0.1" 318-1-16 # it did ...
java is ready.
Now we can finally get ourselves Clojure. Like most Java programs it ships as a JAR.
JARs are basically ZIP files.
Most code and code-like stuff in the Java world is distributed as JARs.
So let's get a JAR for Clojure.
PS C:\Users\adas\clojure> wget http://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.jar -OutFile clojure.jar # let's take a quick peek inside the jar # (notice how we use a function for extracting Zip Files, because JARs are just Zips) PS C:\Users\adas\clojure> [System.IO.Compression.ZipFile]::ExtractToDirectory((Join-Path $PWD "/clojure.jar"), (Join-Path $PWD "jar-disassembly")) # we extracted the jar to jar-disassembly, normally you don't ever manually extract a jar # but we really want to see what's inside # let's see the general dir structure PS C:\Users\adas\clojure> tree jar-disassembly ... ├───clojure │ ├───asm │ │ └───commons │ ├───core │ │ ├───protocols │ │ └───proxy$clojure │ │ └───lang ... └───META-INF └───maven └───org.clojure └───clojure # now let's see full paths PS C:\Users\adas\clojure> tree /F jar-disassembly ... │ │ PersistentHashMap$INode.class │ │ PersistentHashMap$NodeIter.class │ │ PersistentHashMap$NodeSeq.class │ │ PersistentHashMap$TransientHashMap.class │ │ PersistentHashMap.class │ │ PersistentHashSet$TransientHashSet.class │ │ PersistentHashSet.class │ │ PersistentList$EmptyList$1.class │ │ PersistentList$EmptyList.class │ │ PersistentList$Primordial.class │ │ PersistentList.class ... # interesting, mostly a lot of .class files # this is JVM's (Java Virtual Machine) standard format for bytecode # also note how everything is nested in directories # as we'll later see directories are very important in the Java world # to make use of our jar we need to put it on java's "classpath", it's like our system's PATH but for java PS C:\Users\adas\clojure> java -cp clojure.jar ... Usage: java [options] <mainclass> [args...] (to execute a class) ... # but this didn't do anything, we have to indicate a class <mainclass> to run PS C:\Users\adas\clojure> java -cp clojure.jar clojure.main Clojure 1.8.0 user=> "Nice, clojure.main starts a Clojure REPL! (Read, Eval, Print Loop)" "Nice, clojure.main starts a Clojure REPL! (Read, Eval, Print Loop)" user=> "Let's Ctrl-C out of here" # Actually JAR's META-INF/MANIFEST.MF will usually specify a default Main-Class, here clojure.main PS C:\Users\adas\clojure> cat jar-disassembly/META-INF/MANIFEST.MF Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven Built-By: hudson Build-Jdk: 1.6.0_20 Main-Class: clojure.main # so if we do -jar instead, java will automatically run Main-Class - clojure.main PS C:\Users\adas\clojure> java -jar clojure.jar ... user=> "Back to the Clojure REPL" "Back to the Clojure REPL"
;; C:\users\adas\clojure\main.clj remember to save :) (ns main) (def nine 9)
Back to PowerShell:
;; now we'll also add current directory (".") to the classpath so java can find main.clj PS C:\Users\adas\clojure> java -cp "clojure.jar;." clojure.main user=> (require 'main) nil user=> main/nine 9 ; great!
Don't quit the REPL, but let's change our file a little bit:
;; main.clj again, remember to save (ns main) (def nine 9) (println "hello world")
Back to the REPL:
user=> (require 'main) ; hmmm where's our println? nil user=> (require 'main) ; a namespace is loaded only once, requiring twice does nothing nil user=> (require '[main :reload :all]) ; but we can :reload :all to trigger a re-evaluation hello world ; nice! nil
Remember, we want to count dev.to headlines' most frequently occurring words.
A web scraping library would come in handy. After a bit of googling enlive seems like a good choice. But how do we get it? Onto the next part...
Does Clojure have an
npm equivalent (or
gem)? Kind of.
lein is a lot like
npm, but again - to understand we have to go back to the java world.
Java already has an arguably more powerful build and dependency management tool - Maven2.
npm, Maven can ingest our dependency specs and download relevant artifacts - usually JAR files containing code.
But just like the
java command, Maven's cli interface -
mvn isn't easy.
lein comes in3.
It will fetch dependencies with Maven, generate a classpath and run
java, so we don't have to manually craft a very complex
java command by hand. And much, much more!
PS C:\Users\adas\clojure> wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein.bat -OutFile lein.bat PS C:\Users\adas\clojure> ./lein.bat self-install # lein will install itself Downloading Leiningen now... PS C:\Users\adas\clojure> ./lein.bat # should work now ... PS C:\Users\adas\clojure> $env:path+=";" + $PWD # temporarily add it to our PATH so it's available everywhere PS C:\Users\adas\clojure> lein # make sure it's available now ... # we create "project.clj" - it's a lot like npm's "package.json" PS C:\Users\adas\clojure> Out-File -Encoding ASCII project.clj PS C:\Users\adas\clojure> ./project.clj # should open project.clj in your default editor
;; C:\Users\adas\clojure\project.clj (defproject devto-words "0.0.1" :dependencies )
This defines a project
0.0.1 with no dependencies.
First we'd like to pull in Clojure itself, right?
We need to add
[org.clojure/clojure "1.8.0"]4 to our dependencies.
"1.8.0" makes sense, but why the
When lein interprets our dependencies it will use the namespace
org.clojure as a Maven
<dependency> <groupId>org.clojure</groupId> <artifactId>clojure</artifactId> <version>1.8.0</version> </dependency>
This is what lein will understand the dependency vector
[org.clojure/clojure "1.8.0"] to mean.
;; C:\Users\adas\clojure\project.clj (defproject devto "0.0.1" :dependencies [[org.clojure/clojure "1.8.0"] [enlive "1.1.6"]])
src to java's classpath, having your code under
src is a standard practice, so:
PS C:\Users\adas\clojure> mkdir src # make directory src PS C:\Users\adas\clojure> mv main.clj src/ # move our main.clj to src/, remember to close main.clj in your editor PS C:\Users\adas\clojure> src/main.clj # should open main.clj at it's new location
Now when we start the REPL with
lein it will first download our dependencies from Maven Central et al.5, put them on java's classpath, and finally start the REPL:
PS C:\Users\adas\clojure> lein repl # like I promised, lein is downloading dependencies first Retrieving org/clojure/clojure/1.8.0/clojure-1.8.0.pom from central ... nREPL server started on port 56786 on host 127.0.0.1 - nrepl://127.0.0.1:56786 ... REPL-y 0.4.3, nREPL 0.5.3 Clojure 1.8.0 OpenJDK 64-Bit Server VM 11.0.1+13 Docs: (doc function-name-here) (find-doc "part-of-name-here") Source: (source function-name-here) Javadoc: (javadoc java-object-or-class-here) Exit: Control+D or (exit) or (quit) Results: Stored in vars *1, *2, *3, an exception in *e ;; you'll see there's a lot more output when starting this REPL ;; by default lein loads nREPL - a much more feature-packed REPL ;; but for now we don't need to know much about this user=> (require 'main) ;; still works hello world nil ;; confirm that we have enlive on our classpath as per https://github.com/cgrand/enlive#quickstart-tutorial user=> (require '[net.cgrand.enlive-html :as html]) nil ; we do!
Without exiting the REPL let's make some changes to
(ns main) (require '[net.cgrand.enlive-html :as enlive]) (require '[clojure.string :as s]) ;; let's require ourselves a pretty printing function - pprint ;; we'll be looking at a lot of data, might get messy (require '[clojure.pprint :refer [pprint]]) (def url "http://dev.to")
Back to the REPL:
Let's clean up our
main.clj now that we're almost done:
main=> (require '[main :reload :all]) nil main=> (print-top-words) Interviews 1 ... ;NICE! Ctrl+D out of the REPL
lein that main is our entry-point namespace:
;; C:\users\adas\clojure\project.clj (defproject devto-words "0.0.1" :dependencies [[org.clojure/clojure "1.8.0"] [enlive "1.1.6"]] :main main)
By convention the main function of a namespace is
-main (which we've fortunately just added).
PS C:\Users\adas\clojure> lein run #this will load main and run main/-main now Interviews 1 ...
(ns main ;; checkout https://clojuredocs.org/clojure.core/ns to understand what happened here (:gen-class) (:require [net.cgrand.enlive-html :as enlive] [clojure.string :as s])) (def url "https://dev.to") ;; turn everything into functions so we only fetch stuff when actually calling print-top-words rather than on load (defn fetch-document  (enlive/html-resource (java.net.URL. url))) (defn tokenize [s] (filter not-empty (s/split s #"( +|\n|:)+"))) (defn get-top-words  (->> (enlive/select (fetch-document) [:.single-article :h3]) (map (comp tokenize first :content)) flatten frequencies (sort-by val))) (defn print-top-words  (doseq [w (get-top-words)] (println (key w) (val w)))) (defn -main  (println "Will print top words in a sec...") (print-top-words))
PS C:\Users\adas\clojure> lein run # make sure it still works Interviews 1 ... PS C:\Users\adas\clojure> lein jar # we can build a jar of our code ... Compiling main Created C:\Users\adas\clojure\target\devto-words-0.0.1.jar PS C:\Users\adas\clojure> java -jar C:\Users\adas\clojure\target\devto-words-0.0.1.jar # we can't run this jar because it contains just our code, Clojure itself isn't included Exception in thread "main" java.lang.NoClassDefFoundError: clojure/lang/Var at main.<clinit>(Unknown Source) Caused by: java.lang.ClassNotFoundException: clojure.lang.Var at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ... 1 more # instead we can build a fat jar containing everything needed to run our program PS C:\Users\adas\clojure> lein uberjar ... Created C:\Users\adas\clojure\target\devto-words-0.0.1-standalone.jar # should work as expected now PS C:\Users\adas\clojure> java -jar C:\Users\adas\clojure\target\devto-words-0.0.1-standalone.jar # lein install installs our project into the local Maven repo (usually located at ~/.m2/repository) # allowing us to depend on [devto-words "0.0.1"] in other projects on our machine PS C:\Users\adas\clojure> lein install ... Wrote C:\Users\adas\clojure\pom.xml Installed jar and pom into local repo. # to see a complete dependency tree for our project: PS C:\Users\adas\clojure> lein deps :tree [clojure-complete "0.2.5" :exclusions [[org.clojure/clojure]]] [enlive "1.1.6"] [org.ccil.cowan.tagsoup/tagsoup "1.2.1"] [org.jsoup/jsoup "1.7.2"] [nrepl "0.5.3" :exclusions [[org.clojure/clojure]]] [nrepl/bencode "1.0.0"] [org.clojure/clojure "1.8.0"] # notice how enlive itself pulls in 2 dependencies # also we never asked for clojure-complete and nrepl # lein's default "profile" pulls in some extras # we can rerun deps :tree with a profile "provided" which should only have essentials PS C:\Users\adas\clojure> lein with-profile provided deps :tree [enlive "1.1.6"] [org.ccil.cowan.tagsoup/tagsoup "1.2.1"] [org.jsoup/jsoup "1.7.2"] [org.clojure/clojure "1.8.0"] # just what we asked for # let's see what a bare classpath looks like PS C:\Users\adas\clojure> lein with-profile provided classpath C:\Users\adas\clojure\test;C:\Users\adas\clojure\src;C:\Users\adas\clojure\resources;C:\Users\adas\clojure\target\classes;C:\Users\adas\.m2\repository\org\clojure\clojure\1.8.0\clojure-1.8.0.jar;C:\Users\adas\.m2\repository\enlive\enlive\1.1.6\enlive-1.1.6.jar;C:\Users\adas\.m2\repository\org\ccil\cowan\tagsoup\tagsoup\1.2.1\tagsoup-1.2.1.jar;C:\Users\adas\.m2\repository\org\jsoup\jsoup\1.7.2\jsoup-1.7.2.jar # mostly makes sense # but we like to double-check everything PS C:\Users\adas\clojure> java -cp 'C:\Users\adas\clojure\test;C:\Users\adas\clojure\src;C:\Users\adas\clojure\resources ;C:\Users\adas\clojure\target\classes;C:\Users\adas\.m2\repository\org\clojure\clojure\1.8.0\clojure-1.8.0.jar;C:\Users\ adas\.m2\repository\enlive\enlive\1.1.6\enlive-1.1.6.jar;C:\Users\adas\.m2\repository\org\ccil\cowan\tagsoup\tagsoup\1.2 .1\tagsoup-1.2.1.jar;C:\Users\adas\.m2\repository\org\jsoup\jsoup\1.7.2\jsoup-1.7.2.jar' main Will print top words in a sec... # ok it works! # note how this time instead of running class clojure.main we ran main # this corresponds to the main namespace we worked on (clojure.main would start a REPL like before) # if you noticed, in our final cleanup we added (:gen-class) to the ns form # this makes sure a corresponding Java class is generated for our namespace # the last lein feature we'll explore is "new", it generates scaffoldings for new projects. PS C:\Users\adas\clojure> lein new my-project Generating a project called my-project 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`. PS C:\Users\adas\clojure> tree /F my-project ... │ .gitignore │ .hgignore │ CHANGELOG.md │ LICENSE │ project.clj │ README.md │ ├───doc │ intro.md │ ├───resources ├───src │ └───my_project │ core.clj │ └───test └───my_project core_test.clj # this is what a proper Clojure project looks like
We didn't learn much Clojure.
But we learned many little details that most won't guides won't teach you.
Now you can continue learning with confidence. You already understand the confusing parts, congrats!
Look at footnote 6 if you want to make this installation permanent.
Checkout the links for where to go next.
Links & footnotes
- Clojure cheatsheet - always have this open, the function you're looking for is probably on this list
- Official Clojure getting started docs
- Enlive quickstart
- Clojurians slack - if you have a problem they'll probably help you out if you ask
- ClojureVerse - a Clojure forum, beginner friendly
editors & plugins
- Clava - a Clojure extension for VS code
- Cursive - a dedicated Clojure IDE, probably the best out-of-the-box experience
- Cider - Emacs Clojure IDE
Misc & Java-related
- What is Maven?
- Clojars repository
- Official docs on java's classpath
- Official docs on the many commands that ship with the JDK
- Official docs on what JARs really are
- A tutorial on the PowerShell ZipFile stuff
1 Yes, these PowerShell commands are weird. But they enable this guide to work even on the most basic Windows machine. No extra software needed. ↩
3 There are efforts to move away from lein, towards more lightweight solutions. See this. But from a learner's perspective they suffer from the same shortcomings. Knowledge of the java ecosystem is assumed. Whether you end up using lein or something else, all the lessons you learn here apply. And for the time being you'll mostly see people use lein.↩
4 You probably want to use Clojure version
1.10.0 (current Stable Release) in a real project.
This guide uses
1.8.0 as newer versions depend on clojure.spec which isn't bundled in Clojure's JAR. Downloading clojure.spec and adding it to the classpath would add unnecessary complexity to this guide.
As you learned
lein does all that for you. But for consistency we still used
So if you change
project.clj it'll work just fine.
To reiterate - use
[org.clojure/clojure "1.10.0"] next time.↩
5 Maven Central is Maven's main repository.
But unlike npm, the Maven world relies less on a single repository.
In fact most Clojure libraries are hosted on clojars rather than Maven Central.
Even cooler, when you use Maven, your machine also has its local Maven repository.
You can install artifacts into your local repo and it works just like remote ones.
lein install will do exactly that.↩
If you want to make our java and lein installations permanent, move
java11 to a better location like
lein.bat to, say,
C:\lein\lein.bat then add
C:\lein to your PATH.
Don't know how to do that? See this example:
# you need to open this powershell prompt as an Administrator PS C:\Windows\system32> cd ~ PS C:\Users\adas\clojure> PS C:\Users\adas\clojure> mv java11 C:\java11 PS C:\Users\adas\clojure> mkdir C:\lein PS C:\Users\adas\clojure> mv lein.bat C:\lein\ # should open the right windows menu for you # Select Path under System Variables, click Edit then add C:\java11\bin and C:\lein PS C:\Users\adas\clojure> rundll32 sysdm.cpl,EditEnvironmentVariables # restart powershell to reload PATH.... # make sure java and lein works PS C:\Users\adas> java PS C:\Users\adas> lein # don't need this anymore PS C:\Users\adas> rm clojure