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.

In theory, data types unambiguously determine what operations we can perform on values. In practice, using types for this kind of verification can be complicated and ineffective, particularly when testing complex business-application data. Therefore, in addition to types – which in dynamic languages assist the compiler or interpreter more than the programmer – additional mechanisms for testing and validating data are also employed.

Types provide the compiler or interpreter with information about how to manage memory space and automatic conversions from data of one kind to data of another. Depending on the programming language, knowledge about the types used in a program will be more or less explicit. In languages with so-called strong typing, we are required to annotate data with types so that the compiler can catch discrepancies between our declarations and actual usage; for example, passing a number to a function that requires a character string would be a compilation error.

One application of knowledge about data types can be polymorphic code optimization, which involves generating multiple versions of the same subroutine depending on the kinds of accepted input objects. For instance, the argument of a function being created might be an integer or a character string. During compilation, two variants of the function object will then be produced depending on the type of accepted input. An appropriate dispatch function will also be generated, whose job will be to invoke the subroutine matching the type. Thanks to this, the compiler will be able to apply optimizations related to the specific kind of processed values in each variant of the function (e.g., unlike character strings, numeric data need not be rigorously checked for the number of elements or terminating symbols).

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.

We can distinguish programming languages that are weakly typed and strongly typed. The latter perform data type checking when passing values to subroutines (e.g., functions) or assigning them to variables, whereas the former do not impose such restrictions – but the programmer must be more vigilant to avoid performing operations on data whose kinds (types) cannot be correctly handled by those operations. If the programmer fails to take care of this, defects will occur during program execution, since incompatibilities will not be detected at the compilation stage.

In Clojure, we deal with dynamic typing, that is, dynamic type-checking. This means that checking data types and the compatibility of in-memory objects in this regard takes place at program runtime, not at the compilation stage.

The Clojure language also employs implicit typing, which means that the programmer does not have to declare the data types in use. This is possible, among other things, thanks to type inference and the language being equipped with generic constructs that can operate on data of any kind (particularly in the case of collections and sequences).

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. Thanks to it, we can 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 a implicitly and dynamically typed language, although some optimizations may make use of JVM’s static typing, and selected constructs (e.g., function arguments and return values, or certain value classes) can be annotated with types, even though this is not mandatory and does not enable strong typing.

The type system of the Clojure language consists of three subsystems:

  • the object type system of the host platform,
  • the primitive type system of the host platform,
  • an ad-hoc, hierarchical type system.

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 type, will be called an instance or an exemplar of a certain class.

Objects consist of variables in which their data is stored, as well as references to functions defined in classes (called methods) that serve to operate on the variables.

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 whose instances the class exemplars (objects) store data;

  • definitions of member functions, also called methods,
    which are operations that can be performed on the exemplar’s data.

A class resembles the C-language construct known as a structure, but in addition to fields, it also has functions that allow operating on them. An exemplar of a class is – continuing the analogy – a variable whose type is defined by a specific structure. In practice, object-oriented programming mechanisms can be implemented in C, but the source code of such programs would be longer than their counterparts in languages with built-in support for classes and objects.

Inheritance

The way to add new methods or fields to existing classes is inheritance, that is, creating derived classes. 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 of them, because interfaces, unlike base classes, do not define methods – they only announce the necessity of their presence.

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 every method declared in an interface must be defined in the class. Furthermore, classes can later be extended to support new interfaces. This is one of the mechanisms of dynamic polymorphism.

Example of an interface in Java
interface ICountable {
  public Long count-it();
}

class Napis implements ICountable {
  String value;
  public Long count-it() {
    return(new Long(value.length()));
  }
}
interface ICountable { public Long count-it(); } class Napis implements ICountable { String value; public Long count-it() { return(new Long(value.length())); } }

If we, for example, created an interface ICountable, in which a method count-it 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 count-it method. In the case of a class responsible for representing character strings, it could calculate the number of characters composing 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, we will usually not need to deal directly with host system interfaces, unless we wish to perform optimizations using polymorphic operations.

Defining Interfaces, definterface

In Clojure, we can dynamically create new Java interfaces. Definitions may contain method signatures that will have to be defined in every class implementing the interface.

It is worth noting that in Clojure, creating interfaces directly is not recommended, because there is a mechanism called protocols that replaces and enriches them.

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 [& argument...]) or
  • (^type method-name [& argument...]).

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

Generating Interfaces, gen-interface

The gen-interface macro serves for low-level generation of Java interfaces. It causes JVM bytecode for an interface with the given name to be generated in memory; the name should be a fully qualified Java package name. Additionally, if the program is being compiled (in AOT mode), a file with the class extension will be placed in the filesystem directory whose name is stored in the dynamic variable *compile-path*.

Usage:

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

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

Protocols

The Clojure equivalent of Java interfaces are so-called protocols, described further on. They are based on interfaces but contain additional data structures and handling mechanisms typical of Clojure.

The only situations where we would choose interfaces over protocols are those where, for performance reasons, we decide that the declared functions should accept as arguments or return as values data of primitive types.

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 solves 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 certain abstract constructs thanks to which the fact that host system classes are being generated is hidden from us, 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 allows defining object data types. Its use causes a class defining a data type to be dynamically generated, compiled to bytecode, along with a Java package containing it, named the same as the current namespace.

During the full compilation phase of the program (if it takes place), the class name with the package name prepended with a dot will form a filename, to which the class extension will be added. The class definition will be written to the file, which will be placed in the directory with the pathname specified by the special dynamic variable *compile-path* (default: classes).

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.

New fields and their values can be dynamically added to instances of the created type. Fields of this kind are called extension fields.

Usage:

  • (deftype name field... & option... & 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 marked as a volatile mutable object (Java’s volatile modifier);

  • :unsynchronized-mutable
    – setting the value to true (or using :unsynchronized-mutable) will cause the field to be marked as a mutable object.

Field values of defined types are – like other values in Clojure – immutable, but applying the above metadata can change this. They should be used only when there is a genuine need and we can handle potential edge cases, e.g., those related to concurrency handling.

Note: Field names cannot be __meta or __extmap – these are reserved symbols used for internal handling of metadata and extension fields.

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 should consist of the name of a protocol or host platform interface, after which one or more method definitions may (but do not have to) appear. 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.

Note: One should not create instances of types by directly invoking the object’s constructor. It is better to use the Clojure language function designed for this purpose: ->type.

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 Paweł (Person. "Paweł" "Wilk" 18))
 7
 8;; creating an instance of the Person class by invoking the -> function
 9
10(def Paweł (->Person "Paweł" "Wilk" 18))
11
12;; checking types
13
14(type Person)       ; => java.lang.Class
15(type Paweł)        ; => user.Person
16
17;; invoking field accessors
18
19(.name     Paweł)   ; => "Paweł"
20(.last-name Paweł)  ; => "Wilk"
(deftype Person [name last-name age]) ; => user.Person ;; creating an instance of the Person class by invoking the constructor (def Paweł (Person. "Paweł" "Wilk" 18)) ;; creating an instance of the Person class by invoking the -> function (def Paweł (->Person "Paweł" "Wilk" 18)) ;; checking types (type Person) ; => java.lang.Class (type Paweł) ; => user.Person ;; invoking field accessors (.name Paweł) ; => "Paweł" (.last-name Paweł) ; => "Wilk"
Creating Objects, ->type

We can (and should!) create objects of self-defined types not only directly, by referring to the constructor produced as a result of calling deftype, but also by using the ->type function (where type is the name of the type) generated during new type definition.

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(->Person "Paweł" "Wilk" 18)
4; => #object[user.Person 0x53dda21f "user.Person@53dda21f"]
(deftype Person [name last-name age]) (->Person "Paweł" "Wilk" 18) ; => #object[user.Person 0x53dda21f "user.Person@53dda21f"]
Defining Records, defrecord

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

Record type instances (called records) can be accessed using functions that operate on maps. Each field declared in the class is exposed in the record instance as a key of a virtual map.

New fields along with their values can also be dynamically added to objects created based on the defined type, even if they were not declared at the time of type creation. Fields of this kind are called extension fields.

Usage:

  • (defrecord name field... & option... & 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 is used so that bytecode originating from Clojure can be integrated with Java libraries that will want to reference or extend such exposed classes or their instances.

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 the function that will become the constructor;

  • :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 static factory functions 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 implementation of the class’s static initializer will be looked for in a function from the same namespace.

It is worth remembering that the new class will not be equipped with methods, which must be defined using functions in one namespace of the program. They will be dynamically invoked when references to class methods with the same names (with the established prefix or the default prefix -) appear.

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
10;; creating method implementations
11
12(defn -human-Wita [this]
13  (str "Hejka " this))
14
15(defn -human-main [kto]
16  (-human-Wita kto))
(ns pl.randomseed.Human) ;; creating a Java class (gen-class :name pl.randomseed.Human :main true :prefix -human-) ;; creating method implementations (defn -human-Wita [this] (str "Hejka " this)) (defn -human-main [kto] (-human-Wita kto))

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
9clj -e "(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 clj -e "(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, if in our example we tried to perform some arithmetic operation on the value of x, a Java UnsupportedOperationException would be thrown, because only the methods we defined will be exposed in the proxy object.

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 allows us to perform reification, that is, making concrete Java interfaces and Clojure protocols. It is the younger sibling of the proxy macro and a more recommended construct compared to it.

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 are actual methods of the anonymous class, not dynamic references to function objects outside the class. As a result, method invocations are faster – there is no need for dispatching using a special map that associates method names with function objects.

  • 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

When we use some value in Clojure, it will always have an assigned object data type, originating from the host platform. This type will be mapped as classes from the java namespace (most often java.lang in the case of JVM) or added by classes defined in the clojure.lang namespace.

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 []))  ; => user$eval10652$fn__10653
 8(class [])       ; => clojure.lang.PersistentVector
 9(class :a)       ; => clojure.lang.Keyword
10(class 'a)       ; => clojure.lang.Symbol
11
12(class '^{:type ::Coś} 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 [])) ; => user$eval10652$fn__10653 (class []) ; => clojure.lang.PersistentVector (class :a) ; => clojure.lang.Keyword (class 'a) ; => clojure.lang.Symbol (class '^{:type ::Coś} 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

Note that the name of this function can be somewhat misleading, because we are not dealing with casting – this is clearly visible in the last expression of the example.

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(supers Long)
 2; => #{java.io.Serializable
 3; =>   java.lang.Comparable
 4; =>   java.lang.Number
 5; =>   java.lang.Object}
 6
 7(supers clojure.lang.IFn)
 8; => #{java.lang.Runnable
 9; =>   java.util.concurrent.Callable}
10
11(supers nil)
12; => nil
13
14(supers java.lang.Boolean)
15; => #{java.io.Serializable
16; =>   java.lang.Comparable
17; =>   java.lang.Object}
18
19(supers (class :a))
20; => #{clojure.lang.IFn
21; =>   clojure.lang.IHashEq
22; =>   clojure.lang.Named
23; =>   java.io.Serializable
24; =>   java.lang.Comparable
25; =>   java.lang.Object
26; =>   java.lang.Runnable
27; =>   java.util.concurrent.Callable}
(supers Long) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Number ; => java.lang.Object} (supers clojure.lang.IFn) ; => #{java.lang.Runnable ; => java.util.concurrent.Callable} (supers nil) ; => nil (supers java.lang.Boolean) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object} (supers (class :a)) ; => #{clojure.lang.IFn ; => clojure.lang.IHashEq ; => clojure.lang.Named ; => java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object ; => java.lang.Runnable ; => java.util.concurrent.Callable}
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(bases Long)
 2; => (java.lang.Comparable
 3; =>  java.lang.Number)
 4
 5(bases clojure.lang.IFn)
 6; => (java.lang.Runnable
 7; =>  java.util.concurrent.Callable)
 8
 9(bases nil)
10; => nil
11
12(bases java.lang.Boolean)
13; => (java.io.Serializable
14; =>  java.lang.Comparable
15; =>  java.lang.Object)
16
17(bases (class :a))
18; => (java.lang.Object
19; =>  clojure.lang.IFn
20; =>  clojure.lang.IHashEq
21; =>  clojure.lang.Named
22; =>  java.io.Serializable
23; =>  java.lang.Comparable)
(bases Long) ; => (java.lang.Comparable ; => java.lang.Number) (bases clojure.lang.IFn) ; => (java.lang.Runnable ; => java.util.concurrent.Callable) (bases nil) ; => nil (bases java.lang.Boolean) ; => (java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object) (bases (class :a)) ; => (java.lang.Object ; => clojure.lang.IFn ; => clojure.lang.IHashEq ; => clojure.lang.Named ; => java.io.Serializable ; => java.lang.Comparable)
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(parents String)
 5; => #{java.io.Serializable
 6; =>   java.lang.CharSequence
 7; =>   java.lang.Comparable
 8; =>   java.lang.Object}
 9
10(parents Integer)
11; => #{java.lang.Comparable
12; =>   java.lang.Number}
13
14(parents clojure.lang.Symbol)
15; => #{class clojure.lang.AFn
16; =>   interface clojure.lang.IHashEq
17; =>   interface clojure.lang.IObj
18; =>   interface clojure.lang.Named
19; =>   interface java.io.Serializable
20; =>   interface java.lang.Comparable}
21
22(derive clojure.lang.Associative ::collection)
23(parents clojure.lang.Associative)
24; => #{:user/collection
25; =>   clojure.lang.ILookup
26; =>   clojure.lang.IPersistentCollection}
(parents Object) ; => nil (parents String) ; => #{java.io.Serializable ; => java.lang.CharSequence ; => java.lang.Comparable ; => java.lang.Object} (parents Integer) ; => #{java.lang.Comparable ; => java.lang.Number} (parents clojure.lang.Symbol) ; => #{class clojure.lang.AFn ; => interface clojure.lang.IHashEq ; => interface clojure.lang.IObj ; => interface clojure.lang.Named ; => interface java.io.Serializable ; => interface java.lang.Comparable} (derive clojure.lang.Associative ::collection) (parents clojure.lang.Associative) ; => #{:user/collection ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection}
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(ancestors String)
 5; => #{java.io.Serializable
 6; =>   java.lang.CharSequence
 7; =>   java.lang.Comparable
 8; =>   java.lang.Object}
 9
10(ancestors Integer)
11; => #{java.io.Serializable
12; =>   java.lang.Comparable
13; =>   java.lang.Number
14; =>   java.lang.Object}
15
16(derive clojure.lang.Associative ::collection)
17(ancestors clojure.lang.Associative)
18; => #{:user/collection
19; =>   clojure.lang.ILookup
20; =>   clojure.lang.IPersistentCollection
21; =>   clojure.lang.Seqable}
(ancestors Object) ; => nil (ancestors String) ; => #{java.io.Serializable ; => java.lang.CharSequence ; => java.lang.Comparable ; => java.lang.Object} (ancestors Integer) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Number ; => java.lang.Object} (derive clojure.lang.Associative ::collection) (ancestors clojure.lang.Associative) ; => #{:user/collection ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection ; => clojure.lang.Seqable}

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.

In the case of Clojure and the JVM, the primitive types are:

  • boolean – logical values (8- to 32-bit, values: true or false);
  •    byte – bytes (8-bit, range: -128 to 127);
  •    char – Unicode characters (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

Typically, values of primitive types in Java (and thus in Clojure) are not represented directly but are automatically placed in special objects. Primitive types contained within such objects are called boxed types, and the process of their object-based representation is called autoboxing.

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.

So when we see 123 in a program, we are actually dealing with the type java.lang.Long. Inside an object of this type, there is a field of a primitive type (long) holding the actual value. Additionally, the class java.lang.Long contains member functions, i.e. methods, that allow various operations to be performed on instances.

Thanks to boxing, we can use values of primitive types just like other objects (e.g. invoke certain methods, place them in collections, pass them as arguments, etc.), and also operate on the types themselves (e.g. create derived classes from them, extend the classes that define them, etc.). The downside, however, is performance overhead and increased memory consumption.

Under certain conditions, it is possible to use primitive type data directly, bypassing the boxing mechanism. This is useful, for example, in loops that perform many iterations, each time modifying some numerical value. If instead of an int variable, we used a reference variable pointing to an object of the Integer class, each loop iteration would mean creating a new instance placed on the heap, which would subsequently be destroyed by the garbage collection mechanism.

In addition to using boxed types, we can obtain direct access to unboxed types. These are primitive types whose data, although generally boxed in objects, are used directly in certain situations. We then lose many object- oriented mechanisms, but we gain computation speed and the compiler’s ability to perform optimizations.

In Clojure, primitive types of the host platform are handled using boxing objects, but we can use so-called type hinting or type casting to perform operations on unboxed type values under certain conditions.

Primitive type support is available in many contexts, including:

Ad-hoc Type System

The Clojure type system is flexible and extensible. Besides the fact that every value has a data type assigned by the host platform (e.g., JVM), we can create our own additional type tags using metadata, specifically the :type key – of course, for those structures 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 allows creating 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 tag identifiers should be keywords or symbols (in constant forms) with qualified namespaces. The values, in turn, should be data structures that support metadata (e.g. symbols, collections, global variables, some reference objects).

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

The make-hierarchy function allows creating 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.

In hierarchies, we can create relationships not only between types based on custom tags but also between custom types and built-in types. Built-in types can be subtypes of tag types. Thanks to this, we can, for example, indicate that a created type can be represented by specific data structures.

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/kot      #{:user/animal},
15; =>   :user/dog     #{:user/animal},
16; =>   :user/colie #{:user/dog},
17; =>   :user/pudel    #{:user/dog},
18; =>   clojure.lang.Associative #{:user/pudel :user/colie}},
19; =>  :ancestors
20; =>  {:user/kot      #{:user/animal},
21; =>   :user/dog     #{:user/animal},
22; =>   :user/colie #{:user/dog :user/animal},
23; =>   :user/pudel    #{:user/dog :user/animal},
24; =>   clojure.lang.Associative #{:user/pudel :user/colie :user/dog :user/animal}},
25; =>  :descendants
26; =>  {:user/animal  #{:user/dog :user/colie :user/kot :user/pudel clojure.lang.Associative},
27; =>   :user/dog     #{:user/colie :user/pudel clojure.lang.Associative},
28; =>   :user/colie #{clojure.lang.Associative},
29; =>   :user/pudel    #{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/kot #{:user/animal}, ; => :user/dog #{:user/animal}, ; => :user/colie #{:user/dog}, ; => :user/pudel #{:user/dog}, ; => clojure.lang.Associative #{:user/pudel :user/colie}}, ; => :ancestors ; => {:user/kot #{:user/animal}, ; => :user/dog #{:user/animal}, ; => :user/colie #{:user/dog :user/animal}, ; => :user/pudel #{:user/dog :user/animal}, ; => clojure.lang.Associative #{:user/pudel :user/colie :user/dog :user/animal}}, ; => :descendants ; => {:user/animal #{:user/dog :user/colie :user/kot :user/pudel clojure.lang.Associative}, ; => :user/dog #{:user/colie :user/pudel clojure.lang.Associative}, ; => :user/colie #{clojure.lang.Associative}, ; => :user/pudel #{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 allows removing 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/kot #{:user/animal}, :user/dog #{:user/animal}},
10; =>  :ancestors   {:user/kot #{:user/animal}, :user/dog #{:user/animal}},
11; =>  :descendants {:user/animal #{:user/dog :user/kot}}}
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/kot #{:user/animal}, :user/dog #{:user/animal}}, ; => :ancestors {:user/kot #{:user/animal}, :user/dog #{:user/animal}}, ; => :descendants {:user/animal #{:user/dog :user/kot}}} (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 allows checking 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 allows checking what 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 allows checking what 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/colie :user/pudel}
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/colie :user/pudel} (descendants ::collection) ; => #{clojure.lang.Associative}
All Supertypes, ancestors

The ancestors function allows checking what 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 []))  ; => user$eval10652$fn__10653
 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 [])) ; => user$eval10652$fn__10653 (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
Repetition Types
Exception Types
  • clojure.lang.ExceptionInfo – exception information.
Conditional Types
Stream Types
Range Types
Sequential Types
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: