10 life-changing minutes with Clojure (Windows)

adasomg profile image Adaś Updated on ・23 min read

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.

Rich Hickey

author of Clojure and CTO Cognitect

But what is it really?

On the most fundamental level the Clojure runtime and compiler is just a Java program.

Before we begin we need to get java.

Getting Java

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.

In PowerShell:

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

Great, java is ready.

Getting Clojure

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 https://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 

# now let's see full paths
 PS C:\Users\adas\clojure> tree /F jar-disassembly 
 # 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"
 user=> 50 ;; <= this is a number literal

 user=> (+ 50 1) ;; this invokes function + with arguments 50 and 1

 user=> (type 60) ;; a Clojure number is just a java.lang.Long

user=> (def v ["a" "vector"]) ;; square brackets denote vectors
#'user/v ;; this is a #'var, we'll learn what vars are later

user=> v 
["a" "vector"]

user=> (type v)

user=> (first v) ;; first returns the first element of the vector

user=> (nth v 0) ;; if you want to access by an index

user=> (v 0) ;; but this is weird, a vector itself is also a function! A function just like "nth". Interesting.

user=> (second v)

;; now this is a "map" literal, it's very similar to JavaScript's object literals
 user=> (def m {:key "value", :another-key "almost like js right?"}) 

user=> m
{:key "value", :another-key "almost like js right?"}

user=> (type m)

;; but a key, unlike javascript, isn't just notational sugar, 
;; a key, keyword actually, is a first-class value type in clojure
user=> :key 

user=> (type :key)

;; Clojure maps take any basic Clojure values as keys, they don't have to be keywords
user=> {1 "val"} 
{1 "val"}

user=> {[1 2] "val"} ;; even vectors work
{[1 2] "val"}

user=> (get m :key) ;; get takes a collection and a key and returns a value

user=> (m :key) ;; like vectors, maps are also invokable as a function, this is roughly equivalent to (get m :key)

user=> ({[1 2] "val"} [1 2])

user=> (:key m) ;; :keywords are also invokable as a function! this is also roughly equivalent to (get m :key)

;; but other simple types aren't, you cannot invoke a number as a function
;; this is one of the reasons why :keywords are the preferred type for map keys
user=> (1 {1 "val"}) 
ClassCastException class java.lang.Long cannot be cast to class clojure.lang.IFn 

user=> (first {:key "value"}) ;; so what's the "first" element of a map?
[:key "value"] ;; a vector containing the first key/val?

user=> (type (first {:key "value"})) 
clojure.lang.MapEntry ;; well, almost

user=> (first (first {:key "value"})) ;; works a lot like a vector would

user=> (key (first {:key "value"})) ;; but we can do this

user=> (val (first {:key "value"})) ;; and this

;; let's talk about symbols now
user=> 'a-symbol ;; this is a "quoted" symbol
a-symbol ;; why did we "quote" it?

user=> a-symbol ;; if we don't Clojure will try to "resolve" the symbol and retrieve it's value
CompilerException java.lang.RuntimeException: Unable to resolve symbol: a-symbol in this context

user=> m ;; m doesn't throw because we've previously defined m 
{:key "value", :another-key "almost like js right?"}

user=> (resolve 'm) ;; we can manually resolve m

user=> (resolve 'a-symbol)
nil ; nil as in nothing - we cannot resolve a-symbol to anything

user=> (type #'user/m) ;; so what is #'user/m
clojure.lang.Var ;; a var

user=> 'user/m ;; this is a symbol too
user/m  ;; what's this "user" part?

;; user is a namespace, the "namespace-qualified" symbol user/m has namespace "user"
user=> (namespace 'user/m) 

user=> (name 'user/m) ;; and name "m"

user=> (namespace 'm) ;; a bare symbol "m" isn't "namespace-qualified", hence nil

;; namespaces are a very important concept in clojure, everything exists in a namespace
user=> *ns* ;; special symbol *ns* contains the current namespace
#object[clojure.lang.Namespace 0xf8908f6 "user"]

;; this is why we're seeing this "user" thing in our REPL indicating the current namespace
user=> (ns other) ;; we jump to a different ns

other=> m ;; m is undefined here
CompilerException java.lang.RuntimeException: Unable to resolve symbol: m in this context

other=> user/m ;; we could still access the old m by namespace-qualifying our symbol
{:key "value", :another-key "almost like js right?"}

other=> (ns user) ;;ok back to user

; libraries in Clojure will come in their own namespaces, Clojure ships with some built-in ones
user=> (require 'clojure.string) ;; unloaded namespaces have to be required first, like clojure.string

user=> (ns-publics 'clojure.string) ; let's check what clojure.string defines
{ends-with? #'clojure.string/ends-with?, capitalize #'clojure.string/capitalize, reverse #'clojure.string/reverse, join #'clojure.string/join, replace-first #'clojure.string/replace-first, starts-with? #'clojure.string/starts-with?, escape #'clojure.string/escape, last-index-of #'clojure.string/last-index-of, re-quote-replacement #'clojure.string/re-quote-replacement, includes? #'clojure.string/includes?, replace
#'clojure.string/replace, split-lines #'clojure.string/split-lines, lower-case #'clojure.string/lower-case, trim-newline #'clojure.string/trim-newline, upper-case #'clojure.string/upper-case, split #'clojure.string/split, trimr #'clojure.string/trimr, index-of #'clojure.string/index-of, trim #'clojure.string/trim, triml #'clojure.string/triml, blank? #'clojure.string/blank?}

;clojure.string/split looks cool but how do we use it?
user=> (doc clojure.string/split) ; thankfully the REPL has a doc function that'll help
([s re] [s re limit])
  Splits string on a regular expression.  Optional argument limit is
  the maximum number of splits. Not lazy. Returns vector of the splits.

user=> (clojure.string/split "A string we want to split into words" #" ") 
;;  #"regex" <= this is a regex literal (think /regex/ in js)
["A" "string" "we" "want" "to" "split" "into" "words"]

user=> (require '[clojure.string :as s]) ; to save ourselves typing we can require a namespace under a shortened name

user=> (s/split "A string we want to split into words" #" ")
["A" "string" "we" "want" "to" "split" "into" "words"]

user=> (resolve 's/split) ;but it will still resolve to the full name

user=> (require '[clojure.string :as s :refer [split]]) ; require split directly into our namespace

user=> (resolve 'split) ; still nicely resolves to the real deal

user=> (require 'main) ; what if we require a non-existent namespace?
FileNotFoundException Could not locate main__init.class or main.clj on classpath.  clojure.lang.RT.load (RT.java:456)

;; interesting, so java was looking for main__init.class or main.clj on the classpath
;; so what if there actually was main.clj on the classpath?
;; let's add it, unfortunately this means we have to Ctrl-C out of here

;; create main.clj - we explicitly set the encoding because some (like UTF16) may cause issues
PS C:\Users\adas\clojure> Out-File -Encoding ASCII main.clj

PS C:\Users\adas\clojure> ./main.clj
;; should open main.clj in your text editor, or it might ask you to pick a program
;; if you don't have a text editor just pick Notepad, it'll do for now

In main.clj:

;; 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)

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?

user=> (require 'main) ; a namespace is loaded only once, requiring twice does nothing

user=> (require '[main :reload :all]) ; but we can :reload :all to trigger a re-evaluation
hello world ; nice!

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 pip 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.

Like 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.

That's where 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!

Getting lein

As per leiningen.org/#install for windows we need to place lein.bat on our PATH:

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

In project.clj:

;; C:\Users\adas\clojure\project.clj
(defproject devto-words "0.0.1"
  :dependencies [])

This defines a project devto-words version 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. clojure and "1.8.0" makes sense, but why the org.clojure namespace?

When lein interprets our dependencies it will use the namespace org.clojure as a Maven groupId, name clojure as artifactId and "1.8.0" as version.

If we go to Maven Central's search page5 and look for "clojure" you can confirm that there is indeed such an artifact, a full xml spec is given:


This is what lein will understand the dependency vector [org.clojure/clojure "1.8.0"] to mean.

Enlive's github README already gives us a lein-style dependency vector: [enlive "1.1.6"]. Great!

So if we add Clojure and enlive, our project.clj should end up looking like this:

;; C:\Users\adas\clojure\project.clj
(defproject devto "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [enlive "1.1.6"]])

By default lein adds 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 - nrepl://
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

;; 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 main.clj:

(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:

user=> (require '[main :reload :all]) 

user=> (ns main) ; let's set our namespace to main

main=> url
"http://dev.to" ;; oops we actually wanted https not http, change it in main.clj

main=> (require '[main :reload :all]) ;; reload our changes

main=> url
"https://dev.to" ;; ok, all is good now

;; this will fetch the page and parse it (let's not concern ourselves with how it works)
main=> (def document (enlive/html-resource (java.net.URL. url))) 
;; if you really want to see what's in there (pprint document), it'll print A LOT of stuff

;; again how this works is beyond the scope of this guide
;; but it's basically like running document.querySelectorAll(".single-article h3") or $$(".single-article h3") on a webpage
;; it should get us all the right headline elements from dev.to
main=> (def headers (enlive/select document [:.single-article :h3])) 
;; again (pprint headers) if curious

main=> (first headers)
{:tag :h3, :attrs nil, :content ("...")}

main=> (first (:content (first headers))) 
"\n              Freelancing 11: How to get started\n            "
;; what a nasty string

;; let's put the string in a variable so it's easier to work with
main=> (def sample-headline (first (:content (first headers))))

main=> (s/split sample-headline #"[\n ]+")
["" "Freelancing" "11:" "How" "to" "get" "started"]

main=> (s/split sample-headline #"[\n :]+")
["" "Freelancing" "11" "How" "to" "get" "started"]
;; uh we don' want that ""
;; but we're also not smart enough to make a better regexp
;; let's figure out a hack...

;; an identity fn returns it's input, basically does nothing
main=> (identity 3)

;; filter applies a function to every element in a collection
;; and removes any elements that return falsy values (think Array.filter in JavaScript)
main=> (filter identity [1 2 3 nil false ""])
(1 2 3 "") ;; note that both nil and false are "falsy" values in Clojure

main=> (not-empty "string") ;; not-empty returns it's input if it's not empty

main=> (not-empty "") ;; but returns nil if it is

main=> (filter not-empty (s/split sample-headline #"[\n :]+"))
("Freelancing" "11" "How" "to" "get" "started")
;; if you're confused a javascript equivalent for above would be:
;; " Freelancing 11: How to get started".split(/[\n ]+/).filter(x=>x)   empty strings are falsy in js so no need for not-empty

;; instead of doing (first (:content (first headers)))
;; we can compose the two functions into one with comp
main=> ((comp first :content) (first headers))
"\n              Freelancing 11: How to get started\n            "

;; now we're doing the same thing for all headers with map
;; map applies a function to every element of a collection
;; and returns a new collection with each result
main=> (pprint (map (comp first :content) headers))
("\n              Freelancing 11: How to get started\n            "
 "\n            \n  The UX design pyramid with the user needs\n\n        "

;; let's turn our piece of code that splits strings into words into a function
;; we create function tokenize that takes one argument s
main=> (defn tokenize [s] (filter not-empty (s/split s #"[\n :]+")))

;; verify that it still works (remember you should be retyping!)
main=> (tokenize " a test sentence ")
("a" "test" "sentence") ;; it does!

;; so now for every header we call :content, then call first, and then tokenize
main=> (pprint (map (comp tokenize first :content) headers))
(("Freelancing" "11" "How" "to" "get" "started")
 ("The" "UX" "design" "pyramid" "with" "the" "user" "needs")
 ("Why" "Bandwidth" "Still" "Matters")
 ("Using" "API" "first" "and" "TDD" "for" "your" "next" "library")

;; now let's flatten these word lists into one
main=> (pprint (flatten (map (comp tokenize first :content) headers)))

;; we're very lucky, clojure already comes with a function frequencies
;; it returns a map, mapping values to the times they occur in a collection
main=> (pprint (frequencies (flatten (map (comp tokenize first :content) headers))))
{"Interviews" 1,
 "Why" 1,
;; our code is getting really ugly, so many function applications don't look pretty
;; Clojure has a ->> "thread last macro"
;; we can use it to rewrite in a way reads a little more naturally
main=> (->> headers (map (comp tokenize first :content)) flatten frequencies pprint)
{"Interviews" 1,
 "Why" 1,
;; it works, but what's going on???
;; ->> is a macro, for complete docs see (https://clojuredocs.org/clojure.core/-%3E%3E)
;; we can can examine what a macro expands to by doing macroexpand:
main=> (macroexpand '(->> headers (map (comp tokenize first :content)) flatten frequencies pprint)) 
(pprint (frequencies (flatten (map (comp tokenize first :content) headers)))) ;; exactly like before!
;; note how we had to quote the argument to macroexpand with '
;; quoting stops any evaluation from happening, so we can treat this piece of code as data
;; don't worry if it's still confusing, eventually it'll become second nature

;; so now all we need to do is sort our results
;; again, clojure already has a neat function - sort-by (detailed docs https://clojuredocs.org/clojure.core/sort-by)
main=> (->> headers (map (comp tokenize first :content)) flatten frequencies (sort-by val) pprint)
 ["Gift" 2]
 ["in" 2]
 ["In" 3]
 ["for" 3]
 ["5" 3]
 ["a" 3]
 ["A" 3]
 ["your" 3]
 ["to" 3]
 ["of" 4]
 ["How" 4]
 ["the" 5]) 
 ;; yay this is what we wanted all along, 
 ;; we could improve our tokenize to exclude words like the, a, of...
 ;; but we'll leave this as an exercise for the reader

Let's clean up our main.clj now that we're almost done:

;; C:\Users\adas\clojure\src\main.clj
(ns main)

(require '[net.cgrand.enlive-html :as enlive])
(require '[clojure.string :as s])
(require '[clojure.pprint :refer [pprint]])

(def url "https://dev.to")

(defn tokenize [s]
  (filter not-empty (s/split s #"[\n :]+")))

(def document (enlive/html-resource (java.net.URL. url)))

(def headers (enlive/select document [:.single-article :h3]))

(def top-words
  (->> headers
       (map (comp tokenize first :content))
       (sort-by val)))

(defn print-top-words []
  (doseq [w top-words] ;; doseq is like forEach in javascript 
    (println (key w) (val w))))

(defn -main [] ;; we will explain this shortly
  (println "Will print top words in a sec...")


main=> (require '[main :reload :all])
main=> (print-top-words)
Interviews 1
... ;NICE! Ctrl+D out of the REPL

Let's tell 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

Final main.clj cleanup:

(ns main ;; checkout https://clojuredocs.org/clojure.core/ns to understand what happened here
   [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))
       (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...")
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
# 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
.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



# this is what a proper Clojure project looks like 

Thank you

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.

Thank me

If you enjoyed this let me know by 💬commenting💬, ❤liking❤, ⭐starring⭐ on github, or by 👣following the author on twitter👣.

Otherwise I won't know and you'll never see a similar guide from me again 😭

Links & footnotes

learning materials



editors & plugins

Misc & Java-related


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.

2 What is Maven?

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 1.8.0.
So if you change 1.8.0 to 1.10.0 in 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 C:\java11 and lein.bat to, say, C:\lein\lein.bat then add C:\java11\bin and 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 


Andy Fingerhut for spotting crucial errors and making great suggestions.


Editor guide

Minor change needed (I guess maven has upped security), need to use https

wget https://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.jar -OutFile clojure.jar


Thank you! Updated.


This is an amazing guide!

I'm using and teaching Clojure since 1.5.1, but on Mac and Linux and with boot and lately with tools.deps.

I haven't used Windows in decades, but I'm pleasantly surprised how viable is it to work with Clojure on it these days.

In the future, your article will be the 1st I recommend to my friends who are inclined to learn Clojure.


In Clojure this ability to interactively figure things out at the REPL scales to really big projects.

Say in js, you'll test out simple expressions at the REPL/console, but sooner or later you'll have to restart your program or trigger some kind of reloading.

And just to be clear, you don't literally copy into or type at a prompt, usually you work in a source file and press a hotkey to evaluate expressions.

For me, personally, it's not as much about being 10x more productive or whatever. But instead of getting distracted and giving up I can actually get something done sometimes thanks to this immediate feedback that keeps you engaged 😂

Explaining why exactly it scales in LISPs, and even more so in Clojure while it doesn't in other languages is pretty dense, so I'll say this much - the LISP part has to do with homoiconicity, while Clojure's contribution is discouraging statefulness and encouraging pure functions.

Macros are the only other advantage that I'd consider "objective". But again, a huge rabbit hole.

The error-handling thing you talk about is probably about stacktraces being hard to read. You'll be dealing with Java stacktraces and exceptions (and JavaScript's for ClojureScript). It's often hard to understand what really went wrong.

Even in this guide you'll see something like:

user=> (1 {1 "val"}) 
ClassCastException class java.lang.Long cannot be cast to class clojure.lang.IFn 

We're trying to invoke a number as a function here. You have to understand a Clojure number is actually java.lang.Long and clojure.lang.IFn is Clojure's class for invokable stuff. Oh and the entire concept of casting classes. So imagine how confusing that must be to people who are new to both Java and Clojure. And even for veterans, this context switching is mentally exhausting.

The other part of the error-handling criticism is probably from old time Lispers. In something like Common Lisp or even Emacs Lisp you don't get a stacktrace, you get backtrace, which shows you what each and every form along the way evaluated to. Hard to explain. But really powerful. And Common Lisp also has this thing called restarts which gives you a lot of control over error-handling, again really dense. But, anyways, from their perspective Clojure is just worse than other LISPs.

Practically speaking I think the biggest con is Clojure is a hard sell for anything specific. You won't do very low-level stuff because the JVM is too heavy. Data science stuff, well Python has a better community. Web stuff - why not JavaScript? For almost everything, there's a language or ecosystem that seems like a better choice if you want to do this one thing.

So where it really shines is huge monolithic projects that unify a lot of different domains.


This is an extremely competent guide to the setup of clojure for windows users. The first 10 minutes were truly joyful; I can see the rest will also be "life changing", but it will take me a while longer. We are blessed to have Adaś thoughtful step-by-step guide, not to mention insightful asides (really useful to know stuff). Adaś says "In only 10 minutes you'll be spared weeks of frustration". So true, but the sheer joy, to have spent some time looking for a way in, to actually find it here!


Thank you!


This is indeed, quite a comprehensive guide, however, it has one premise that is not entirely accurate: "On the most fundamental level Clojure is just a Java program." This is untrue (to a point) Clojure is a JVM language,it compiles directly to byte code, it is not Java, although interop is entirely integrated. Java is an OOP language, Clojure is a functional Lisp, there are fundamental differences. Its true Clojure processes are built on the Java Memory Model ( the memory model is JVM based not language-centric), but in context Java and Clojure are different languages,that share the same underlying platform, but to say Clojure is a Java Program is not beneficial. Clojure can use Java Libs, but as semantics and approach it is does not share the paradigm.


It's absolutely on a fundamental level a Java program, its low-level source code is perfectly readable Java code. All the compiler magic is fairly contained. When dealing with trickier issues you will absolutely be looking at the Clojure's Java source or the Java source of the many builtin Java libraries it uses for pretty much everything. Happens all the time.

Never encountered an issue where you'd even think about bytecode generation or JVM internals. When I started with Clojure the only "hard" part was catching up on all the Java stuff since that was never my thing. Hence this guide tries to explain both at the same time.


It very much depends on how you define the concept of program. To me A Clojure program is a program written in Clojure, a Java program is one written in Java. Whilst they share the underling JVM and some libraries they are not the same, and yes even when there is interoperable features.

Kotlin and Groovy are also JVM languages, are they also Java programs?

For me, a program is about the way you approach your need in a language, the core facets of the language, how its structured how its implemented. Its not merely about syntax.

In this case, Clojure is a data driven, functionally opinionated way of approaching a solution on the JVM (it also has variants that run on other run times). It provides more facile less bloated ways of abstraction and immutability (though newer versions of Java have improved on this). These are fundamental facets of functional programming and give Clojure its identity, as a Clojure program.

Certainly as mentioned it is possible to provide some of these abilities an a Java Program, but Java is not inherently functional, the predominant way Java is used is primarily different from the way Clojure is used, even if it is to achieve the same outcome.

As a clearer analogy for example, the Norwegian and Swedish languages (and to an Extent Danish) share common lexical foundations, some common words, constructs, semantics and linguistic similarities, but Norwegian is not Swedish nor Danish, neither is Danish Norwegian or Swedish or Swedish Norwegian or Danish. Each has its own applications, uses, nuances and contexts, but each is not the other. One wouldn't initially choose to speak Danish in Norway to provide a solution to communicate.

So for me, my decision on what language to use, depends on the individual strengths and characteristics as well as its issues and foibles, basically, how best it could provide a solution for the domain issue faced. This defines for me, what a program is, and if I apply such logic here, I would not consider Clojure a Java Program, nor Clojure a Java Program, as they both approach domain issues differently.

This is not to disagree nor critisize your guide, certainly not, I enjoyed it, it was well written and researched,an excellent guide. As someone who works with both Java and Clojure (and admittedly, a big functional programming and Clojure fan), I personally took slight umbrage with your statement about Clojure being a Java program.

Right, I'm absolutely with you as far as the concept of Clojure the language goes. A Clojure program is a Clojure program. While Clojure itself, the runtime and compiler, is a Java (and Clojure) program. And it's not just a technical detail, but something that is very apparent when working on any non-trivial project. RT.java is something you'll be seeing in your stacktraces a lot.

Say, with ClojureScript, the situation is different. CLJS code AND its runtime gets compiled (by the JVM Clojure) into JavaScript code that abuses JS quite heavily and only has decent interop one-way. I don't think any parts of the CLJS runtime are implemented in JavaScript, it's all generated from Clojure I believe. If you have a problem with the CLJS implementation you will be looking at Clojure[Script] code. Also the Clojure compiler is present at runtime, while ClojureScript's is obviously not.

No idea how Kotlin or Groovy are implemented.

Updated the wording to "Clojure runtime and compiler", hopefully this eliminates any confusion


Great write up, been getting into Clojure and windows at the same time, thus far I've been using WSL with VSCode and leiningen which has worked well for learning and smaller things. It seems like everyone is using EMACS to develop with and I'm wondering if there are any pitfalls to setting this up in windows as well? (The Emacs setup failed miserably on my mac with the latest Catalina which made me a little wary of trying on windows)


This is a really great guide. I know java and the jvm fairly well and I know Lisp but never knew how the two came together to make clojure, as well as the tools that come with it.


Thanks, glad you enjoyed!


Wow this saved me days off installation. very good article.Thanks for sharing


Isn't Clojure support for Windows still in alpha? If that's the case, would it be better to run on Linux through WSL?

Some comments have been hidden by the post's author - find out more

Code of Conduct Report abuse