The Joy of Clojure
Thinking the Clojure Way
by Michael Fogus & Chris Houser
- On Amazon
- ISBN: 978-1935182641
The Joy of Clojure feels like the wiser brother of Practical Clojure. It covers about the same topics, but goes deeper. It also explains the Clojure philosophy better in my opinion.
My notes
Foundations
Clojure philosophy
Clojure was born out of creator Rich Hickey's desire to avoid many of the complications, both inherent and incidental, of managing state using traditional object-oriented techniques.
Clojure is an opinionated language.
It's hard to write simple solutions to complex problems. But every experienced programmer has also stumbled on areas where we've made things more complex than necessary, what you might call incidental complexity as opposed to complexity that's essential to the task at hand.
Pure function: takes a few arguments and produces a return value based solely on those arguments.
Clojure strives to be practical – a tool for getting the job done. If a decision about some design point in Clojure had to weigh the trade-offs between the practical solution and a clever, fancy, or theoretically pure solution, usually the practical solution won out.
Clojure works to provide consistency in two specific ways: consistency of syntax and of data structures. Consistency of syntax is about the similarity in form between related concepts. The consistency of data structures is the deliberate design of all of Clojure's persistent collection types to provide interfaces as similar to each other as possible, as well as to make them as broadly useful as possible.
Computer programmers are perpetually in search of beauty, and more often than not, this beauty presents itself in the form of simplicity.
In a Clojure program, code is data.
Functional programming is one of those computing terms that has a nebulous definition. If you ask 100 programmers for their definition, you'll likely receive 100 different answers.
A workable definition of functional programming: Functional programming concerns and facilitates the application and composition of functions. Further, for a language to be considered functional, its notion of function must be first-class. The functions of a language must be able to be stored, passed, and returned just like any other piece of data within that language.
Object-oriented programmers and functional programmers will often see and solve a problem in different ways. Whereas an object-oriented mindset will foster the approach of defining an application domain as a set of nouns (classes), the functional mind will see the solution as the composition of verbs (functions).
The first important term to define is time. Time refers to the relative moments when events occur. Over time, the properties associated with an entity will form a concrescence and be logically deemed its identity. It follows from this that at any given time, a snapshot can be taken of an entity's properties defining its state. This notion of state is an immutable one because it's not defined as a mutation in the entity itself, but only as a manifestation of its properties at a given moment in time.
By focusing on immutability, Clojure eliminates entirely the notion of mutable state and instead expounds that most of what's meant by objects are instead values.
By adhering to a strict model of immutability, concurrency suddenly becomes a simpler (although not simple) problem, meaning if you have no fear that an object's state will change, then you can promiscuously share it without fear of concurrent modification.
An imperative programming language is one where a sequence of statements mutates program state. The preferred flavor of imperative programming is the object-oriented style.
Polymorphism is the ability of a function or method to have different definitions depending on the type of the target object. Clojure provides polymorphism via both multimethods and protocols.
The expression problem refers to the desire to implement an existing set of abstract methods for an existing concrete class without having to change the code that defines either.
Drinking from the Clojure firehose
Numbers in Clojure can include an optional M, that flags a number as a decimal requiring arbitrary precision. In many programming languages, the precision of numbers is restricted by the host platform. Clojure on the other hand uses the host language's primitive numbers when appropriate, but rolls over to the arbitrarily precise versions when needed, or when explicitly specified.
Integers in Clojure can theoretically take an infinitely large value, although in practice the size is limited by the memory available.
Clojure provides a rational type in addition to integer and floating-point numbers. Rational numbers offer a more compact and precise representation of a given value over floating-point. Rationals are represented classically by an integer numerator and denominator, and that's exactly how they're represented in Clojure: 22/7
. They will be simplified, if possible.
Symbols in Clojure are objects in their own right, but are often used to represent another value. When a symbol is evaluated, you'll get back whatever value that symbol is referring to in the current context.
Keywords are similar to symbols, except that they always evaluate to themselves. Keywords are prefixed by a colon :
.
Lists are written with parentheses: (hello world)
. When a list is evaluated, the first item of the list will be resolved to a function, macro, or special form. If it is a function, the remaining items will be passed to the function as its parameters. If it is a macro or special form, the remaining items in the list aren't necessarily evaluated, but are processed as defined by the macro or operator. Lists can contain items of any type, including other collections.
A form is any Clojure object meant to be evaluated. A special form is a form with special syntax or special evaluation rules that are typically not implemented using the base Clojure forms.
The empty list in Clojure, written as ()
, isn't the same as nil
.
Vectors are written with square brackets: [1 2 :a :b :c]
.
Maps store unique keys and one value per key – similar to what other languages call dictionaries or hashes. Maps are written with alternating keys and values inside curly braces, and commas are frequently used between pairs: {1 "one", 2 "two", 3 "three"}
Sets store unique items and are written using curly braces with a leading hash: #{1 2 "three" :four}
Clojure uses prefix notation for calling functions: (+ 1 2 3)
. The obvious advantage of the prefix notation: it allows any number of operands per operator.
Definition of an anonymous (unnamed) Clojure function: (fn [x y] #{x y})
.
The second form to define functions allows for arity overloading of the invocations of a function. Arity refers to the differences in the argument count that a function will accept. Example:
(fn
([x] #{x})
([x y] #{x y}))
The way to denote variable arguments is to use the &
symbol followed by a symbol. Every symbol in the arguments list before the &
will still be bound one-for-one to the same number of arguments passed during the function call. But any additional arguments will be aggregated in a sequence bound to the symbol following the &
symbol.
The def
special form is a way to assign a symbolic name to a piece of Clojure data. To associate a name with a function using def
, we'd use: (def make-a-set (fn [x y] #{x y}))
.
Another way to define functions in Clojure is using the defn
macro:
(defn make-a-set
"Takes two values and makes a set from them"
([x y] #{x y}))
Clojure provides a shorthand notation for creating an anonymous function using the #()
reader feature. Reader features are analogous to preprocessor directives in that they signify that some given form should be replaced with another at read time. The #()
form can also accept arguments that are implicitly declared through the use of special symbols prefixed with %
. Example: (def make-a-list #(list %1 %2))
Clojure's closest analogy to a variable is the Var. A Var is named by a symbol and holds a single value.
Using def
is the most common way to create Vars in Clojure: (def x 42)
.
Use the do
form when you have a series or block of expressions that need to be treated as one. All the expressions will be evaluated, but only the last one will be returned:
(do
6
(+ 5 4)
3)
Clojure doesn't have local variables, but it does have (immutable) locals. Locals are created and their scope defined using a let
form, which starts with a vector that defines the bindings, followed by any number of expressions that make up the body.
(let [r 5
pi 3.1415
r-squared (* r r)]
(println "radius is" r)
(* pi r-squared))
Clojure has a special form called recur
for tail recursion:
(defn print-down-from [x]
(when (pos? x)
(println x)
(recur (dec x))))
Sometimes you want to loop back not to the top of the function, but to somewhere inside. To help, there's a loop
form that acts exactly like let
but provides a target for recur
to jump to:
(defn sum-down-from [initial-x]
(loop [sum 0, x initial-x]
(if (pos? x)
(recur (+ sum x) (dec x))
sum)))
A form is in the tail position of an expression when its value may be the return value of the whole expression.
If you try to use the recur
form somewhere other than a tail position, Clojure will remind you at compile time.
The quote
special form simply prevents its argument from being evaluated.
The idiomatic way to create an instance of a Java class is to use a dot after the class name: (java.util.Hashmap. {"foo" 42 "bar" 9})
.
To access instance properties, precede the property or method name with a dot: (.x (java.awt.Point. 10 20))
Dipping our toes in the pool
Every value looks like true
to if
, except for false
and nil
. That means that values which some languages treat as false – zero-length strings, empty lists, the number zero, and so on – are all treated as true
in Clojure.
If you need to differentiate between the two false values, you can use nil?
and false?
.
Because empty collections act like true
in Boolean contexts, we need an idiom for testing whether there's anything in a collection to process. We can use the seq
function for this purpose, it returns either a sequence view of a collection, or nil
if the collection is empty. Example:
(defn print-seq [s]
(when (seq s)
(prn (first s))
(recur (rest s))))
Destructuring allows us to positionally bind locals based on an expected form for a composite data structure. Destructuring is loosely related to pattern matching found in languages like Haskell or Scala, but much more limited in scope.
Example of destructuring with a vector:
(def guys-whole-name ["Guy" "Lewis" "Steele"])
(let [[first-name middle-name last-name] guys-whole-name]
(str last-name ", " first-name " " middle-name))
We can use an ampersand in a destructuring vector to indicate that any remaining values of the input should be collected into a seq: (let [a b c & more] (range 10)])
.
The final feature of vector destructuring is :as
, which can be used to bind a local to the entire collection. It must be placed after the &
local, if there is one, at the end of the destructuring vector: (let a b c & more :as all] (range 10)])
.
Example of destructuring with a map:
(def guys-name-map
{:first-name "Guy" :middle-name "Lewis" :last-name "Steele"})
(let [{first-name :first-name, middle-name :middle-name, last-name :last-name} guys-name-map]
(str last-name ", " first-name " " middle-name))
This can be simplified by using :keys
instead of a binding form, were we're telling Clojure that the next form will be a vector of names that it should convert to keywords in order to look up their values in the input map:
(let [{:keys [first-name middle-name last-name]} guys-name-map]
(str last-name ", " first-name " " middle-name))
Similarly, if we had used :strs
, Clojure would be looking for items in the map with string keys, and :syms
would indicate symbol keys.
If the destructuring map looks up a key that's not in the source map, it's normally bound to nil
, but you can provide different defaults with :or
: (let [{:keys [title first-name last-name], :or {title "Mr."}} guys-name-map])
.
Each function parameter can destructure a map or sequence:
(defn print-last-name [{:keys [last-name]}]
(println last-name))
(print-last-name guys-name-map)
It's idiomatic in Clojure to build your application objects by composing maps and vectors as necessary. This makes destructuring natural and straightforward. So anytime you find that you're calling nth
on the same collection a few times, or looking up constants in a map, or using first
or next
, consider using destructuring instead.
Data types
On scalars
A scalar data type is one that can only hold one value at a time.
Truncation refers to the limiting of accuracy for a floating-point number based on a deficiency in the corresponding representation. When a number is truncated, its precision is limited such that the maximum number of digits of accuracy is bound by the number of bits that can "fit" into the storage space allowed by its representation.
If high precision is required for your floating-point operations, then explicit typing is required. You do it by using Clojure's literal notation, a suffix character M
, to declare a value as requiring arbitrary decimal representation.
Clojure is able to detect when overflow occurs, and will promote the value to a numerical representation that can accommodate larger values. This promotion within Clojure is automatic, as the primary focus is first correctness of numerical values, then raw speed.
Underflow is the inverse of overflow, where a number is so small that its value collapses into zero.
The best way to ensure that your calculations remain as accurate as possible is to ensure that they're all done using rational numbers.
Clojure's rational type is a double-edged sword. The calculation of rational math, though accurate, isn't nearly as fast as with floats or doubles. Each operation in rational math has an overhead cost (such as finding the least common denominator) that should be accounted for.
Keywords always refer to themselves. What this means is that the keyword :magma
always has the value :magma
. Because keywords are self-evaluating and provide fast equality checks, they're almost always used in the context of map keys. An equally important reason to use keywords as map keys is that they can be used as functions, taking a map as an argument, to perform value lookups:
(def population {:zombies 2700, :humans 9})
(:zombies population)
Symbols in Clojure are roughly analogous to identifiers in many other languages – words that refer to other things. In a nutshell, symbols are primarily used to provide a name for a given value.
A literal regular expression in Clojure looks like this: #"an example pattern"
.
Regular expressions accept option flags that can make a pattern case-insensitive or enable multiline mode, and Clojure's regex literals starting with (?<flag>)
set the mode for the rest of the pattern: #"(?i)yo"
.
The re-seq
is Clojure's regex workhorse. It returns a lazy seq of all matches in a string, which means it can be used to efficiently test whether a string matches at all or to find all matches in a string: (re-seq #"\w+" "one-two/three")
.
Composite data types
A persistent collection in Clojure allows you to preserve historical versions of its state, and promises that all versions will have the same update and lookup complexity guarantees. The specific guarantees depend on the collection type.
A sequential collection is one that holds a series of values without reordering them.
A sequence is a sequential collection that represents a series of values that may or may not exist yet. They may be values from a concrete collection or values that are computed as necessary. A sequence may also be empty.
Clojure has a simple API called seq for navigating collections. It consists of two functions: first
and rest
. If the collection has anything in it, (first coll)
returns the first element; otherwise it returns nil
. (rest coll)
returns a sequence of the items other than the first. If there are no other items, rest
returns an empty sequence and never nil
. A seq is any object that implements the seq API.
Clojure classifies each composite data type into one of three logical categories or partitions: sequentials, maps, and sets. These divisions draw clear distinctions between the types and help define equality semantics. Specifically, two objects will never be equal if they belong to different partitions.
If two sequentials have the same values in the same order, =
will return true for them, even if their concrete types are different.
All an object needs to do to be a sequence is to support the two core functions: first
and rest
.
Every Clojure collection provides at least one kind of seq object for walking through it contents, exposed via the seq
function.
Algorithmic complexity is a system for describing the relative space and time costs for algorithms. Typically the complexity of an algorithm is described using what's known as Big-O notation.
In analyzing algorithms you rarely care about the best-case scenario because it's too rare to matter much. What you really care about when analyzing algorithms is the expected case, or what you'd likely see in practice.
Vectors store zero or more values sequentially indexed by number, a bit like arrays, but are immutable and persistent.
Vectors are particularly efficient at three things relative to lists: adding or removing things from the right end of the collection, accessing or changing items in the interior of the collection by numeric index, and walking in reverse order.
Any item in a vector can be accessed by its index number in essentially constant time. You can do this using the function nth
; the function get
, essentially treating the vector like a map; or by invoking the vector itself as a function:
(def my-vector ["a" "b" "c"])
(nth my-vector 2)
(get my-vector 2)
(my-vector 2)
Any item in a vector can be "changed" using the assoc
function: (assoc my-vector 2 "x")
. It only works on indices that already exist in the vector, or as a special case, exactly one step past the end.
Classic stacks have at least two operations, push and pop, and with respect to Clojure vectors these operations are called conj
and pop
respectively. The conj
function adds elements to and pop
removes elements from the right side of the stack.
In idiomatic Clojure code, lists are used almost exclusively to represent code forms.
If the final usage of a collection isn't as Clojure code, lists rarely offer any value over vectors and are thus rarely used.
It's important to point out that Clojure's PersistentQueue
is a collection, not a workflow mechanism.
How would you go about creating a queue? The answer is that there's a readily available empty queue instance to use, clojure.lang.PersistentQueue/EMPTY
.
It's impossible to rely on a specific ordering if the key/value pairs for a standard Clojure map, because there are no order guarantees at all. Using the sorted-map
and sorted-map-by
functions, you can construct maps with order assurances. By default, the function sorted-map
will build a map sorted by the comparisons of its keys.
Clojure provides a special map that ensures insertion ordering called an array map.
Functional programming
Being lazy and set in your ways
The first principle of immutability: all of the possible properties of immutable objects are defined at the time of their construction and can't be changed thereafter.
Clojure directly supports immutability as a language feature with its core data structures. By providing immutable data structures as a primary language feature, Clojure separates the complexity of working with immutable structures from the complexities of their implementation.
Clojure is partially a lazy language.
Laziness allows the avoidance of errors in the evaluation of compound structures.
Idiomatic Clojure code will always strive to deal with, and produce, lazy sequences.
The primary advantage of laziness in Clojure is that it prevents the full realization of interim results during a calculation.
Because Clojure's sequences are lazy, they have the potential to be infinitely long.
Although Clojure sequences are largely lazy, Clojure itself isn't. In most cases, expressions in Clojure are evaluated once prior to their being passed into a function rather than at the time of need. But Clojure does provide mechanisms for implementing what are known as call-by-need semantics. The most obvious of these mechanisms is its macro facilities. The other mechanism are Clojure's delay
and force
. In short, the delay
macro is used to defer the evaluation of an expression until explicitly forced using the force
function.
The if-let
and when-let
macros are useful when you'd like to bind the results of an expression based on if it returns a truthy value:
(if :truthy-thing
(let [res :truthy-thing] (println res)))
; this can be simplified to:
(if-let [res :truthy-thing] (println res))
Functional programming
Clojure functions are first-class – they can be both passed as arguments and returned as results from other functions.
Splitting functions into smaller, well-defined pieces fosters composability and, as a result, reuse.
There may be times when instead of building a new function from chains of functions as comp
allows, you need to build a function from the partial application of another: ((partial + 5) 100 200)
.
First-class functions can not only be treated as data; they are data. Because a function is first-class, it can be stored in a container expecting a piece of data.
A higher-order function is a function that does at least one of the following: takes one or more functions as arguments, or returns a function as a result.
Simply put, pure functions are regular functions that, through convention, conform to the following simple guidelines: The function always returns the same result, given some expected arguments; and the function doesn't cause any observable side-effects.
If a function of some arguments always results in the same value and changes no other values within the greater system, then it's essentially a constant, or referentially transparent (the reference to the function is transparent to time).
Every function in Clojure can potentially be constrained on its inputs, its output, and some arbitrary relationship between them. These constraints take the form of pre- and postcondition vectors contained in a map defined in the function body:
(defn my-function [p1 p2]
{:pre [(not= p1 p2) (vector? p1) (vector? p2)]
:post [(float? %)]}
( ; do something
))
By pulling out the assertions into a wrapper function, we can detach some domain-specific requirements from a potentially globally useful function and isolat them in aspects. By detaching pre- and postconditions from the functions themselves, you can mix in any implementation that you please, knowing that as long as it fulfills the contract, its interposition is transparent.
A closure is a function that has access to locals from a larger scope, namely the context in which it was defined.
Clojure does provide a tail call special form recur
, but it only optimizes the case of a tail-recursive self-call and not the generalized tail call.
There's no technical reason why Clojure couldn't automatically detect and optimize recursive tail calls, but there are valid reasons why Clojure doesn't. First, by making recur
an explicit optimization, Clojure doesn't give the pretense of providing full TCO. Second, having recur
as an explicit form allows the Clojure compiler to detect errors caused by an expected tail call being pushed out of the tail position.
Large-scale design
Macros
With Clojure, there's no distinction between the textual form and the actual form of a program. When a program is the data that composes the program, then you can write programs to write programs.
A few rules of thumb to observe when writing macros:
- Don't write a macro if a function will do. Reserve macros to provide syntactic abstractions or create binding forms.
- Write an example usage.
- Expand your example usage by hand.
- Use
macroexpand
,macroexpand-1
, andclojure.walk/macroexpand-all
liberally to understand how your implementation works. - Experiment at the REPL.
- Break complicated macros into smaller functions whenever possible.
The most obvious advantage of macros over higher-order functions is that the former manipulate compile-time forms, transforming them into runtime forms. This allows your programs to be written in ways natural to your problem domain, while still maintaining runtime efficiency.
Regardless of your application domain and its implementation, programming language boilerplate code inevitably occurs. But identifying these repetitive tasks and writing macros to simplify and reduce or eliminate the tedious copy-paste-tweak cycle can work to reduce the incidental complexities inherent in a project.
One way to design macros is to start by writing out example code that you wish worked – code that has the minimal distance between what you must specify and the specific application domain in which you're working. Then, with the goal of making this code work, you begin writing macros and functions to fill in the missing pieces.
Whereas functions accept and return values that are meaningful to your application at runtime, macros accept and return code forms that are meaningful at compile time.
Clojure programmers don't write their apps in Clojure. They write the language that they use to write their apps in Clojure.
Combining data and code
In idiomatic Clojure source code, you'll see the ns
macro used almost exclusively to create namespaces. By using the ns
macro, you automatically get two sets of symbolic mappings – all classes in the java.lang
package and all of the functions, macros, and special forms in the clojure.core
namespace.
If you decide to name your namespaces with hyphens, à la my-cool-lib
, then the corresponding source file must be named with underscores in place of the hyphens (my_cool_lib.clj
).
When defining namespaces, it's important to include only the references that are likely to be used.
Multimethods provide a way to perform function polymorphism based in the result of an arbitrary dispatch function.
A protocol in Clojure is simply a set of function signatures, each with at least one parameter, that are given a collective name. They fulfill a role somewhat like Java interfaces – a class that claims to implement a particular protocol should provide specialized implementations of each of the functions in that protocol.
Java.next
The apprentice avoids all use of Java classes. The journeyman embraces Java classes, The master knows which classes to embrace and which to avoid.
Extending concrete classes is seen often in Java, doing so in Clojure is considered poor design, leading to fragility, and should therefore be restricted to those instances where interoperability demands it.
Mutation
Clojure's main tenet isn't the facilitation of concurrency. Instead, Clojure at its core is concerned with the sane management of state, and facilitating concurrent programming naturally falls out of that.
Concurrency refers to the execution of disparate tasks at roughly the same time, each sharing a common resource.
Parallelism refers to partitioning a task into multiple parts, each run at the same time.
In Clojure's model, a program must accommodate the fact that when dealing with identities, it's receiving a snapshot of its properties at a moment in time, not necessarily the most recent. Therefore, all decisions must be made in a continuum. Clojure provides the tools for dealing with identity semantics via its Ref reference type, the change semantics of which are governed by Clojure's software transactional memory (STM); this ensures state consistency throughout the application timeline, delineated by a transaction.
Clojure's STM uses multiversion concurrency control to ensure snapshot isolation. Snapshot isolation means that each transaction gets its own view of the data that it's interested in.
Clojure has but one transaction per thread, thus causing all subtransactions to be subsumed into the larger transaction. Therefore, when a restart occurs in a subtransaction, it causes a restart of the larger transaction.
Clojure's STM eliminates the need for locking and as a result eliminates dreaded deadlock scenarios.
The general rule of thumb when partitioning units of work should always be get in and get out as quickly as possible.
The most important point to remember about choosing between reference types is that although their features sometimes overlap, each has an ideal use.
The unique feature of Refs is that they're coordinated. This means that reads and writes to multiple refs can be made in a way that guarantees no race conditions.
Value access via the @
reader feature or the deref
function provide a uniform client interface, regardless of the reference type used. On the other hand, the write mechanism associated with each reference type is unique by name and specific behavior, but similar in structure. Each referenced value is changed through the application of a pure function. The result of this function will become the new referenced value. Finally, all reference types allow the association of a validator function via set-validator
that will be used as the final gatekeeper on any value change.
As a rule of thumb, it's best to avoid having both short- and long-running transactions interacting with the same ref.
Each Agent has a queue to hold actions that need to be performed on its value, and each action will produce a new value for the Agent to hold and pass to the subsequent action. Thus the state of the Agent advances through time, action after action, and by their nature only one action at a time can be operating on a given Agent. You can queue an action on any Agent by using send
or send-off
.
send
is for actions that stay busy using the processor and not blocking on I/O or other threads, whereas send-off
is for actions that might block, sleep, or otherwise tie up the thread.
You can choose between two different error-handling modes for each Agent: :continue
and :fail
.
By default, new Agents start out using the :fail
mode, where an exception thrown by an Agent's action will be captured by the Agent and held so that you can see it later. Meanwhile, the Agent will be considered failed or stopped and will stop processing its action queue – all the queued actions will have to wait patiently until someone clears up the Agent's error.
The other error mode of Agents is :continue
, where any action that throws an exception is skipped and the Agent proceeds to the next queued action if any.
Memoization is a way for a function to store calculated values in a cache so that multiple calls to the function can retrieve previously calculated results from the cache, instead of performing potentially expensive calculations every time. Clojure provides a core function memoize
that can be used on any referentially transparent function.
Futures are simple yet elegant constructs useful for partitioning a typically sequential operation into discrete parts. These parts can then be asynchronously processed across numerous threads that will block if the enclosed expression hasn't finished. All subsequent dereferencing will return the calculated value.
Promises are similar to futures, in that they represent a unit of computation to be performed on a separate thread. Likewise, the blocking semantics when dereferencing an unfinished promise are also the same. Whereas futures encapsulate an arbitrary expression that caches its value in the future upon completion, promises are placeholders for values whose construction is fulfilled by another thread via the deliver
function.
The pvalues
macro executes an arbitrary number of expressions in parallel.
The pmap
function is the parallel version of the core map
function. Given a function and a set of sequences, the application of the function to each matching element happens in parallel.
Clojure provides a pcalls
function that takes an arbitrary number of functions taking no arguments and calls them in parallel, returning a lazy sequence of the results.
Tangential considerations
Performance
Make it work first, then make it fast.
The rule of type hinting: Write your code so that it's first and foremost correct; then add type-hint adornment to gain speed. Don't trade the efficiency of the program for the efficiency of the programmer.
We can illuminate a potential cause of inefficiency by using a REPL flag named *warn-on-reflection*
, which by default is set to false: (set! *warn-on-reflection* true)
. This statement signals to the REPL to report when the compiler encounters a condition where it can't infer the type of an object and must use reflection to garner it at runtime.
Clojure provides an optimization technique called transients, which offer a mutable view of a collection.
The rule of transients: Write your code so that it's first and foremost correct using the immutable collections and operations; then, make changes to use transients for gaining speed.
Clojure changes the way you think
When a language is built from the same data structures that the language itself manipulates, it's known as homoiconic. When a programming language is homoiconic, it's simple to mold the language into a form that bridges the gap between the problem and solution domains.
In Clojure, it's common practice to start by defining and implementing a low-level language specifically for the levels above. Creating complex software systems is hard, but using this approach, you can build the complicated parts out of smaller, simpler pieces.
No amount of testing can substitute for thoroughly thinking through the fundamental design details.