stats

Read Me Clojure, Pt. 5

Type Systems

Image

Data types allow us to classify values according to various characteristics and to establish relations between the resulting classes. This helps the programmer define operations performed on data of different kinds, and it helps language mechanisms manage memory and detect certain kinds of errors. In Clojure, we deal with several interrelated type systems that we can extend, and by leveraging their polymorphic mechanisms, we are able to abstract data management and build unified information exchange interfaces.

Type Systems

Because of the one-dimensional, finite-size operational memory in use, a computer program must be able to predict (at compile time or at runtime) how much space to reserve for given data and what form to give it. Furthermore, the programmer should be able to distinguish, on some basis, what kinds of information are being dealt with, so as to apply appropriately chosen algorithms to the structures representing them. To address these issues, one can introduce a classification of data kinds and label each value stored in memory accordingly.

Data Types

A data type is a class of values characterized by certain properties common to a given kind of information and the ways in which the programming language mechanisms manage it. These properties may include ranges, sizes of occupied memory spaces, ways of ordering or representing data, as well as other attributes considered distinguishing within what is known as a type system.

Types determine which operations and representations are valid for values. Depending on the language, type information may be explicit or inferred, and constraints may be checked before execution or while the program runs. Strong versus weak typing is separate from static versus dynamic checking: a strongly typed language need not require annotations or report every mismatch at compile time.

Type information can also support optimization. A compiler or JIT may select a primitive instruction, resolve a host method without reflection, inline a call, or specialize hot machine code for observed classes. These techniques are implementation- and context-dependent; a polymorphic Clojure function is not automatically compiled into one source-level variant per possible argument type.

Type System Characteristics

A type system is, in programming languages, a mechanism that labels each value, reference to a value, or area storing a value with a data type, and also enforces typing rules and maintains relations between types.

One can say that within a type system, each data type has an assigned set of rules that must be adhered to. This protects against errors occurring at application runtime and allows the creation of predictable data exchange interfaces between different parts of the software.

Kinds of Typing

Checking whether the operations and rules of the type system are applied can take place during compilation or while the program is running. In the first case, we call this process static typing, and in the second dynamic typing.

In statically typed languages, a way to relieve the programmer of the obligation to specify the type of every expression is type inference. It consists of the compiler automatically detecting the data types of individual expressions and internally labeling them in the appropriate way.

The labels weakly typed and strongly typed are used inconsistently. Broadly, they describe how readily a language permits operations or implicit conversions across incompatible types; they do not tell us when checks occur. It is therefore more useful to state the concrete conversion and error rules than to infer them from either label.

In Clojure, values carry runtime types and most value-dependent compatibility checks occur during execution. The compiler still rejects malformed forms, unresolved symbols, invalid arities in some statically known calls, and some impossible host interop.

Clojure is also implicitly typed in the practical sense that declarations normally need no type annotations. The compiler can track selected host and primitive types for code generation, but this local inference is not a comprehensive static proof of function contracts.

Type Hierarchies

Within a type system, hierarchical membership relations can be maintained. We can then speak of subtypes and supertypes relative to selected types. Such mechanisms allow not only distinguishing the relations of value classes and using this information to steer the application’s logic, but they are also often integrated with constructs that allow operating on data. For example, in more strongly typed languages, to a function whose argument types have been predetermined, we may also pass values of their subtypes. In Java, this includes methods that accept arguments of the Object type, defined by a class that is an ancestor of nearly all other object classes (with the exception of the Object class itself and interfaces). We can therefore pass values of other types (e.g., String or Integer) to such functions, which are direct or indirect subtypes of the Object type.

The handling of subtypes and supertypes, allowing us to operate on passed values in the manner described above, is called subtype polymorphism or inclusion polymorphism. It lets us abstract operations on data while preserving the relations between their specific types and make use of so-called type coercion.

Roles of Type Systems

To summarize, we can distinguish the following functions of typing depending on the category:

  • Domain modeling (business semantics):

    • distinguishing conceptual classes
      (e.g., types Human and Plant),

    • determining relations between value classes and concretization
      (e.g., type Animal and subtype Mammal).

  • Building abstractions:

    • distinguishing implementation data
      (e.g., types String and Integer),

    • distinguishing interfaces
      (e.g., Seq, Comparable).

  • Contracts:

    • specifying interface requirements regarding input/output data
      (e.g., types of arguments or values returned by functions or APIs),

    • enforcing completeness of case handling
      (e.g., based on sum types).

  • Representation:

    • determining properties of data structures
      (e.g., sizes or ways of representing values in memory),

    • controlling data organization in memory
      (e.g., int vs. long, value structures vs. references),

    • hints for the compiler regarding optimizations.

  • Correctness verification:

    • determining correctness conditions
      (e.g., maximum value ranges or pattern matching),

    • early detection of errors involving type mismatches and incompatible properties.

  • Documentation:

    • tools for IDEs
      (e.g., auto-completion, refactoring, navigation),

    • generating schemas
      (e.g., OpenAPI or GraphQL).

Type Systems in Clojure

Clojure is an implicitly and dynamically typed language. Type hints on symbols and expressions can help the JVM compiler avoid reflection or select supported primitive representations, but hints neither validate arbitrary values nor turn a function into a statically checked API contract.

For this chapter it is useful to distinguish three interacting type-related layers:

  • the object type system of the host platform,
  • the primitive type system of the host platform,
  • Clojure’s ad-hoc hierarchies, used primarily by multimethods.

Protocols add open, class-based dispatch over host types, while predicates, specs, or schemas express data contracts that these three layers do not capture by themselves.

Object Type System

The runtime platforms of the Clojure language, such as the JVM or CLR, are object-oriented. In such environments, we can not only use built-in types, but also create new data types, sometimes called object types.

Objects

An object, i.e., a value of a specific object type, is called an instance of a particular class.

An object has class identity and may contain instance fields. Methods are defined by its class or inherited contracts; an implementation need not store a separate function reference in every object.

Classes

class characterizes a data type – it is a template used to define it and consists of:

  • declarations of member variables, also called fields or attributes,
    in which class instances store data;

  • definitions of member functions, also called methods,
    which are operations associated with instances or with the class itself.

A class combines a representation contract with methods and inheritance rules. A C struct can model part of the representation, but it does not by itself provide the class identity, method dispatch, visibility, or inheritance semantics of the JVM.

Inheritance

Inheritance creates a derived class with access to inherited members; it does not modify the existing base class. For example, the class Animal can be a base class relative to the class Mammal. In the latter, we then do not need to define the properties and operations typical for objects of the Animal type, because they will be inherited. We can therefore extend existing classes in subclasses by adding new methods or fields, as well as replace existing ones with more suitable variants.

In turn, the way to ensure that different classes are equipped with predetermined sets of operations is through the use of so-called interfaces. Once created, an interface containing method declarations can then be implemented by multiple classes. For example, the Mammal class can implement the Animalistic interface, which would contain declarations of operations typical for animals (e.g., get_number_of_legs, set_weight, etc.). The Mammal class would have to define all abstract methods required by the contract. Modern Java interfaces may also contain default, static, and private method implementations, so they are not limited to bare declarations.

We learned earlier about subtype polymorphism, which is also present in object type systems. In this case, it can also be referred to as interface inheritance, and according to the adopted nomenclature, the direct supertype of a given type is also called:

  • a base class,
  • a parent class,
  • a superclass;

and the direct subtype:

  • a derived class,
  • a child class,
  • a subclass.

We will also consider all descendant classes, sometimes called descendants, as subtypes, and all ancestor classes, called ancestors, as supertypes.

In the case of Java, the ancestor of all classes is java.lang.Object.

Interfaces

An interface is a collection of method declarations with a common purpose. We can treat an interface as a contract between a class and the rest of the program, in which the class, by promising to implement the interface, declares that the set of capabilities specified by it will be reflected in the methods defined within it.

A class can implement more than one interface and must supply any abstract methods not already provided by a superclass or interface default. An already compiled Java class does not acquire a new Java interface merely because client code wishes it to; Clojure protocol extension is a separate, open mechanism.

Example of an interface in Java
interface ICountable {
  int countIt();
}

class TextValue implements ICountable {
  String value;
  public int countIt() {
    return value.length();
  }
}
interface ICountable { int countIt(); } class TextValue implements ICountable { String value; public int countIt() { return value.length(); } }

If we, for example, created an interface ICountable, in which a method countIt were declared for calculating the number of elements of some abstract collection, every class that we would mark as implementing ICountable would have to be equipped with a countIt method. In the case of a class responsible for representing character strings, it could calculate the number of UTF-16 code units in the string stored in the instance; in the case of arrays, the number of their elements, etc. In the program, we could then check whether a given class implements our interface and thus expect it to contain the counting method specified by the contract.

In Clojure, host interfaces matter when integrating with Java APIs, defining primitive method signatures, or exposing a contract directly to Java. For Clojure-owned open abstractions, protocols are often the more flexible default.

Defining Interfaces, definterface

In Clojure, definterface can define and load a Java interface during normal code loading. Its abstract method signatures must be implemented by concrete classes.

Protocols complement rather than universally replace Java interfaces. Choose according to the boundary: protocol for open Clojure dispatch, interface for a host-level contract or interop requirement.

To create a new Java interface, we can use the definterface macro.

Usage:

  • (definterface name signature...);

The name should be an unquoted symbol, while the signatures should be list S-expressions in the form:

  • (method-name [parameter...]) or
  • (^return-type method-name [^parameter-type parameter...]).

Java interface methods have fixed arities; multiple parameter vectors declare overloads rather than a Clojure variadic method.

Generating Interfaces, gen-interface

The gen-interface macro performs low-level AOT generation of Java interfaces. When compiling, it writes bytecode for the package-qualified interface name to the directory held in *compile-path*; outside compilation it does nothing.

Usage:

  • (gen-interface & option...).

Protocols

Clojure protocols describe named operations with class-based runtime dispatch. On the JVM a protocol generates an interface plus Vars and dispatch machinery, but its extension semantics are more open than Java’s implements declaration.

Choose a Java interface when an external API requires that interface, when overloads or primitive method signatures are part of the host contract, or when Java callers should see that precise type. Choose a protocol when independently extending Clojure-owned operations to existing classes is the central need.

We can view protocols as one of the ways to extend existing functions to handle new data types without the need to modify previously written code. This is a mechanism of dynamic polymorphism that addresses the open-extension side of the so-called expression problem.

In a protocol, we declare a set of functions, each of which must then be defined in a variant for every object data type implementing the given protocol. When such a polymorphic function is called, the data type of the value passed as its first argument determines which specific function implementation the call will be dispatched to.

Created protocols can be assigned to data types – in which case the declared functions must be defined immediately. One can also enrich newly created data types with protocol implementations, and even extend existing types with compatibility with selected protocols.

See also:

Defining Protocols, defprotocol

To create a new protocol, we can use the defprotocol macro.

Usage:

  • (defprotocol name doc-string? & option... & signature...).

The protocol name provided as the first argument should be an unquoted symbol, which will become the name of the created interface (on the JVM platform). After the name, an optional documentation string can be placed, expressed as a string literal enclosed in quotation marks. The subsequent arguments of the macro are options and declarations of polymorphic functions expressed as signatures built from list S-expressions.

Example of a protocol in Clojure
(defprotocol Countable
  (count-it [this]))

(extend-type java.lang.String
  Countable
    (count-it [s] (count s)))

(count-it "abc")
; => 3
(defprotocol Countable (count-it [this])) (extend-type java.lang.String Countable (count-it [s] (count s))) (count-it "abc") ; => 3

The defprotocol macro requires that declared methods always specify a first argument, which will serve to pass the value matched to the detected data type when deciding which variant of the function to call.

A detailed description of the defprotocol macro’s usage can be found in the chapter on polymorphism.

Creating Types

Creating new object types involves defining classes that will determine their properties. In Clojure, we can use abstractions that hide the generation of host-system classes, although there are also appropriate macros that allow close integration with Java mechanisms and more precise control over class construction.

Defining Types, deftype

The deftype macro defines a named JVM class with the requested fields and protocol/interface methods. Its package is derived from the current namespace, and bytecode is generated dynamically so the type is available during interactive development as well as AOT compilation.

During AOT compilation, the generated class files are written beneath the directory named by *compile-path*, following the JVM’s package-to-path convention.

Instances of the data type created using the deftype macro will contain the fields specified by the programmer. It will also be possible to use methods declared in implemented Clojure protocols and Java interfaces, defined within the macro invocation.

The field set of a deftype instance is fixed by its class. Unlike a record, a deftype does not implement a persistent map and cannot acquire extra associative entries through assoc.

Usage:

  • (deftype name [field...] option-or-spec...).

The first argument of the macro should be a type name, and the subsequent (optional) ones should be field names, which may contain type hints.

Fields expressed as symbols can optionally be equipped with metadata that affect how they are handled:

  • :volatile-mutable
    – setting the value to true (or using ^:volatile-mutable) will cause the field to be mutable with Java volatile visibility semantics;

  • :unsynchronized-mutable
    – setting the value to true (or using ^:unsynchronized-mutable) will cause the field to be mutable without synchronization or volatile visibility guarantees.

Field slots are final by default. The two mutable markers make the slot assignable from the type’s methods; they do not make an immutable value stored in that slot mutable. Use them only when the required mutation and concurrency contract is explicit.

After field names, we can provide options and so-called specs (short for specifications). The latter are important when implementing methods specified by protocols or host platform interfaces.

Each specification consists of the name of a protocol or host platform interface, followed by the method implementations needed to satisfy that contract. This is used for creating polymorphic operations, which are described in chapter XXI.

Each option should be a key value pair, and currently the only useful key is :load-ns. If we associate it with the value true, then importing the class defining the type will cause the namespace in which the type was defined to be loaded.

A constructor will also be added to the newly created type, which allows initializing created objects with field values. In addition, a function that invokes it will be defined:

  • (->name value...).

where name is the name of the created type, and value is a field value. It allows creating objects by providing field values expressed positionally.

Both the generated constructor and ->type function are supported. The function is often convenient in Clojure and easier to pass as a value; direct constructor syntax is appropriate at explicit JVM interop boundaries.

As a result of the macro’s work, a class with the specified fields and methods will be defined.

Example of using the deftype macro
 1(deftype Person [name last-name age])
 2; => user.Person
 3
 4;; creating an instance of the Person class by invoking the constructor
 5
 6(def person (Person. "Paweł" "Wilk" 18))
 7
 8;; creating an instance of the Person class by invoking the -> function
 9
10(def person (->Person "Paweł" "Wilk" 18))
11
12;; checking types
13
14(type Person)  ; => java.lang.Class
15(type person)  ; => user.Person
16
17;; invoking field accessors
18
19(.name person)       ; => "Paweł"
20(.last-name person)  ; => "Wilk"
(deftype Person [name last-name age]) ; => user.Person ;; creating an instance of the Person class by invoking the constructor (def person (Person. "Paweł" "Wilk" 18)) ;; creating an instance of the Person class by invoking the -> function (def person (->Person "Paweł" "Wilk" 18)) ;; checking types (type Person) ; => java.lang.Class (type person) ; => user.Person ;; invoking field accessors (.name person) ; => "Paweł" (.last-name person) ; => "Wilk"
Creating Objects, ->type

Objects of a type defined with deftype can be created either with direct JVM constructor syntax or with the generated ->type function, where type is the type name.

Usage:

  • (->type field...).

The arguments of the function call should be the values of successive fields defined by the object’s type. The return value is an object of the defined type.

Example of using the ->type function
1(deftype Person [name last-name age])
2
3(instance? Person (->Person "Paweł" "Wilk" 18))
4; => true
(deftype Person [name last-name age]) (instance? Person (->Person "Paweł" "Wilk" 18)) ; => true
Defining Records, defrecord

The defrecord macro defines so-called record data types, which are new types of the host platform used to store information in the form of records.

Record instances provide a complete persistent-map interface. Each declared field is available under a keyword key while retaining efficient field-backed storage.

Additional key/value entries may be associated with a record. They live in its persistent extension map; they do not become new JVM fields or mutate the existing record instance.

Usage:

  • (defrecord name [field...] option-or-spec...).

The first argument of the macro should be the name of the record type, and the subsequent (optional) ones should be field names. After the field names, we can provide options and so-called specs (short for specifications).

A description of the defrecord macro’s usage can be found in the chapter on collections.

Generating Classes, gen-class

When the need arises to create a new, “pure” Java class, we can use the gen-class macro. It causes JVM bytecode for a class with the given name to be generated during compilation; the name should be a fully qualified Java package name. A file with the class extension will be placed in the filesystem directory whose name is stored in the dynamic variable *compile-path*.

When the program is not being compiled (this refers to AOT compilation), gen-class will have no effect.

Using gen-class, we will be able to create Java classes, but with limitations (including no mutable, public fields). This stems from the purpose of gen-class and other constructs of this kind. They serve primarily to ensure interoperability with Java, not to generate all possible constructs of that language. For instance, gen-class can expose Clojure implementations through named classes that Java libraries can reference or extend.

The most common use case for the gen-class macro is to generate a class equipped with a public, static main method, whose implementation can be defined as a Clojure function, e.g.: (defn -main [] (println "hey")). This makes it possible to build standalone applications that are easy to launch. We will also encounter similar constructs when a Clojure program is intended to handle requests in the servlet model.

Usage:

  • (gen-class & option...).

The applicable options are key-value pairs, where keys are keywords and values are various types depending on specific options:

  • :name name
    – the Java class name;

  • :extends class
    – the name of the superclass whose public methods will be overridden;

  • :implements [interface...]
    – names of interfaces whose methods will be defined;

  • :init constructor
    – the name of an initialization function that returns superclass-constructor arguments and optional instance state;

  • :constructors {[parameter-types] [superclass-parameter-types] ...}
    – constructor signatures, if different from the inherited ones;

  • :post-init trigger
    – the name of a function that will be called during instantiation;

  • :methods [ [name [parameter-types] return-type] ...]
    – signatures of additional methods that will be included in the class;

  • :main flag
    – when the flag is true, a static main function will be generated;

  • :factory factory-name
    – creates a static factory method with the given name;

  • :state field
    – creates a public, final field with the given name;

  • :exposes {protected-field {:get name :set name} ...}
    – generates getters and setters for protected fields of the superclass;

  • :exposes-methods {superclass-method-name alias ...}
    – exposes superclass methods under local aliases;

  • :prefix prefix
    – sets the prefix for names of Clojure functions implementing the methods;

  • :impl-ns namespace
    – the namespace containing Var objects pointing to method implementations;

  • :load-impl-ns flag
    – when true, the generated class loads its implementation namespace from its static initializer.

The generated class contains real Java methods. Their bodies locate and invoke Vars in the implementation namespace, using the configured prefix (or the default -) to derive each Clojure implementation-function name.

Example of using gen-class
 1(ns pl.randomseed.Human)
 2
 3;; creating a Java class
 4
 5(gen-class
 6 :name         pl.randomseed.Human
 7 :main         true
 8 :prefix       "-human-"
 9 :methods      [[greet [String] String]])
10
11;; creating method implementations
12
13(defn -human-greet [_ name]
14  (str "Hello, " name))
15
16(defn -human-main [args]
17  (println (-human-greet nil (or (first args) "world"))))
(ns pl.randomseed.Human) ;; creating a Java class (gen-class :name pl.randomseed.Human :main true :prefix "-human-" :methods [[greet [String] String]]) ;; creating method implementations (defn -human-greet [_ name] (str "Hello, " name)) (defn -human-main [args] (println (-human-greet nil (or (first args) "world"))))

To compile the above example, place the presented source code in the appropriate file:

SH
 1mkdir -p /tmp/gen-class-example/src/pl/randomseed
 2mkdir /tmp/gen-class-example/classes
 3cd /tmp/gen-class-example
 4
 5# editing the file
 6edit src/pl/randomseed/Human.clj
 7
 8# compilation
 9clojure -Sdeps '{:paths ["src"]}' -M \
10  -e "(binding [*compile-path* \"classes\"] (compile 'pl.randomseed.Human))"
mkdir -p /tmp/gen-class-example/src/pl/randomseed mkdir /tmp/gen-class-example/classes cd /tmp/gen-class-example # editing the file edit src/pl/randomseed/Human.clj # compilation clojure -Sdeps '{:paths ["src"]}' -M \ -e "(binding [*compile-path* \"classes\"] (compile 'pl.randomseed.Human))"

See also:

Generating proxies, proxy

A macro similar to gen-class is proxy. It also generates a new class, but with three significant differences:

  • the class resides in memory
    (not only in a .class file during AOT compilation);

  • the class does not have a public name
    that could be referenced later without resorting to certain tricks;

  • the class is immediately instantiated
    (the value returned by the macro is an object).

We cannot add new methods to proxy classes. They serve to “wrap” existing classes and override their existing methods, or to define methods that were declared earlier (in the case of interfaces). Methods defined in the proxy construct are not class methods, but functions to which method calls of the anonymous class are redirected.

We will most often use the proxy macro to quickly ensure compatibility with integrated Java libraries.

Usage:

  • (proxy [class-or-interface...] [argument...] & definition...),

where:

  • [class-or-interface...]
    is a vector S-expression with class and/or interface names;

  • [argument...]
    is a vector S-expression with arguments passed to the superclass constructor;

  • definition...
    is a list S-expression defining functions.

Example of using the proxy macro
;; defining a function
;; that will produce an object "wrapped" in a proxy

(defn numeric [value]
  (proxy [java.lang.Number] []              ; proxy for the number superclass
    (toString [] (str "number: " value))))  ; implementation of the toString method

;; defining a variable with a value

(def x (numeric 123))

;; displaying the value
;; (overridden toString method returns the number with a prefix)

x
; => number: 123
;; defining a function ;; that will produce an object "wrapped" in a proxy (defn numeric [value] (proxy [java.lang.Number] [] ; proxy for the number superclass (toString [] (str "number: " value)))) ; implementation of the toString method ;; defining a variable with a value (def x (numeric 123)) ;; displaying the value ;; (overridden toString method returns the number with a prefix) x ; => number: 123

Note that the str function used in our implementation internally also refers to toString, but in the original implementation. Additionally, it is worth knowing that proxy closes over all lexical bindings that exist in the lexical environment of its invocation.

However, arithmetic on x would eventually call one of Number’s abstract numeric conversion methods. Our proxy implements none of them, so its generated fallback throws UnsupportedOperationException. Inherited concrete methods remain available; the failure comes from the unimplemented abstract contract.

In functions implementing methods, we can also refer to the original counterparts from the ancestor class. The proxy-super function serves this purpose.

See also:

Reification, reify

The reify macro creates an instance of an anonymous class implementing selected Java interfaces and Clojure protocols. It is usually the smaller construct when no superclass constructor or dynamic proxy method map is needed.

With reify we can create objects that behave according to the specification defined by the given patterns. To achieve this, an anonymous class implementing the defined methods is created. This allows us to quickly produce a one-off instance of the needed type, modified in terms of behavior in a way that suits us.

Usage:

  • (reify option... specification...).

Each provided specification should consist of a protocol name, interface name, or the Object class, as well as list S-expressions containing method definitions:

pattern-name
(method-name [argument...] body) ...

Defined methods close over the values of bindings from the lexical environment.

The differences compared to the proxy macro are as follows:

  • Defined methods become methods of the generated anonymous class and may close over lexical values. Their performance still depends on the call path, compiler, and JVM; reify is chosen primarily for its contract and scope, not a blanket speed promise.

  • As a consequence of the above, dynamic method replacement is not possible.

  • We can only reify interfaces or protocols, not classes.

  • Method definitions should accept the value of the own object as the first argument.

Let us try to see reify in action, using an example of enriching a data type with the countability trait.

Example of using the reify macro
(defn numeric [value]
  (reify
    Object
    (toString [this] (str "number: " value))))

(str (numeric 3))

;; we bind the numeric value to x

(def x (numeric 123))

;; we display the numeric value
;; converted to a string
;; using the toString method
;; in our own version

(str x)
; => number: 123
(defn numeric [value] (reify Object (toString [this] (str "number: " value)))) (str (numeric 3)) ;; we bind the numeric value to x (def x (numeric 123)) ;; we display the numeric value ;; converted to a string ;; using the toString method ;; in our own version (str x) ; => number: 123

In the above example we can observe that the value of the value function argument was closed over in the method of the anonymous class produced by the reify macro. The produced object, when its toString method is called, will use its modified version, in which the value is always the one provided during the invocation of the numeric function, that is, during object generation.

Extending types

By supertypes of existing object types, we mean not only data types defined by classes from which the defining classes inherit, but also implemented interfaces and protocols.

We can declare that a given type implements a protocol also after the type has been defined. When we already have a protocol that is (or is not) implemented by some data types, we can extend new types with its support. This involves associating the type with the protocol and defining the required handler functions.

To extend types with protocol support, we can use one of three forms:

They work similarly, but the choice of a specific one will depend on whether we have many data types that we want to enrich with an implementation of some protocol (extend-protocol), or perhaps we have a single type that we wish to extend (extend-type). Both macros use the extend function, which works similarly to the first one mentioned, but requires the use of maps in which the keys naming the defined functions are keywords, and the values are function objects.

A detailed description of how to use the extend function and the extend-type and extend-protocol macros can be found in the chapter on polymorphism.

Testing types

Every non-nil JVM Clojure value has a runtime Java class. Classes from java.lang are imported into each namespace, other classes may be imported or named fully, and Clojure defines additional classes under clojure.lang. The value nil corresponds to Java null and has no class.

Let us look at example literals and the object data types corresponding to the values they represent:

  •    "a"java.lang.String,
  •     \ajava.lang.Character,
  •   truejava.lang.Boolean,
  •    123java.lang.Long,
  •   123Mjava.math.BigDecimal,
  •   123Nclojure.lang.BigInt,
  •     :aclojure.lang.Keyword,
  •     'aclojure.lang.Symbol,
  •   '(1)clojure.lang.PersistentList,
  •    [1]clojure.lang.PersistentVector,
  • {:a 2}clojure.lang.PersistentArrayMap.
Examining class, class

The class function returns the class of the given object.

Usage:

  • (class value).

The argument to the function call should be any value, and the returned value will be the class.

Examples of using the class function
 1(class 1)        ; => java.lang.Long
 2(class "abc")    ; => java.lang.String
 3(class \a)       ; => java.lang.Character
 4(class nil)      ; => nil
 5(class true)     ; => java.lang.Boolean
 6(class false)    ; => java.lang.Boolean
 7(class (fn []))  ; => an implementation-generated function class
 8(class [])       ; => clojure.lang.PersistentVector
 9(class :a)       ; => clojure.lang.Keyword
10(class 'a)       ; => clojure.lang.Symbol
11
12(class '^{:type ::Something} a)
13; => clojure.lang.Symbol
(class 1) ; => java.lang.Long (class "abc") ; => java.lang.String (class \a) ; => java.lang.Character (class nil) ; => nil (class true) ; => java.lang.Boolean (class false) ; => java.lang.Boolean (class (fn [])) ; => an implementation-generated function class (class []) ; => clojure.lang.PersistentVector (class :a) ; => clojure.lang.Keyword (class 'a) ; => clojure.lang.Symbol (class '^{:type ::Something} a) ; => clojure.lang.Symbol

Note the difference in the returned value in the last expression compared to the example of calling type.

Type assertion, cast

The cast function is used to condition further program execution depending on whether we are dealing with a given type or one of its subtypes.

Usage:

  • (cast type value).

The first argument to the function should be a data type, and the second the value being checked. If the type of the value is the given type or one of its subtypes (one of the subclasses or interface implementations), the given value is returned. Otherwise, a java.lang.ClassCastException exception is thrown.

Examples of using the cast function
 1(cast clojure.lang.Keyword :a)
 2; => :a
 3
 4(cast Object :a)
 5; => :a
 6
 7(cast String :a)
 8; >> java.lang.ClassCastException:
 9; >> Cannot cast clojure.lang.Keyword to java.lang.String
10
11(cast Long (int 5))
12; >> java.lang.ClassCastException:
13; >> Cannot cast java.lang.Integer to java.lang.Long
(cast clojure.lang.Keyword :a) ; => :a (cast Object :a) ; => :a (cast String :a) ; >> java.lang.ClassCastException: ; >> Cannot cast clojure.lang.Keyword to java.lang.String (cast Long (int 5)) ; >> java.lang.ClassCastException: ; >> Cannot cast java.lang.Integer to java.lang.Long

cast performs a checked JVM reference cast; it does not convert the value. The last expression therefore fails instead of turning an Integer into a Long.

Class predicate, class?

The class? function is used to check whether we are dealing with a class.

Usage:

  • (class? value).

The function returns true if the given value is a class. Otherwise, it returns false.

Examples of using the class? function
1(class? 1)                    ; => false
2(class? "abc")                ; => false
3(class? nil)                  ; => false
4(class? true)                 ; => false
5(class? Long)                 ; => true
6(class? java.lang.Long)       ; => true
7(class? clojure.lang.Symbol)  ; => true
(class? 1) ; => false (class? "abc") ; => false (class? nil) ; => false (class? true) ; => false (class? Long) ; => true (class? java.lang.Long) ; => true (class? clojure.lang.Symbol) ; => true
Instance predicate, instance?

The instance? function is used to check whether the object representing a value is an instance of a class or an interface with the given name.

Usage:

  • (instance? class value).

The function returns true if the value given as the second argument is an instance of the class passed as the first argument. Otherwise, it returns false.

All interfaces that the given object implements, as well as all its superclasses in the inheritance path, will be checked.

Examples of using the instance? function
1(instance? Long       1)  ; => true
2(instance? Integer    1)  ; => false
3(instance? Number     1)  ; => true
4(instance? Object     1)  ; => true
5(instance? String "abc")  ; => true
6
7(instance? java.lang.Number      1)  ; => true
8(instance? clojure.lang.Keyword :a)  ; => true
9(instance? clojure.lang.IFn     :a)  ; => true
(instance? Long 1) ; => true (instance? Integer 1) ; => false (instance? Number 1) ; => true (instance? Object 1) ; => true (instance? String "abc") ; => true (instance? java.lang.Number 1) ; => true (instance? clojure.lang.Keyword :a) ; => true (instance? clojure.lang.IFn :a) ; => true
Inheritance predicate, isa?

The isa? function allows us to check whether a given type is a direct or indirect subtype of another type.

Usage:

  • (isa? descendant parent).

The first accepted argument is the descendant class, and the second is the parent class.

The returned value will be true if the given derived class is a direct or indirect descendant of the parent class. Otherwise, the returned value will be false.

All superclasses of the given descendant class, as well as all their interfaces, will be examined.

Example of using the isa? function
1(isa? String  Object)                           ; => true
2(isa? Class   Object)                           ; => true
3(isa? Integer Number)                           ; => true
4(isa? clojure.lang.Keyword java.lang.Runnable)  ; => true
5
6(derive clojure.lang.Associative ::collection)
7(isa?   clojure.lang.Associative ::collection)
8; => true
(isa? String Object) ; => true (isa? Class Object) ; => true (isa? Integer Number) ; => true (isa? clojure.lang.Keyword java.lang.Runnable) ; => true (derive clojure.lang.Associative ::collection) (isa? clojure.lang.Associative ::collection) ; => true

Note that isa? also allows examining the relationship between object types and (discussed later) tag-based types, when they have been defined by a hierarchy. The function can also be used in reference to tag-based types.

Superclasses and interfaces, supers

The supers function is used to examine the superclasses and interfaces of a given class.

Usage:

  • (supers class).

The function returns a set containing the direct and all indirect superclasses, as well as all interfaces that the class or interface passed as an argument implements.

Examples of using the supers function
 1(contains? (supers Long) Number)                  ; => true
 2(contains? (supers Long) Object)                  ; => true
 3(contains? (supers Long) java.io.Serializable)    ; => true
 4
 5(contains? (supers clojure.lang.IFn)
 6           java.util.concurrent.Callable)         ; => true
 7
 8(supers nil)
 9; => nil
10
11(contains? (supers java.lang.Boolean) Object)     ; => true
12(contains? (supers (class :a)) clojure.lang.IFn) ; => true
(contains? (supers Long) Number) ; => true (contains? (supers Long) Object) ; => true (contains? (supers Long) java.io.Serializable) ; => true (contains? (supers clojure.lang.IFn) java.util.concurrent.Callable) ; => true (supers nil) ; => nil (contains? (supers java.lang.Boolean) Object) ; => true (contains? (supers (class :a)) clojure.lang.IFn) ; => true

The complete sets can differ between Java releases as standard classes gain new interfaces; tests should assert the relationships they require rather than a frozen printed set.

Superclass and interfaces, bases

The bases function is used to examine the superclass and direct interfaces of a given class.

Usage:

  • (bases class).

The function returns a sequence containing the base class, as well as all direct interfaces that the class passed as an argument (which can also be an interface) implements.

Examples of using the bases function
 1(contains? (set (bases Long)) Number)                  ; => true
 2(contains? (set (bases Long)) Comparable)              ; => true
 3
 4(contains? (set (bases clojure.lang.IFn))
 5           java.util.concurrent.Callable)              ; => true
 6
 7(bases nil)
 8; => nil
 9
10(contains? (set (bases java.lang.Boolean)) Object)     ; => true
11(contains? (set (bases (class :a))) clojure.lang.IFn) ; => true
(contains? (set (bases Long)) Number) ; => true (contains? (set (bases Long)) Comparable) ; => true (contains? (set (bases clojure.lang.IFn)) java.util.concurrent.Callable) ; => true (bases nil) ; => nil (contains? (set (bases java.lang.Boolean)) Object) ; => true (contains? (set (bases (class :a))) clojure.lang.IFn) ; => true
Supertypes, parents

The parents function allows us to check what the supertypes of a given data type (class or class interface) are.

Usage:

  • (parents class).

The argument should be a class name, and the returned value is a set containing its base class, all interfaces directly implemented by the class, and all direct supertypes based on tags, if such relations have been created. There is also a variant of this function that handles the latter.

If the given type has no supertypes, the returned value is nil.

Example of using the parents function
 1(parents Object)
 2; => nil
 3
 4(contains? (parents String) CharSequence)                 ; => true
 5(contains? (parents Integer) Number)                      ; => true
 6(contains? (parents clojure.lang.Symbol)
 7           clojure.lang.Named)                            ; => true
 8
 9(derive clojure.lang.Associative ::collection)
10(contains? (parents clojure.lang.Associative) ::collection)
11; => true
(parents Object) ; => nil (contains? (parents String) CharSequence) ; => true (contains? (parents Integer) Number) ; => true (contains? (parents clojure.lang.Symbol) clojure.lang.Named) ; => true (derive clojure.lang.Associative ::collection) (contains? (parents clojure.lang.Associative) ::collection) ; => true
All supertypes, ancestors

The ancestors function allows us to check what all direct and indirect parent classes of a given class or interface are, as well as all interfaces implemented by it. Tag-based types are also taken into account, if they are supertypes of the given class.

Usage:

  • (ancestors class).

The accepted argument is a class or interface whose supertypes are to be examined.

The function returns a set containing all direct and indirect superclasses, all implemented interfaces, and tag-based supertypes of the given class. There is also a variant of this function that operates on ad-hoc tag-based types.

If the given class has no superclasses, the returned value is nil.

Example of using the ancestors function
1(ancestors Object)
2; => nil
3
4(contains? (ancestors String) CharSequence) ; => true
5(contains? (ancestors Integer) Object)      ; => true
6
7(derive clojure.lang.Associative ::collection)
8(contains? (ancestors clojure.lang.Associative) ::collection)
9; => true
(ancestors Object) ; => nil (contains? (ancestors String) CharSequence) ; => true (contains? (ancestors Integer) Object) ; => true (derive clojure.lang.Associative ::collection) (contains? (ancestors clojure.lang.Associative) ::collection) ; => true

Primitive Type System

Object types are not the only family of types available in Clojure. In some cases, we can also use so-called primitive types. They also originate from the host platform.

Primitive data types are types that are built into the language and serve as fundamental units from which composite types can be created.

For example, an object may have attributes that are not objects but rather primitive types, not defined in any classes.

The JVM primitive types relevant to Clojure interop are:

  • boolean – logical values true and false (storage size is context-dependent);
  •    byte – bytes (8-bit, range: -128 to 127);
  •    char – UTF-16 code units (16-bit, range: 0 to 65535);
  •   short – short integers (16-bit, range: -32768 to 32767);
  •     int – integers (32-bit, range: -231 to 231-1);
  •    long – long integers (64-bit, range: -263 to 263-1);
  •   float – floating-point numbers (32-bit, IEEE 754 compliant);
  •  double – double-precision floating-point numbers (64-bit, IEEE 754).

Boxed Types

Primitive values crossing an object-only boundary are represented by wrapper objects; this conversion is called boxing and may be inserted automatically. In primitive locals, arithmetic, arrays, and host calls the compiler may keep values unboxed.

Let us see which object types are used for boxing primitive types:

  • booleanjava.lang.Boolean;
  •    bytejava.lang.Byte;
  •    charjava.lang.Character;
  •   shortjava.lang.Short;
  •     intjava.lang.Integer;
  •    longjava.lang.Long;
  •   floatjava.lang.Float;
  •  doublejava.lang.Double.

At an object boundary such as a REPL result, the literal 123 appears as a java.lang.Long. During compiled arithmetic the same value may travel through primitive long locals and instructions. Source syntax alone does not force one representation at every point.

Boxing lets values cross object-oriented APIs and enter ordinary persistent collections. Wrapper classes are normal Java classes but are generally final, so boxing does not imply that applications can subclass them. Allocation, indirection, and memory overhead may matter in hot numeric paths.

Under certain conditions Clojure can use primitive data directly. This is useful in tight numerical loops and primitive-array operations. Boxing can allocate wrapper objects, although caches, escape analysis, and JIT optimization mean that one source iteration does not guarantee one heap allocation.

In addition to using boxed types, we can obtain direct access to unboxed types. These are primitive types whose data, although often boxed at generic object boundaries, can be used directly in selected contexts. Primitive paths avoid object methods and may reduce boxing overhead, but their performance effect should be measured in the actual call path.

Clojure’s compiler supports primitive paths in selected contexts. Coercion functions can establish primitive locals, while type hinting helps select primitive signatures and host calls. Hints are code-generation information, not runtime assertions.

Primitive type support is available in many contexts, including:

Ad-hoc Type System

The Clojure type system is flexible and extensible. In addition to the runtime class of each non-nil JVM value, we can attach application-level type tags using the :type metadata key on values that support metadata.

Additional, platform-independent types can be used to identify data structures that are meaningful in the context of the adopted application logic, but also when creating certain polymorphic operations, which will be discussed later.

Hierarchical Nature

A type system built on custom tags can be called a tag type system or an ad-hoc hierarchy system. It is hierarchical, meaning it can create relationships between types, thus distinguishing supertypes and subtypes. We can create multiple different hierarchies or use the default global hierarchy.

According to the adopted nomenclature, the direct supertype of a given tag type will also be called:

  • a parent,
  • a parent type,
  • a base type,
  • a direct ancestor.

In turn, a direct subtype will be called:

  • a child,
  • a child type,
  • a derived type,
  • a direct descendant.

We will also consider all descendant types, called descendants or derived types, to be subtypes, and all ancestral types, called ancestors or base types, to be supertypes.

Clojure Support

Tagging Values with Types

Usage:

  •  ^{:type tag} value,
  • '^{:type tag} value.

Type tags are commonly namespace-qualified keywords or symbols, especially when they participate in the global hierarchy. The tagged values must support metadata, for example symbols, Clojure collections, Vars, or selected reference objects.

Example of tagging a data type
1(def person
2  ^{:type ::Human} {:name      "Paweł"
3                    :last-name "Wilk"
4                    :gender    :m})
5
6(type person)
7; => :user/Human
(def person ^{:type ::Human} {:name "Paweł" :last-name "Wilk" :gender :m}) (type person) ; => :user/Human
Creating Hierarchies, make-hierarchy

The make-hierarchy function creates custom type hierarchies based on tags.

Usage:

  • (make-hierarchy).

The function takes no arguments, and the returned value is a map in which information about hierarchical relationships of tag-based types will be stored.

Example of using the make-hierarchy function
(def h (make-hierarchy))
(def h (make-hierarchy))
Type Derivation, derive

The derive function allows establishing an ancestor-descendant relationship between two types in a given tag type hierarchy.

Hierarchies can relate custom tags to one another and can also derive host classes from custom tags. A multimethod can therefore treat several otherwise unrelated host classes as members of one application-level category.

Usage:

  • (derive           type ancestor),
  • (derive hierarchy type ancestor).

In the basic variant, the function takes two arguments. The first should be a type expressed as a symbol (with a qualified namespace), a keyword (with a qualified namespace), or a class. The value of the second argument should be a type that is to be its supertype (its parent type), expressed as a symbol or keyword. The change will be applied to the global tag type hierarchy, and the returned value will be nil.

In the three-argument version, the first argument should be a hierarchy map, and the call will not cause a side effect of modifying it but will return an updated object.

Examples of using the derive function
 1;; custom hierarchy:
 2
 3(def h
 4  (-> (make-hierarchy)
 5      (derive ::cat    ::animal)
 6      (derive ::dog    ::animal)
 7      (derive ::collie ::dog)
 8      (derive ::poodle ::dog)
 9      (derive clojure.lang.Associative ::collie)
10      (derive clojure.lang.Associative ::poodle)))
11
12h
13; => {:parents
14; =>  {:user/cat      #{:user/animal},
15; =>   :user/dog      #{:user/animal},
16; =>   :user/collie   #{:user/dog},
17; =>   :user/poodle   #{:user/dog},
18; =>   clojure.lang.Associative #{:user/poodle :user/collie}},
19; =>  :ancestors
20; =>  {:user/cat      #{:user/animal},
21; =>   :user/dog      #{:user/animal},
22; =>   :user/collie   #{:user/dog :user/animal},
23; =>   :user/poodle   #{:user/dog :user/animal},
24; =>   clojure.lang.Associative #{:user/poodle :user/collie :user/dog :user/animal}},
25; =>  :descendants
26; =>  {:user/animal #{:user/dog :user/collie :user/cat :user/poodle clojure.lang.Associative},
27; =>   :user/dog    #{:user/collie :user/poodle clojure.lang.Associative},
28; =>   :user/collie #{clojure.lang.Associative},
29; =>   :user/poodle #{clojure.lang.Associative}}}
30
31;; global hierarchy:
32
33(derive ::cat    ::animal)
34(derive ::dog    ::animal)
35(derive ::collie ::dog)
36(derive ::poodle ::dog)
37(derive clojure.lang.Associative ::collie)
38(derive clojure.lang.Associative ::poodle)
39; => nil
;; custom hierarchy: (def h (-> (make-hierarchy) (derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (derive clojure.lang.Associative ::collie) (derive clojure.lang.Associative ::poodle))) h ; => {:parents ; => {:user/cat #{:user/animal}, ; => :user/dog #{:user/animal}, ; => :user/collie #{:user/dog}, ; => :user/poodle #{:user/dog}, ; => clojure.lang.Associative #{:user/poodle :user/collie}}, ; => :ancestors ; => {:user/cat #{:user/animal}, ; => :user/dog #{:user/animal}, ; => :user/collie #{:user/dog :user/animal}, ; => :user/poodle #{:user/dog :user/animal}, ; => clojure.lang.Associative #{:user/poodle :user/collie :user/dog :user/animal}}, ; => :descendants ; => {:user/animal #{:user/dog :user/collie :user/cat :user/poodle clojure.lang.Associative}, ; => :user/dog #{:user/collie :user/poodle clojure.lang.Associative}, ; => :user/collie #{clojure.lang.Associative}, ; => :user/poodle #{clojure.lang.Associative}}} ;; global hierarchy: (derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (derive clojure.lang.Associative ::collie) (derive clojure.lang.Associative ::poodle) ; => nil
Removing Type Derivations, underive

The underive function removes a specified ancestor-descendant relationship between two types in a given tag type hierarchy.

Usage:

  • (underive           type ancestor),
  • (underive hierarchy type ancestor).

In the basic variant, the function takes two arguments. The first should be a type expressed as a symbol (with a qualified namespace), a keyword (with a qualified namespace), or a class. The value of the second argument should be a type that is its supertype, expressed as a symbol or keyword. The change will be applied to the global tag type hierarchy, and the returned value will be nil.

In the three-argument version, the first argument should be a hierarchy map, and the call will not cause a side effect of modifying it but will return an updated object.

Examples of using the underive function
 1;; custom hierarchy:
 2
 3(def h
 4  (-> (make-hierarchy)
 5      (derive ::cat  ::animal)
 6      (derive ::dog ::animal)))
 7
 8h
 9; => {:parents     {:user/cat #{:user/animal}, :user/dog #{:user/animal}},
10; =>  :ancestors   {:user/cat #{:user/animal}, :user/dog #{:user/animal}},
11; =>  :descendants {:user/animal #{:user/dog :user/cat}}}
12
13(alter-var-root (var h) underive ::cat ::animal)
14; => {:ancestors   {:user/dog #{:user/animal}}
15; =>  :descendants {:user/animal #{:user/dog}}
16; =>  :parents     {:user/dog #{:user/animal}}}
17
18;; global hierarchy:
19
20(derive   ::cat ::animal)
21(derive   ::dog ::animal)
22(underive ::cat ::animal)
23; => nil
24
25@#'clojure.core/global-hierarchy
26; => {:ancestors   {:user/dog #{:user/animal}}
27; =>  :descendants {:user/animal #{:user/dog}}
28; =>  :parents     {:user/dog #{:user/animal}}}
;; custom hierarchy: (def h (-> (make-hierarchy) (derive ::cat ::animal) (derive ::dog ::animal))) h ; => {:parents {:user/cat #{:user/animal}, :user/dog #{:user/animal}}, ; => :ancestors {:user/cat #{:user/animal}, :user/dog #{:user/animal}}, ; => :descendants {:user/animal #{:user/dog :user/cat}}} (alter-var-root (var h) underive ::cat ::animal) ; => {:ancestors {:user/dog #{:user/animal}} ; => :descendants {:user/animal #{:user/dog}} ; => :parents {:user/dog #{:user/animal}}} ;; global hierarchy: (derive ::cat ::animal) (derive ::dog ::animal) (underive ::cat ::animal) ; => nil @#'clojure.core/global-hierarchy ; => {:ancestors {:user/dog #{:user/animal}} ; => :descendants {:user/animal #{:user/dog}} ; => :parents {:user/dog #{:user/animal}}}
Inheritance Predicate, isa?

The isa? function checks whether a given type (tag-based or object-based) is a direct or indirect subtype of another given type.

Usage:

  • (isa?           descendant parent),
  • (isa? hierarchy descendant parent),

The first accepted argument is a descendant type tag or a host system object type (class), and the second is a parent type tag.

In the three-argument variant, the first argument should be a map defining the type hierarchy.

The returned value will be true if the given derived type is a direct or indirect descendant of the parent type. Otherwise, the returned value will be false.

Example of using the isa? function
 1(derive ::cat    ::animal)
 2(derive ::dog    ::animal)
 3(derive ::collie ::dog)
 4(derive ::poodle ::dog)
 5
 6(isa? ::cat    ::animal)  ; => true
 7(isa? ::poodle ::dog)     ; => true
 8(isa? ::cat    ::dog)     ; => false
 9(isa? ::collie ::cat)     ; => false
10
11(derive clojure.lang.Associative ::collection)
12(isa?   clojure.lang.Associative ::collection)
13; => true
(derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (isa? ::cat ::animal) ; => true (isa? ::poodle ::dog) ; => true (isa? ::cat ::dog) ; => false (isa? ::collie ::cat) ; => false (derive clojure.lang.Associative ::collection) (isa? clojure.lang.Associative ::collection) ; => true

The last two lines of the example show a use case in which the ad-hoc type ::collection is marked as a supertype of the object type clojure.lang.Associative. In this way, we are able to group object types and check their membership.

Note that the function can also be used in the object variant.

Supertypes, parents

The parents function reports the direct supertypes of a given data type are.

Usage:

  • (parents           type),
  • (parents hierarchy type).

In the single-argument variant, a type tag should be passed, and in the two-argument variant, a hierarchy defined as a map (created using make-hierarchy) and a type tag.

The function returns a set containing all direct supertypes of the given type. Object types are also supported: the function returns base classes and directly implemented interfaces of the given class, as well as tag-based types that are in a parent relationship with the given host system object types.

If the given type has no supertypes, the returned value is nil.

Example of using the parents function
 1(derive ::cat    ::animal)
 2(derive ::dog    ::animal)
 3(derive ::collie ::dog)
 4(derive ::poodle ::dog)
 5(derive clojure.lang.Associative ::collection)
 6
 7(parents ::cat)         ; => #{:user/animal}
 8(parents ::collie)      ; => #{:user/dog}
 9(parents ::collection)  ; => nil
10
11(parents clojure.lang.Associative)
12; => #{:user/collection
13; =>   clojure.lang.ILookup
14; =>   clojure.lang.IPersistentCollection}
(derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (derive clojure.lang.Associative ::collection) (parents ::cat) ; => #{:user/animal} (parents ::collie) ; => #{:user/dog} (parents ::collection) ; => nil (parents clojure.lang.Associative) ; => #{:user/collection ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection}
All Subtypes, descendants

The descendants function reports the direct and indirect subtypes of a type identified by a given tag are.

Usage:

  • (descendants           type),
  • (descendants hierarchy type).

The argument should be a tag identifying the examined type, and in the two-argument variant, a hierarchy map (created using make-hierarchy) and the examined type’s tag.

The function returns a set containing all direct and indirect descendant types relative to the given tag-based type.

Example of using the descendants function
 1(derive ::cat    ::animal)
 2(derive ::dog    ::animal)
 3(derive ::collie ::dog)
 4(derive ::poodle ::dog)
 5(derive clojure.lang.Associative ::collection)
 6
 7(descendants ::cat)         ; => nil
 8(descendants ::collie)      ; => nil
 9(descendants ::dog)         ; => #{:user/collie :user/poodle}
10(descendants ::collection)  ; => #{clojure.lang.Associative}
(derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (derive clojure.lang.Associative ::collection) (descendants ::cat) ; => nil (descendants ::collie) ; => nil (descendants ::dog) ; => #{:user/collie :user/poodle} (descendants ::collection) ; => #{clojure.lang.Associative}
All Supertypes, ancestors

The ancestors function reports all direct and indirect supertypes of a given data type are.

Usage:

  • (ancestors           type),
  • (ancestors hierarchy type).

In the single-argument variant, a type tag should be passed, and in the two-argument variant, a hierarchy defined as a map (created using make-hierarchy) and a type tag.

The function returns a set containing all direct and indirect supertypes of the given type. Object types are also supported: the function returns superclasses and all implemented interfaces of the given class, as well as tag-based types that are in a parent relationship with the given host system object types.

If the given type has no supertypes, the returned value is nil.

Example of using the ancestors function
 1(derive ::cat    ::animal)
 2(derive ::dog    ::animal)
 3(derive ::collie ::dog)
 4(derive ::poodle ::dog)
 5(derive clojure.lang.Associative ::collection)
 6
 7(ancestors ::cat)         ; => #{:user/animal}
 8(ancestors ::collie)      ; => #{:user/dog :user/animal}
 9(ancestors ::collection)  ; => nil
10
11(ancestors clojure.lang.Associative)
12; => #{:user/collection
13; =>   clojure.lang.ILookup
14; =>   clojure.lang.IPersistentCollection
15; =>   clojure.lang.Seqable}
(derive ::cat ::animal) (derive ::dog ::animal) (derive ::collie ::dog) (derive ::poodle ::dog) (derive clojure.lang.Associative ::collection) (ancestors ::cat) ; => #{:user/animal} (ancestors ::collie) ; => #{:user/dog :user/animal} (ancestors ::collection) ; => nil (ancestors clojure.lang.Associative) ; => #{:user/collection ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection ; => clojure.lang.Seqable}

Generic Operations

There are generic operations in Clojure that are common to all type systems. Some of them even allow mixing types from different families (e.g. tag- based and object-based).

Type Checking

Examining the Type of a Value, type

The type function allows examining what type of value we are dealing with. It works for ad-hoc and object types.

Usage:

  • (type value).

The argument of the function call should be any value, and the returned value will be its type.

The type will be read from the value’s metadata (the :type key), and if that fails, the name of the class of which the object is an instance will be returned.

Examples of using the type function
 1(type 1)        ; => java.lang.Long
 2(type "abc")    ; => java.lang.String
 3(type \a)       ; => java.lang.Character
 4(type nil)      ; => nil
 5(type true)     ; => java.lang.Boolean
 6(type false)    ; => java.lang.Boolean
 7(type (fn []))  ; => an implementation-generated function class
 8(type [])       ; => clojure.lang.PersistentVector
 9(type :a)       ; => clojure.lang.Keyword
10(type 'a)       ; => clojure.lang.Symbol
11
12(type '^{:type ::Something} a)
13; => :user/Something
(type 1) ; => java.lang.Long (type "abc") ; => java.lang.String (type \a) ; => java.lang.Character (type nil) ; => nil (type true) ; => java.lang.Boolean (type false) ; => java.lang.Boolean (type (fn [])) ; => an implementation-generated function class (type []) ; => clojure.lang.PersistentVector (type :a) ; => clojure.lang.Keyword (type 'a) ; => clojure.lang.Symbol (type '^{:type ::Something} a) ; => :user/Something

We can see that some data types come directly from Java, while others are hierarchical types specific to Clojure. In the last expression, we assigned a custom type tag to the symbol a. Note the difference in the returned value of this expression compared to the example call of class.

Polymorphic Operations

Conversion, Coercion, and Casting

Certain operations on data types can be considered simple polymorphic mechanisms, as they allow treating values of one type as if they were values of another. These operations are:

  • conversion – creating a value of a new type based on a value of another type;
  • casting – treating a value as if it were a value of another type;
  • coercion – converting the type of a value passed as a function argument.

It is worth noting at the outset that in practice some of these terms are used interchangeably due to imprecise naming conventions and differences in compiler implementation details.

Conversion, coercion, and casting are operations of the object and primitive type systems of the host platform.

Object Type Polymorphism

In Clojure, we can create new object types, using the mechanism of so-called records and type definitions. Records and custom types can then be matched to appropriate operations according to rules defined by so-called protocols.

Ad-hoc Type Polymorphism

Hierarchical ad-hoc types based on tags can be created and then used in conjunction with the multimethod mechanism to construct polymorphic operations based on them (type) or on relationships between them (isa?).

Built-in Data Types

Built-in Object Types

Simple Types
Simple JVM Types
Reference Types
Function Types
  • clojure.lang.IFn – invocation interface implemented by Clojure function values and several callable data types.
Repetition Types
Exception Types
  • clojure.lang.ExceptionInfo – exception information.
Conditional Types
Stream Types
Range Types
Sequential Types
  • clojure.lang.Cons – cons cells,
  • clojure.lang.ChunkedCons – cons cells of chunked lists,
  • clojure.lang.Cycle – cyclic sequences,
  • clojure.lang.IndexedSeq – indexed sequences,
  • clojure.lang.LazySeq – lazy sequences,
  • clojure.lang.SeqEnumeration – enumeration sequences,
  • clojure.lang.SeqIterator – iterator sequences,
  • clojure.lang.StringSeq – a sequence over UTF-16 code units in a string.
Collection Types
Transactional Types
Literal Types

Built-in Primitive Types

Simple Types

  • boolean – logical value,
  • byte    – byte,
  • char    – character,
  • int     – integer,
  • short   – short integer,
  • long    – long integer,
  • float   – floating-point number,
  • double  – double-precision number.
Array Types

  • booleans – arrays of logical values,
  • bytes    – byte arrays,
  • chars    – character arrays,
  • ints     – integer arrays,
  • shorts   – short integer arrays,
  • longs    – long integer arrays,
  • floats   – floating-point number arrays,
  • doubles  – double-precision number arrays.
Array Predicates

The Clojure standard library provides only the bytes? array predicate:

  • (bytes? value) – checks whether the value is a byte array.

The remaining array predicates (booleans?, chars?, ints?, shorts?, longs?, floats?, doubles?) do not exist in the language core.

Current section: Read Me Clojure
Categories:

Taxonomies: