Chapter 8. Metadata

Describing Your Code, in Code

Programmers often talk about metadata, or data about data. The definition of metadata varies in different contexts. Clojure provides mechanisms to attach metadata to objects, but it has a very specific definition: metadata is a map of data attached to an object that does not affect the value of the object.

Two objects with the same value and different metadata are considered equal (and have the same hash code). However, metadata has the same immutable semantics as Clojure's other data structures; modifying an object's metadata yields a new object, with the same value (and the same hash code) as the original object.

When updating a value, some operations preserve metadata and some do not, which this chapter discusses.

Reading and Writing Metadata

By default, metadata is not printed at the REPL. You can change this by setting *print-meta* to true, as we did for all the examples in this chapter.

(set! *print-meta* true)

You can attach metadata to a symbol or any of Clojure's built-in data structures with the with-meta function, and retrieve it using the meta function:

(with-meta obj meta-map)
(meta obj)

with-meta returns a new object, with the same value as obj, that has meta-map as its metadata. meta returns the metadata map of obj. For example, the following code:

user=> (with-meta [1 2] {:about "A vector"})
#^{:about "A vector"} [1 2]

You can also modify the metadata map of an object with the vary-meta function:

(vary-meta obj function & args)

vary-meta takes a function and applies it to the current metadata map of the object plus any arguments. It returns a new object with the updated metadata. For example, the following code:

user=> (def x (with-meta [3 4] {:help "Small vector"}))
user=> x
#^{:help "Small vector"} [3 4]
user=> (vary-meta x assoc :help "Tiny vector")
#^{:help "Tiny vector"} [3 4]

Notice that two objects with the same value and different metadata are equal (tested with Clojure's = function), but they are not the same object in memory (tested with Clojure's identical? function):

user=> (def v [1 2 3])
user=> (= v (with-meta v {:x 1}))
true
user=> (identical? v (with-meta v {:x 1}))
false

Also, note that you can only add metadata to Clojure-specific types such as lists, vectors, maps, and symbols (and functions in Clojure 1.2). Java classes, such as String and Number, do not support metadata.[5]

Metadata-Preserving Operations

Some operations that "modify" an immutable data structure preserve its metadata, others do not. For example, conj on a list preserves its metadata, but cons does not:

user=> (def x (with-meta (list 1 2) {:m 1}))
user=> x
#^{:m 1} (1 2)
user=> (conj x 3)
#^{:m 1} (3 1 2)
user=> (cons 3 x)
(3 1 2)  ;; no metadata!

In general, collection functions (conj, assoc, dissoc, and so on) are supposed to preserve metadata, while sequence functions (cons, take, drop, etc.) are not. But there are exceptions. In Clojure 1.0, conj on a vector does not preserve metadata (this is a bug) but in Clojure 1.1 it does. The moral is this: be careful with operations on data structures that have metadata, and don't assume that metadata will be preserved. Always test first.

Warning

Metadata is quite an unusual feature of Clojure; few programming languages have anything like it. Whenever you consider using metadata, think very carefully about its semantics: metadata is not part of the value of an object. In general, any data that is relevant to users of your application should not be considered metadata. Metadata is information that only you, the programmer, care about.

Read-Time Metadata

The Clojure reader (described in Chapter 2) allows you to attach metadata to forms as they are read using the #^ reader macro. #^ is followed by a map of metadata, which will be attached to the next form read. When *print-meta* is true, Clojure prints metadata using the same syntax. For example, you can attach metadata to a literal vector like this:

user=> #^{:m 1} [1 2]
#^{:m 1} [1 2]

However, be very careful: #^ is not a substitute for with-meta! #^ attaches metadata to literal forms. Consider the following:

user=> #^{:m 1} (list 1 2)
(1 2)   ;; no metadata!

In this example, the #^ reader macro attaches the metadata map {:m 1} to the literal form (list 1 2). When that form is evaluated, it returns the list (1 2) with no metadata.

The #^ reader macro is normally used to attach metadata to symbols, not data structures. Special forms such as def can make use of this read-time metadata (see the following section).

Note

Clojure 1.0 provided the ^ reader macro as a shortcut for meta. However, this shortcut was not very useful and is deprecated in Clojure 1.1. Clojure 1.2 uses ^ in place of #^ for setting read-time metadata.

Metadata on Vars

The most common use of metadata in Clojure is to attach descriptive information to Vars. The def, defn, and defmacro forms attach some default metadata to every Var. For example, the following code:

user=> (meta (var or))
{:ns #<Namespace clojure.core>
 :name or
 :file "clojure/core.clj"
 :line 504
 :doc "Evaluates exprs one at a time..."
 :arglists ([] [x] [x & next])
 :macro true}

In addition, def and its kin will copy metadata from the symbol used to name the Var onto the Var itself. Combined with the #^ reader macro, this provides a convenient way to attach metadata to Vars when they are created:

user=> (def #^{:doc "My cool thing"} *thing*)
#'user/*thing*
user=> (:doc (meta (var *thing*)))
"My cool thing"

Clojure's doc macro uses a Var's :doc and :arglists metadata to print a description of it:

user=> (doc *thing*)
-------------------------
user/*thing*
nil
  My cool thing

The documentation string in the defn and defmacro forms is automatically set as :doc metadata on the Var being defined. defn and defmacro also accept an optional metadata map between the documentation string and the parameter list:

(defn name doc-string meta-map [params] ...)
(defmacro name doc-string meta-map [params] ...)

Clojure uses several standard metadata keys for global Vars. These are described in Table 8-1. If you are adding application-specific metadata, it is recommended that you use namespace-qualified keywords, such as :my-app/meta, as keys to avoid potential name clashes.

Table 8-1. Standard Var Metadata

Metadata Key

Value

Type

:name

The Var's name

Symbol

:ns

The Var's namespace

Namespace

:file

File from which it was loaded

String

:line

Line on which it was defined

Integer

:doc

Documentation string

String

:arglists

Function/macro arguments

List of Vectors of Symbols

:macro

True for macros; false by default

Boolean

:private

True for private Vars, false by default

Boolean

:tag

Type of the value or function return value

Class or Symbol

Type Tags

The :tag metadata key is used to attach type "hints" to symbols and Vars. This helps the Clojure compiler optimize the bytecode it generates. Type hints are described in detail in Chapter 15.

Private Vars

As described in Chapter 7, Vars with : private true in their metadata are private. Private Vars cannot be referred from namespaces other than the one in which they were defined. The defn- macro creates private functions; to create private macros or other Vars, add metadata to the Var like this:

(def #^{:private true} *my-private-var*)  ;; for Vars
(defmacro my-private-macro {:private true} [args] ...)  ;; for macros

Metadata on Reference Types

Clojure's mutable reference types—Var, Ref, Agent, Atom, and also Namespaces—all support metadata. You can change the metadata map for any reference type with the alter-meta! function:

(alter-meta! iref f & args)

alter-meta! works like alter does for Refs; it calls function f on the current metadata map of iref, with addition arguments args. For example, you can add metadata to an existing Var like this:

user=> (alter-meta! (var for) assoc :note "Not a loop!")
{:note "Not a loop!", :macro true, ...
user=> (:note (meta (var for)))
"Not a loop!"

alter-meta! is an atomic operation, but it does not require a transaction like alter.

The ref, agent, and atom functions accept a :meta option specifying an initial metadata map. For example, the following code:

user=> (def r (ref nil :meta {:about "This is my ref"}))
user=> (meta r)
{:about "This is my ref"}

Summary

Metadata is an unusual feature, not something you will make frequent use of in day-to-day programming. Many things that might be reasonably described as "metadata" within in application, such as timestamps or user names, turn out to be a bad fit for Clojure metadata. Metadata is most useful for metaprogramming, where it can describe one piece of code for use by another piece of code. In that sense, it fills a role similar to Java's annotations. The full capabilities of metadata are still being explored by Clojure programmers. Metadata already plays a role in the Clojure compiler (for type hinting) and that role will likely be expanded in future Clojure releases.



[5] Why not? Conceivably, metadata could be stored in a global hash table, allowing metadata to be attached to arbitrary Java objects. However, this design has serious drawbacks with regard to performance and memory usage, so it is not supported.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.76.72