/ CLOJURE, SPECIFICATIONS, TYPE

Learning Clojure: coping with dynamic typing

My new position requires me to get familiar with the Clojure language. In intend to document what I learn in a series of posts, to serve as my personal reference notes. As a side-effect, I hope it will also be beneficial to others who want to take the same path. There are already a multitude of great tutorials available: hence, each post will focus on a specific theme, that is specific to Clojure considering that most of my experience comes from OOP.

This is the 2nd post in the Learning Clojure focus series.Other posts include:

  1. Decoding Clojure code, getting your feet wet
  2. Learning Clojure: coping with dynamic typing (this post)
  3. Learning Clojure: the arrow and doto macros
  4. Learning Clojure: dynamic dispatch
  5. Learning Clojure: dependent types and contract-based programming
  6. Learning Clojure: comparing with Java streams
  7. Feedback on Learning Clojure: comparing with Java streams
  8. Learning Clojure: transducers

As a newcomer to Clojure, a big issue of mine is the lack of types. This is not specific to Clojure. I miss types in JavaScript, Groovy, Python, etc. While I value dynamic languages ease of use when writing scripts, my bread-and-butter is still to develop regular applications. With that in mind, I prefer to let the compiler catch type-related errors: that means focusing on actual business features, instead of writing tests to catch those errors.

While Clojure doesn’t offer types in the language syntax, its design allows to build similar capabilities via constructs. Even better, there’s an existing library to take care of that, aptly named spec.

spec is available out-of-the-box since Clojure 1.9. Earlier versions require to explicitly add the dependency on the classpath.

Basics

To start using spec, just require the clojure.spec.alpha package in the namespace:

(ns ch.frankel.blog.clojure.spec
  (:require [clojure.spec.alpha :as sparc]))

The next step is to define the expected type of a value, using the def function. It accepts two parameters:

# Name Description

1

k

Symbol name

2

spec-form

Predicate

More valid parameter values are possible, but this is quite enough to start with.

For simple values, this is quite straightforward:

(spec/def ::nil nil?)        (1)
(spec/def ::bool boolean?)   (2)
(spec/def ::string string?)  (3)
1 Defines ::nil as the nil value
2 Defines ::bool as a boolean value
3 Defines ::string as any string value

Keywords are symbolic identifiers that evaluate to themselves. They provide very fast equality tests. Like Symbols, they have names and optional namespaces, both of which are strings. The leading ':' is not part of the namespace or name.

— Clojure documentation
https://clojure.org/reference/data_structures#Keywords

The :: syntax is the shortcut for a qualified keyword, one fully-qualified using the current namespace. For example, ::bool above resolves to :ch.frankel.blog.clojure.spec/bool.

This technique is not limited to simple types. It’s also possible to restrict values to an enumeration:

(spec/def ::direction #{::NORTH ::EAST ::SOUTH ::WEST})

Spec checks

Once a spec has been def’ed, there are two different ways to use it.

  1. The valid? function returns a boolean, depending whether a value conforms (or not) to the spec e.g.:
    Source Conforms? Returns
    (spec/valid? ::nil nil)

    true

    (spec/valid? ::string "f")

    true

    (spec/valid? ::nil "f")

    false

    (spec/valid? ::string nil)

    false

  2. The conform? function returns:
    • the value if conforms to the spec
    • or clojure.spec.alpha/invalid if it does’t
    Source Conforms? Returns
    (spec/conform? ::nil nil)

    nil

    (spec/conform? ::string "f")

    "f"

    (spec/conform? ::nil "f")

    clojure.spec.alpha/invalid

    (spec/conform? ::string nil)

    clojure.spec.alpha/invalid

Custom spec functions

The above code uses out-of-the-box functions e.g. nil? and string?. There are a lot of similar functions. Here’s a sample, taken from clojure.core:

Function Checks wether the parameter is…​

keyword?

a keyword (obviously…​)

symbol?

a symbol (obviously as well)

ident?

a symbol or a keyword

uuid?

a java.util.UUID instance

uri?

a java.net.URI instance

While some use-cases are covered, a lot of specific ones are not. In that case, any function that accepts an argument and returns a boolean can be used.

Here’s a function that checks whether the parameter is a LocalDate, and how it can be used:

(defn local-date?
  "Check if the parameter is a java.time.LocalDate instance"
  [x]
  (instance? LocalDate x))

(spec/def ::date local-date?)

(spec/valid? ::date (LocalDate/of 2009 1 1)) (1)
(spec/valid? ::date "f")                     (2)
1 Evaluate to true
2 Evaluate to false

Spec’ing data structures

Now that we know how to spec simple values, it’s time to spec more complex ones. In Clojure, one common way to model an "entity" is to use a data map.

My favorite example is a Person entity, with the following properties:

  • First name
  • Last name
  • Birthdate

It can be spec’ed using the keys function. Parameters allow to specify which keys are required, and which ones are optional:

(spec/def ::first-name string?)
(spec/def ::last-name string?)
(spec/def ::birthdate local-date?)

(spec/def ::person (spec/keys :req [::first-name ::last-name]  (1)
                              :opt [::birthdate]))             (2)
1 Required values
2 Optional value

Here are some samples, and some associated validity checks:

Source Returns Rationale
(spec/valid? ::person {
    ::first-name "John"
    ::last-name "Doe"
    ::birthdate (LocalDate/of 1970 1 1)})

true

(spec/valid? ::person "f")

false

string is not a map

(spec/valid? ::person {
    ::last-name "Doe"
    ::birthdate (LocalDate/of 1970 1 1)})

false

map doesn’t contain a value under the ::first-name key

(spec/valid? ::person {
    ::first-name "John"
    ::last-name "Doe"})

true

birthdate is not required

(spec/valid? ::person {
    ::first-name "John"
    ::last-name "Doe"
    ::birthdate "Unknown"})

false

birthdate is not a LocalDate

(spec/valid? ::person {
    ::first-name "John"
    ::last-name "Doe"
    ::birthdate (LocalDate/of 1970 1 1)
    ::title "Mr"})

true

Additional entries are fine

Spec’ing collections

The next step is to use spec to validate the type of elements in a collection, just like with generics in Java e.g List<T>, Map<T> or Set<T>. This is achieved with the help of additional functions:

  • coll-of for "standard" Clojure collection, vector or list, etc.
  • map-of for maps

For example, to spec a list of LocalDate is quite straightforward:

(spec/def ::dates (spec/coll-of ::date))

Likewise, for a map of keyword / LocalDate:

(spec/def ::map-dates (spec/map-of keyword? ::date))

Of course, it works also with data structures:

(spec/def ::map-persons (spec/map-of keyword? ::person))

Conclusion

While Clojure is a dynamically-typed language, it’s possible to supplement types by using the spec library. It allows to validate simple types, enumerations, maps and collections, just as with any statically-typed language.

To go further:

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Learning Clojure: coping with dynamic typing
Share this