A Lisp list is not really terminated with an empty list -- it's terminated with a special value, traditionally called nil
. As it happens, that also traditionally evaluates as false
-- which makes it about as close to C's null pointer as you can get (in C, a null pointer constant is an integer constant equal to zero, which also evaluates to false
).
An empty list is basically just a rather short-hand way of expressing NIL -- since you don't even have a single cons cell, all you're left with is the NIL that terminates the list. In other words, it's not that a list is terminated with an empty list -- it's that an empty list consists of only a terminator.
Using dot notation, it's possible to link cons cells together in other ways, but the result isn't a "list" as the term is normally used.
First, a disclaimer: I'm not deeply familiar with the uniform access principle so you may want to take this with a grain of salt. That said, I would argue that Clojure does observe a uniform access principle: function calls.
The key quote on Wikipedia seems to be that "all services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation", and that's exactly the case with function calls in Clojure. In fact, everything in Clojure (and other Lisps) is a function call except for special forms and macros - and macros actually are functions, with the distinction that they operate on source code. You can even specify function call behavior for your own types by implementing clojure.lang.IFn
:
(deftype Invokable []
clojure.lang.IFn
(invoke [this]
:was-invoked))
(def invokable (Invokable.))
(invokable) ;=> :was-invoked
You may have in mind the use of keywords as functions, which are most specifically associated with hash-maps, but you can actually implement that behavior for different types as well. Here's a silly example where keyword access reads a file (with the same name as the keyword) from your home directory, or returns a default value if such a file doesn't exist:
(ns weird.example
(:require [clojure.java.io :as io]))
(deftype WeirdKlass []
clojure.lang.ILookup
(valAt [this k not-found]
(let [home (System/getProperty "user.home")
file (io/file home (name k))]
(if (.isFile file)
(slurp file)
not-found)))
(valAt [this k]
(.valAt this k nil)))
(def klass (WeirdKlass.))
;; assuming you have a file at $HOME/testfile
(:testfile klass) ;=> "test content!"
(:blah klass) ;=> nil
(:blah klass :not-found) ;=> :not-found
Update based on Alexey's comment
I think I have a better idea what you mean now but in practice I haven't found it to be an inconvenience. When you want or need a level of indirection/abstraction between your code's functionality and its underlying data representation the way to achieve it is: functions!
The simplest way would be to define full-name
as a function which happens to use keyword lookup but could later be changed to instead compute the name (without clients knowing or caring):
;; For now
(defn full-name [person]
(:full-name person))
;; Maybe later
(defn full-name [person]
(str (:first-name person)
" "
(:last-name person)))
Of course, you could also use records and protocols:
(defprotocol IPerson
(full-name [p])
(first-name [p])
(last-name [p]))
(defrecord Person1 [name]
IPerson
(full-name [_]
name)
(first-name [_]
(first (.split name " ")))
(last-name [_]
(last (.split name " "))))
(def person1 (->Person1 "Joe Schmoe"))
(full-name person1) ;=> "Joe Schmoe"
(first-name person1) ;=> "Joe"
(last-name person1) ;=> "Schmoe"
(defrecord Person2 [f-name l-name]
IPerson
(full-name [_]
(str f-name " " l-name))
(first-name [_]
f-name)
(last-name [_]
l-name))
(def person2 (->Person2 "Joe" "Schmoe"))
(full-name person2) ;=> "Joe Schmoe"
(first-name person2) ;=> "Joe"
(last-name person2) ;=> "Schmoe"
The big (very big, in my view) upside of using "plain" data structures when possible is that it means you and your users have Clojure's full range of map and sequence operations at your disposal. Alan Perlis expressed this idea well when he said that "it is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."
Best Answer
One reason Algol-based languages encourage the braces on their own line is to encourage adding more lines in between the delimiting braces without having to move the braces. That is, if one starts out with
it's easy to come along and add another statement within the braces:
Had the original form been
then we'd have to have "moved" two braces, but my example is more concerned with the latter. Here, the braces are delimiting what's intended to be a sequence of statements, mostly invoked for side effect.
Conversely, Lisp lacks statements; every form is expression, yielding some value—even if in some rare cases (thinking of Common Lisp), that value is deliberately chosen to be "no values" via an empty
(values)
form. It's less common to find sequences of expressions, as opposed to nested expressions. The desire to "open up a sequence of steps until the closing delimiter" doesn't arise as often, because as statements go away and return values become more common currency, it's more rare to ignore the return value of an expression, and hence more rare to evaluate a sequence of expressions for side effect alone.In Common Lisp, the
progn
form is an exception (as are its siblings):Here,
progn
evaluates the three expressions in order, but discards the return values of the first two. You could imagine writing that last closing parenthesis on its own line, but note again that since the last form is special here (not in the Common Lisp sense of being special, though), with distinct treatment, it's more likely that one would add new expressions in the middle of the sequence, rather than just "adding another one on to the end," as callers would then be impacted not just by any new side effects but rather by a likely change in return value.Making a gross simplification, the parentheses in most parts of a Lisp program are delimiting arguments passed to functions—just like in C-like languages—and not delimiting statement blocks. For the same reasons we tend to keep the parentheses bounding a function call in C close around the arguments, so too do we do the same in Lisp, with less motivation to deviate from that close grouping.
The closing of the parentheses is of far less import than the indentation of the form where they open. In time, one learns to ignore the parentheses and write and read by shape—much like Python programmers do. However, don't let that analogy lead you to think that removing the parentheses entirely would be worthwhile. No, that's a debate best saved for
comp.lang.lisp
.