Channels ▼


The Clojure Philosophy

Extreme Flexibility

Why has Lisp persevered for more than 50 years while countless other languages have come and gone? There are probably complex reasons, but chief among them is likely the fact that Lisp as a language genotype fosters language flexibility in the extreme. Newcomers to Lisp are sometimes unnerved by its pervasive use of parentheses and prefix notation, which is different than non-Lisp programming languages. The regularity of this behavior not only reduces the number of syntax rules you have to remember, but also makes the writing of macros trivial. Let's take a look at one now. It's an example that we'll get working on in a moment:

(defn query [max]
  (SELECT [a b c]
    (FROM X
      (LEFT-JOIN Y :ON (= X.a Y.b)))
    (WHERE (AND (< a 5) (< b ~max)))))

We hope some of those words look familiar to you, because this isn't an article on SQL. Regardless, our point here is that Clojure doesn't have SQL support built in. The words SELECT, FROM, and so forth aren't built-in forms. They're also not regular functions, because if SELECT were, then the use of a, b, and c would be an error, because they haven't been defined yet.

So what does it take to define a domain-specific language (DSL) like this in Clojure? Well, it's not production-ready code and doesn't tie into any real database servers; but with just one macro and the three functions shown in Lisiting One, the preceding query returns these handy values:

(query 5)
;=> ["SELECT a, b, c FROM X LEFT JOIN Y ON (X.a = Y.b)
         WHERE ((a < 5) AND (b < ?))"

Note that some words such as FROM and ON are taken directly from the input expression, whereas others such as ~max and AND are treated specially. The max that was given the value 5 when the query was called is extracted from the literal SQL string and provided in a separate vector, perfect for using in a prepared query in a way that will guard against SQL-injection attacks. The AND form was converted from the prefix notation of Clojure to the infix notation required by SQL.

Listing One: A domain-specific language for embedding SQL queries in Clojure.

(ns joy.sql
  (:use [clojure.string :as str :only []])

(defn expand-expr [expr]
  (if (coll? expr)
    (if (= (first expr) 'unquote)
      (let [[op & args] expr]
        (str "(" (str/join (str " " op " ")
                           (map expand-expr args)) ")")))

(declare expand-clause)

(def clause-map
  {'SELECT (fn [fields & clauses]
            (apply str "SELECT " (str/join ", " fields)
              (map expand-clause clauses)))
    'FROM (fn [table & joins]
            (apply str " FROM " table
             (map expand-clause joins)))
    'LEFT-JOIN (fn [table on expr]
            (str " LEFT JOIN " table
                 " ON " (expand-expr expr)))
    'WHERE (fn [expr]
            (str " WHERE " (expand-expr expr)))})

(defn expand-clause [[op & args]]
  (apply (clause-map op) args))

(defmacro SELECT [& args]
  [(expand-clause (cons 'SELECT args))
    (vec (for [n (tree-seq coll? seq args)
              :when (and (coll? n) (= (first n) 'unquote))]
          (second n)))])

In Listing One, on line 2, we use core string functions. On line 6, we handle unsafe literals. On line 9-11, we convert prefix to infix. On lines 13-15, we support each kind of clause. On line 28, we call the appropriate converter. And starting on line 31, we provide the main entrypoint macro.

But the point here isn't that this is a particularly good SQL DSL — more complete ones are available. Our point is that once you have the skill to easily create a DSL like this, you'll recognize opportunities to define your own that solve much narrower, application-specific problems than SQL does. Whether it's a query language for an unusual non-SQL datastore, a way to express functions in some obscure math discipline, or some other application we as authors can't imagine, having the flexibility to extend the base language like this, without losing access to any of the language's own features, is a game-changer.

Although we shouldn't get into too much detail about the implementation, take a brief look back at Listing One and follow along as we discuss important aspects of its implementation.

Reading from the bottom up, you'll notice the main entry point, the SELECT macro. This returns a vector of two items — the first is generated by calling expand-clause, which returns the converted query string, whereas the second is another vector of expressions marked by ~ in the input. The ~ is known as unquote. Also note the use of tree-seq here to succinctly extract items of interest from a tree of values, namely the input expression.

The expand-clause function takes the first word of a clause, looks it up in the clause-map, and calls the appropriate function to do the actual conversion from Clojure s-expression to SQL string. The clause-map provides the specific functionality needed for each part of the SQL expression: inserting commas or other SQL syntax, and sometimes recursively calling expand-clause when subclauses need to be converted. One of these is the WHERE clause, which handles the general conversion of prefix expressions to the infix form required by SQL by delegating to the expand-expr function.

Overall, the flexibility of Clojure demonstrated in this example comes largely from the fact that macros accept code forms, such as the SQL DSL example we showed, and can treat them as data — walking trees, converting values, and more. This works not only because code can be treated as data, but because in a Clojure program, code is data.

Code is Data

The notion that "code is data" is difficult to grasp at first. Implementing a programming language where code shares the same footing as its comprising data structures presupposes a fundamental malleability of the language itself. When your language is represented as the inherent data structures, the language itself can manipulate its own structure and behavior. You may have visions of Ouroboros after reading the previous sentence, and that wouldn't be inappropriate, because Lisp can be likened to a self-licking lollypop — more formally defined as homoiconicity. Lisp's homoiconicity takes a great conceptual leap in order to fully grasp, but we'll lead you toward that understanding in hopes that you too will come to realize the inherent power.

Functional Programming

Quick, what does functional programming mean? Wrong answer.

Don't be too discouraged, however — we don't really know the answer either. 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. Sure, some definitions will be similar, but like snowflakes, no two will be exactly the same. To further muddy the waters, the cognoscenti of computer science will often contradict one another in their own independent definitions. Likewise, the basic structure of any definition of functional programming will be different depending on whether your answer comes from someone who favors writing their programs in Haskell, ML, Factor, Unlambda, Ruby, or Qi. How can any person, book, or language claim authority for functional programming? As it turns out, just as the multitudes of unique snowflakes are all made mostly of water, the core of functional programming across all meanings has its core tenets.

A Workable Definition of Functional Programming

Whether your own definition of functional programming hinges on the lambda calculus, monadic I/O, delegates, or java.lang.Runnable, your basic unit of currency is likely to be some form of procedure, function, or method — herein lies the root. 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. It's beyond this core concept that the definitions branch toward infinity, but thankfully, it's enough to start. Of course, we'll also present a further definition of Clojure's style of functional programming that includes such topics as purity, immutability, recursion, laziness, and referential transparency.

The Implications of Functional Programming

Object-oriented programmers and functional programmers will often see and solve a problem in different ways. Whereas an object-oriented mindset will foster the pproach of defining an application domain as a set of nouns (classes), the functional mind will see the solution as the composition of verbs (functions). Though both programmers may in all likelihood generate equivalent results, the functional solution will be more succinct, understandable, and reusable. Grand claims indeed! We hope that you'll agree that functional programming fosters elegance in programming. It takes a shift in mindset to start from thinking in nouns to arrive at thinking in verbs, but the journey is worthwhile. In any case, we think there's much that you can take from Clojure to apply to your chosen language — if only you approach the subject with an open mind.

Why Clojure Isn't Especially Object-Oriented

Elegance and familiarity are orthogonal. — Rich Hickey

Clojure was born out of frustration provoked in large part by the complexities of concurrent programming, complicated by the weaknesses of object-oriented programming in facilitating it. This section explores these weaknesses and lays the groundwork for why Clojure is functional and not object-oriented.

Defining Terms

Before we begin, it's useful to define terms. (These terms are also defined and elaborated on in Rich Hickey's presentation, "Are We There Yet?").

The first important term to define is time. Simply put, time refers to the relative moments when events occur. Over time, the properties associated with an entity — both static and changing, singular or composite — 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. Imagine a child's flip book to understand the terms fully. The book itself represents the identity. Whenever you wish to show a change in the illustration, you draw another picture and add it to the end of your flip book. The act of flipping the pages therefore represents the states over time of the image within. Stopping at any given page and observing the particular picture represents the state of the image at that moment in time.

It's important to note that in the canon of object-oriented programming, there's no clear distinction between state and identity. In other words, these two ideas are conflated into what's commonly referred to as mutable state. The classical object-oriented model allows unrestrained mutation of object properties without a willingness to preserve historical states. Clojure's implementation attempts to draw a clear separation between an object's state and identity as they relate to time. To state the difference to Clojure's model in terms of the aforementioned flip book, the mutable state model is different, so modeling state change with mutation requires that you stock up on erasers. Your book becomes a single page, requiring that in order to model changes, you must physically erase and redraw the parts of the picture requiring change. Using this model, you should see that mutation destroys all notion of time, and state and identity become one.

Immutability lies at the cornerstone of Clojure, and much of the implementation ensures that immutability is supported efficiently. By focusing on immutability, Clojure eliminates entirely the notion of mutable state (which is an oxymoron) and instead expounds that most of what's meant by objects are instead values. Value by definition refers to an object's constant representative amount, magnitude, or epoch. (Some entities have no representative value — Pi is an example. But in the realm of computing, where we're ultimately referring to finite things, this is a moot point.) You might ask yourself: what are the implications of the value-based programming semantics of Clojure?

Related Reading

More Insights

Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.