Learn how symbols name locals, vars, classes, and code forms in everyday Clojure.
For a Java developer, the fastest useful mental model is this:
Symbol: A name the evaluator tries to resolve to some meaning in the current context.
That meaning might be:
So when you read a bare token like user, str/trim, or String, you are usually looking at a symbol. The reader turns the characters into a symbol value, and then evaluation decides what that symbol refers to.
The most common mistake for Java engineers is to treat symbols and keywords as interchangeable “identifiers.” They are not.
If you see this:
1(defn normalize-user [user]
2 (update user :email clojure.string/lower-case))
then:
normalize-user, user, update, and clojure.string/lower-case are symbols:email is a keywordThat distinction becomes much easier once you start asking two separate questions while reading:
A symbol often resolves to the nearest relevant binding.
1(def tax-rate 0.13)
2
3(defn total-with-tax [subtotal]
4 (let [tax (* subtotal tax-rate)]
5 (+ subtotal tax)))
Here:
subtotal and tax are local symbolstax-rate resolves to a var in the current namespace* and + resolve to vars from clojure.coreThat resolution model matters because it is what makes Clojure code compact. You are not writing this.taxRate or Math.multiplyExact everywhere. The namespace and local scope provide the context.
Java mental model: a symbol is closer to “whatever name lookup is happening here” than to “just a variable name.”
When you write a namespace-qualified symbol, you are telling the reader exactly where the var lives:
1(ns my.app.users
2 (:require [clojure.string :as str]))
3
4(defn canonical-email [email]
5 (str/lower-case (str/trim email)))
str/lower-case is a qualified symbol:
str is the namespace aliaslower-case is the var nameThis is one of the biggest readability wins in Clojure. You can usually tell where a function comes from without chasing imports scattered across the file.
Another important shift is that symbols can also appear as data.
1'customer-id
2;; => customer-id
Without the quote, customer-id would be resolved as a name. With the quote, the evaluator does not resolve it and instead gives you the symbol value itself.
That matters in:
For most application work, you do not create symbol data often. But you do need to recognize when quoted code is talking about a symbol instead of using the symbol.
You can build a symbol with symbol:
1(symbol "customer" "id")
2;; => customer/id
This is useful in macro code or tooling, but it is not how everyday business logic should be written. If you find yourself dynamically constructing symbols in ordinary application code, pause and ask whether plain data or a map lookup would be simpler.
You may encounter resolve while learning:
1(resolve 'map)
2;; => #'clojure.core/map
That can be helpful at the REPL, but it is not an everyday application pattern. In most code, you want symbol resolution to happen naturally through lexical scope and namespaces, not through manual lookup of names.
This is a good example of a Clojure difference from Java: the language gives you reflective and dynamic tools, but idiomatic code is usually simpler than the most dynamic thing the runtime allows.
When reading a new Clojure form, use these shortcuts:
str/join is a symbol resolved through a namespace alias'status is symbol data, not a lookupThose heuristics are enough to make most everyday code readable long before macros become comfortable.
resolve or dynamic symbol tricks in normal codeThe healthy default is simple: