Chapter 9. Protocols, records, and types

This chapter covers

  • An overview of the expression problem
  • A custom solution to the expression problem
  • Clojure’s solution to the expression problem
  • Working with types and records: deftype, defrecord, and reify

Abstraction is an important tenet of software development because it results in code that’s more maintainable and extensible. Clojure itself is built on abstractions. For instance, most things in Clojure are coded to interfaces rather than being concrete implementations. This allows for reuse of code that expects those interfaces and allows for the seamless addition of more implementations to the mix.

Sooner or later, during your time on most projects, you’ll run into an abstraction-oriented issue known as the expression problem. It has to do with how to cleanly extend or use existing code—either something you wrote yourself or, more important, something you don’t own. In this chapter, we’ll explore the expression problem in some depth, as well as Clojure’s approach to handling it (protocols). Then, we’ll come up with a solution. Finally, we’ll explore the Clojure solution in more depth by looking at protocols, data types, and the reify macro.

9.1. The expression problem

Let’s imagine that you’ve been writing an application to manage employee expenses, one that will eventually replace an existing Java application that does a similar job. Naturally, you’re writing it in Clojure. If you find yourself needing to take two or more classes or sets of classes (or any other abstractions) and make them work together in a seamless way, you’re likely experiencing what’s known as the expression problem. This is a situation that comes up quite often. Whatever solution you come up with to do this, it’s likely you’ll need to support further extensibility, perhaps to support even more operations or data types.

There are two sides to solving the expression problem—namely, data types and the operations on them. We’ll start by looking at the operations side of the situation and follow up by looking at a couple ways to define data types. But before we dive into that, let’s take a moment to set up the example scenario.

9.1.1. Setting up the example scenario

There are two parts to this example: new code written in Clojure and legacy code written in Java. Both will have to coexist in the same program, so the functions you write need to operate on both native Clojure and native Java types seamlessly. First, you’ll write code using only the Clojure features you’ve learned so far; then we’ll take a look at the legacy Java code.

Creating a base

Here’s a portion of the expense namespace, with a function to create a simple map containing expense information:

(ns clj-in-act.ch9.expense
  (:import [java.text SimpleDateFormat]))
(defn new-expense [date-string dollars cents category merchant-name]
  {:date (.parse (SimpleDateFormat. "yyyy-MM-dd") date-string)
   :amount-dollars dollars
   :amount-cents cents
   :category category
   :merchant-name merchant-name})

As you’ve seen several times already, using a map is straightforward and is often the idiomatic way in Clojure to hold data of any kind. Now, for the purposes of illustration, here’s a function called total-cents that computes the expense amount in cents:

(defn total-cents [e]
  (-> (:amount-dollars e)
      (* 100)
      (+ (:amount-cents e))))

Nothing in this function ought to be unfamiliar, including the threading macro, which you saw in chapter 2.

Adding some functions

You’ll also add a function to calculate the total amount, given a list of expenses, and possibly a criteria function by which to select expenses from a list:

(defn total-amount
  ([expenses-list]
     (total-amount (constantly true) expenses-list))
  ([pred expenses-list]
     (->> expenses-list
         (filter pred)
         (map total-cents)
         (apply +))))

Then you’ll add a couple functions to help you create the predicate functions that can be used with total-amount, specifically to select a particular category of expenses:

(defn is-category? [e some-category]
  (= (:category e) some-category))
(defn category-is [category]
  #(is-category? % category))

The second function is syntactic sugar to help create single-argument predicates so your code reads easier. Let’s see how it looks by having you write some code that tests these functions. Consider a new namespace, such as expense-test, which you’ll start by creating a few sample expenses to play with:

(ns clj-in-act.ch9.expense-test
  (:require [clj-in-act.ch9.expense :refer :all]
            [clojure.test :refer :all]))
(def clj-expenses [(new-expense "2009-8-20" 21 95 "books" "amazon.com")
                   (new-expense "2009-8-21" 72 43 "food" "mollie-stones")
                   (new-expense "2009-8-22" 315 71 "car-rental" "avis")
                   (new-expense "2009-8-23" 15 68 "books" "borders")])
Testing the code

Here’s a test that uses this data set to compute the total amounts:

(deftest test-clj-expenses-total
  (is (= 42577 (total-amount clj-expenses)))
  (is (=  3763 (total-amount (category-is "books") clj-expenses))))
Clojure unit tests

This chapter may make it seem like you’ve been thrown into the deep end of the pool with regard to writing unit tests in Clojure. Rest assured that the next chapter will go into a lot of detail around this developer activity. You’ll need to know some basics to get through these pages.

You’re going to use the clojure.test library to write the tests. You start by requireing (or use-ing) the clojure.test namespace and using the deftest macro to write tests. You run tests by calling the run-tests function, passing in symbols of your test namespaces. In this example, you’d evaluate

(run-tests 'clj-in-act.ch9.expense-test)

This would output the results of your test run, and you can see if your tests passed.

If you run these tests, you’ll see that they pass. So now you have some basic code that shows the intent of the application. You can imagine a lot more functionality that helps an organization track expenses, but you’ve written sufficient code to demonstrate the issues you set out to face, so let’s move on.

The Java world

It’s now time to face a kind of reality you often face when working on application rewrites: business reasons might compel you to deal with the old codebase alongside the new one. For the purposes of this example, it means that you’re going to have to deal with instances of the Java-based Expense class. You could imagine such a class being similar to the one shown in the following listing.

Listing 9.1. Skeleton Java class implementing the concept of the expense item
package com.curry.expenses;
import java.util.Calendar;
import java.text.SimpleDateFormat;
import java.text.ParseException;
public class Expense {
    private Calendar date;
    private int amountDollars;
    private int amountCents;
    private String merchantName;
    private String category;
    public Expense(String dateString, int amountDollars, int amountCents,
              String category, String merchantName) throws ParseException {
        this.date = Calendar.getInstance();
        this.date.setTime(new SimpleDateFormat(
                              "yyyy-MM-dd").parse(dateString));
        this.amountDollars = amountDollars;
        this.amountCents = amountCents;
        this.merchantName = merchantName;
        this.category = category;
    }
    public Calendar getDate() {
        return date;
    }
    public int getAmountDollars() {
        return amountDollars;
    }

    public int getAmountCents() {
        return amountCents;
    }
    public String getMerchantName() {
        return merchantName;
    }
    public String getCategory() {
        return category;
    }
    public int amountInCents() {
        return this.amountDollars*100 + this.amountCents;
    }
}

To begin working with this class, you’ll write a sanity test to ensure everything is in order. Consider the following:

(def java-expenses [(Expense. "2009-8-24" 44 95 "books" "amazon.com")
                    (Expense. "2009-8-25" 29 11 "gas" "shell")])
(deftest test-java-expenses-total
  (let [total-cents (map #(.amountInCents %) java-expenses)]
    (is (= 7406 (apply + total-cents)))))
Getting JAR files onto the classpath

Once you compile the Expense class and create a JAR file, you’ll need to make sure that it’s included in the Clojure classpath. With Lein version 2.x, the easiest way to do this is to create a local Maven repository, deploy your JAR file into there, and then refer to the dependency in your project.clj file.

There’s a convenient Lein plugin called lein-localrepo that makes this much easier. It’s an open source contribution to the Clojure ecosystem, and it’s available on GitHub at https://github.com/kumarshantanu/lein-localrepo.

It’s recommended because it makes adding local JAR files to your project a breeze. Just follow the instructions at the GitHub web address.

You’ll need to import your Expense class into the test namespace for this to work. Once you do, you’ll see that running this test will result in it passing. Now that you’re able to access the Java class, you need to tackle the situation where you have both kinds of expenses together. For instance, you have to deal with a list of Clojure expense maps constructed via the new-expense function, as well as instances of the com.curry.expenses.Expense class.

You’ll capture this requirement in another test. Consider the following:

(def mixed-expenses (concat clj-expenses java-expenses))
(deftest test-mixed-expenses-total
  (is (= 49983 (total-amount mixed-expenses)))
  (is (= 8258 (total-amount (category-is "books") mixed-expenses))))

Now this test won’t pass. Indeed, the first assertion will print a long exception stack trace, because total-amount (and the underlying total-cents and is-category?) function only knows how to deal with Clojure map versions of expenses. To fix this, you’re going to have to deal with a design issue.

9.1.2. A closer look at the expression problem and some potential solutions

Philip Wadler is an ACM fellow and a computer science professor at the University of Edinburgh. He has made several important contributions to the field of functional programming, including the theory behind the Haskell programming language. He also coined the term expression problem:

The Expression Problem is a new name for an old problem. The goal is to define a data-type by cases, where one can add new cases to the data-type and new functions over the data-type, without recompiling existing code, and while retaining static type safety (e.g., no casts).[1]

1

Philip Wadler, The Expression Problem, November 12, 1998, http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt.

How can you add functionality to the code (your data type) so it plays well with code (data types) written by someone else (or any other code that you have no control over)? The last test you wrote to handle the case of mixed expenses has forced you to face the expression problem. Specifically, you need the total-amount function to accept and work with an entirely new data type that has its own set of operations (functions) defined for it. You’d also like the category-is function to create functions that can operate on this new data type, even though the new data type has no notion of such a category selector function right now.

The expression problem is common in our industry. There are several approaches to handle the issue, and we’ll briefly look at a few.

Wrappers

Because you have no control over the new data type (the Expense class), you could create a new wrapper class around it with the right methods that you can call from your program.

The trouble with this approach is that it increases incidental complexity because you’ve added a new class to your system. You’ve confused the identity: is an object that wraps an instance of Expense identical to it? How should code written elsewhere treat this new wrapper class if it’s passed in? Such an identity crisis is an example of the kind of nonlocal trouble that can arise when an instance of the wrapper is passed to unsuspecting code elsewhere in the system. You’d also have to create a wrapper class each time a new data type such as this comes along, leading to an explosion of such wrappers.

When all is said and done, languages such as Java often have no other choice than to go this route.

Monkey patching

Once classes have been written and compiled in Java, they can’t be modified (without manipulating bytecode). This is in contrast with languages such as Ruby, which are more dynamic and support open classes—classes that can be changed by anyone using the class, without directly editing the source code of that class. This is called monkey patching. Often, the syntax looks the same as writing the class the first time around, and any new methods defined (or redefined) become part of the original class.

The problem with this approach is that it’s a dangerous one, almost even more so than the wrapper approach. Because all changes to a class happen in a global manner (the class itself being the namespace), it has the potential for collisions. If you open a class and monkey patch it with a new method named total-cents, and someone else comes along and does the same, they will overwrite your patch. Such a collision can cause insidious problems, because the cause isn‘t immediately obvious.

if-then-else

Finally, there’s the approach of not using any well-structured tactic at all and checking for types inline with the code as needed. Client code such as the total-amount function will need to do different things depending on whether it was passed a Clojure map or an instance of the Java Expense class, using good-old if-then-else constructs.

This quickly gets complex, depending on how many data types need to be handled. Moreover, if support for a new data type needs to be added at a later point, it isn’t possible without modifying the code in all the places where this type checking is done. The incidental complexity of this approach is too great given that the solution is both rigid and inelegant.

What’s needed is an approach that doesn’t suffer from these problems. The Clojure programming language has the feature for this, and you saw it earlier in chapter 4. We’re talking about multimethods, and in the next section, you’ll write an implementation that works as desired.

9.1.3. Clojure’s multimethods solution

Multimethods allow you to decouple data types and operations on the data types in an elegant manner. We demonstrated this in chapter 4, where you used multimethods to handle a situation that would have required the visitor pattern in a language such as Java. In this section, you’ll use multimethods to get the latest test to pass without modifying the Java code for the Expense class and without creating wrappers or monkey patches.

Let’s refresh your memory by looking at the test that won’t pass right now:

(deftest test-mixed-expenses-total
  (is (= 49983 (total-amount mixed-expenses)))
  (is (= 8258 (total-amount (category-is "books") mixed-expenses))))

As noted before, the trouble is that the total-amount, is-category?, and total-cents functions only know how to work with Clojure maps. Your first step, then, will be to address this issue by changing the implementation of the total-cents and is-category? functions. You won’t touch total-amount, because it’s an example of client code (perhaps written by someone using the expense library). You can assume that you don’t control it and, indeed, that it’s a requirement of solving the expression problem, and you can’t change the alien data type or the client code.

Consider the following code, which is a replacement of the total-cents function:

(defmulti total-cents class)
(defmethod total-cents clojure.lang.IPersistentMap [e]
  (-> (:amount-dollars e)
      (* 100)
      (+ (:amount-cents e))))

Similarly, the following code will serve as the replacement of the is-category? function:

(defmulti is-category? (fn [e category] (class e)))
(defmethod is-category? clojure.lang.IPersistentMap [e some-category]
  (= (:category e) some-category))

You haven’t changed a lot of code; the bodies of the functions are the same as before. All you did was convert the functions to multimethods and redefine the old functions as methods, focusing on the fact that the expense object will be an instance of clojure.lang.IPersistentMap (which all Clojure maps are). Refer to chapter 4 to get a refresher on how this works with respect to dispatch functions and dispatch values.

At this point, if you run the tests, the old tests should still pass. Also, the new test will still fail because you haven’t written any code to deal with the Java Expense class. You’ll do that now, starting with the total-cents function:

(defmethod total-cents com.curry.expenses.Expense [e]
  (.amountInCents e))

And similarly, here’s the is-category? function:

(defmethod is-category? com.curry.expenses.Expense [e some-category]
  (= (.getCategory e) some-category))

With this, the new test will pass. Note, once again, that you didn’t change the Java Expense class in any way: you didn’t write a wrapper class for it, and you didn’t change the calling code (the total-amount function). You also kept all the code in your own namespace, allowing others to create their own functions named total-cents and is-category? without the fear of collisions.

Using multimethods has allowed you to solve this problem of handling new data types in an easy and elegant manner. You’re even set up to deal with more data types now, for example, if you need to ever deal with a third-party expense library.

There are a couple of downsides to this approach, though. The first is that even though multimethods allow you to dispatch via arbitrary functions, you’re using only the class of the first argument, which is either the Clojure map containing expense information or the Java Expense class. You don’t need the full power of multimethods here, and it would be nice if you didn’t have to explicitly write the dispatch functions the way you did previously.

The second issue is that even though the two multimethods you wrote are related to the task of computing totals, it isn’t obvious in the code. If someone were to read this code later, the fact that the two belong together wouldn’t jump out at them. This is even more apparent when you have several multimethods that should ideally show some kind of logical grouping. We’ll solve these issues next.

9.2. Examining the operations side of the expression problem

In this section, we’ll solve the two issues mentioned in the previous section. First, you don’t need the conceptual or syntactic complexity of full multimethods when you only want to dispatch on the class of the first argument. Second, you want to group related multimethods together so they read better.

We’ll call the solution to this modus operandi, which is a Latin phrase that means “method of operating.” The name reflects our intention here, which is to describe a set of operating procedures for something.

9.2.1. def-modus-operandi

Let’s start with the code you’d like to be able to write:

(def-modus-operandi ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))

What you’re saying here is that you’re defining a modus operandi called Expense-Calculations that will consist of two methods, namely total-cents and is-category?. You won’t specify the dispatch function as you did before, because you always want it to be the class of the first argument of each method. In this case, both methods will dispatch based on the class of the expense object, be it a Clojure map or the Java Expense class or any other data type you end up supporting.

Now, let’s look at implementing it. As you can imagine, def-modus-operandi is a macro. Here’s the code along with a couple of associated helper functions to make the code easier to read:

(defn dispatch-fn-for [method-args]
  `(fn ~method-args (class ~(first method-args))))
(defn expand-spec [[method-name method-args]]
  `(defmulti ~method-name ~(dispatch-fn-for method-args)))
(defmacro def-modus-operandi [mo-name & specs]
  `(do
     ~@(map expand-spec specs)))

So all you’re doing is generating code that creates multimethods. Here’s what the expanded version looks like:

(do
  (clojure.core/defmulti total-cents (clojure.core/fn [e]
                                       (clojure.core/class e)))
  (clojure.core/defmulti is-category? (clojure.core/fn [e category]
                                        (clojure.core/class e))))

Notice that the expanded form of is-category? is the same as when you wrote it by hand earlier. The expansion for total-cents is slightly different, only because you can generate the same dispatch function no matter how many arguments the function takes.

Now that you have a way to specify the methods in your modus operandi, you need a way to detail it for the types you’d like to support. We’ll do that next.

9.2.2. detail-modus-operandi

After defining the what of a modus operandi, you need to define the how. You’ll create a new macro called detail-modus-operandi that you’ll use in the following manner:

(detail-modus-operandi ExpenseCalculations
  clojure.lang.IPersistentMap
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (is-category? [e some-category]
    (= (:category e) some-category)))

Most of the code should be familiar to you, because it’s nearly identical to the code from the previous section. Because all the methods are being defined for the same dispatch value, you’ve made it so that you have to specify it only once. Here’s the implementation of the macro, along with an associated helper function:

(defn expand-method [data-type [name & body]]
  `(defmethod ~name ~data-type ~@body))
(defmacro detail-modus-operandi [mo-name data-type & fns]
  `(do
     ~@(map #(expand-method data-type %) fns)))

The expansion of this call to detail-modus-operandi is as follows:

(do
  (clojure.core/defmethod total-cents clojure.lang.IPersistentMap [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (clojure.core/defmethod is-category?
     clojure.lang.IPersistentMap [e some-category]
   (= (:category e) some-category)))

So you’ve done what you set out to do. You have a new abstraction that sits atop multimethods that behave like subtype polymorphism. The methods dispatch on the type of the first argument.

Notice that even though you specified the name of your modus operandi here (called ExpenseCalculations), you haven’t used it for anything. You can make your modus operandi more useful if you use the named objects to track such things as what it contains and who implements it. Let’s do that next.

9.2.3. Tracking your modus operandi

So far, you’ve allowed declarations of a modus operandi that’s a set of related multimethods that dispatches on the type of the first argument. In this section, you’ll collect some meta-information about these methods that you can use to programmatically query things about the modus operandi.

During def-modus-operandi

The first thing you’ll do is define a var with the name of the modus operandi. Doing that by itself is easy enough: you add a call to def in the def-modus-operandi macro. The question is what should the var be bound to? A simple option is to create a map containing information about the modus operandi. Try that approach:

(defmacro def-modus-operandi [mo-name & specs]
  `(do
     (def ~mo-name ~(mo-methods-registration specs))
     ~@(map expand-spec specs)))

You’ve delegated to a helper function called mo-methods-registration, so implement that next:

(defn mo-method-info [[name args]]
  {(keyword name) {:args `(quote ~args)}})
(defn mo-methods-registration [specs]
  (apply merge (map mo-method-info specs)))

You’re collecting the name and arguments of each method into a map. This map, with all the information about the methods being specified as part of the modus operandi, will become the root binding of a var by the same name as the modus operandi. You can try it. First, you’ll redefine the modus operandi:

(def-modus-operandi ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))
;=> #'user/is-category?

Next, see what the ExpenseCalculations var is bound to:

ExpenseCalculations
;=> {:is-category? {:args [e category]}, :total-cents {:args [e]}}

So you have the basic information. Next, you’ll collect some more information every time detail-modus-operandi is called.

During detail-modus-operandi

To collect information of the implementer of a modus operandi, you’ll first need to pass the modus operandi into the expand-method function:

(defmacro detail-modus-operandi [mo-name data-type & fns]
  `(do
     ~@(map #(expand-method mo-name data-type %) fns)))

Now that the expand-method knows which modus operandi it’s going to create a method for, you can collect information about it:

(defn expand-method [mo-name data-type [method-name & body]]
  `(do
     (alter-var-root (var ~mo-name) update-in
               [(keyword '~method-name) :implementors] conj ~data-type)
     (defmethod ~method-name ~data-type ~@body)))

To better understand this addition to the expand-method function, let’s talk about the data you’re collecting. Recall that the modus operandi var is bound to a map that contains a key for each method. The value for each such key is another map. The only key in the inner map so far is :args, and to collect the data types of the implementors to this map, you’ll introduce another key called :implementors. So here you’re going to conj the data type onto the list of implementors (if any) each time a method of a modus operandi is implemented.

Finally, let’s look at the function alter-var-root. Here’s the doc string:

(doc alter-var-root)
-------------------------
clojure.core/alter-var-root
([v f & args])
  Atomically alters the root binding of var v by applying f to its
  current value plus any args

So you’re passing it the var for the modus operandi and the function update-in. The arguments to update-in are a sequence of keys that locates a nested value and a function that will be applied to the existing value along with any other arguments. In this case, update-in is passed the function conj along with the data type you’d like recorded.

Phew, that’s a lot of work for a single line of code! The following listing shows the complete implementation of modus operandi in a single namespace.

Listing 9.2. Implementing modus operandi on top of multimethods
(ns clj-in-act.ch9.modus-operandi)
(defn dispatch-fn-for [method-args]
  `(fn ~method-args (class ~(first method-args))))

(defn expand-spec [[method-name method-args]]
  `(defmulti ~method-name ~(dispatch-fn-for method-args)))
(defn mo-method-info [[name args]]
  {(keyword name) {:args `(quote ~args)}})
(defn mo-methods-registration [specs]
  (apply merge (map mo-method-info specs)))
(defmacro def-modus-operandi [mo-name & specs]
  `(do
     (def ~mo-name ~(mo-methods-registration specs))
     ~@(map expand-spec specs)))
(defn expand-method [mo-name data-type [method-name & body]]
  `(do
     (alter-var-root (var ~mo-name) update-in [(keyword '~method-name) :implementors] conj ~data-type)
     (defmethod ~method-name ~data-type ~@body)))
(defmacro detail-modus-operandi [mo-name data-type & fns]
  `(do
     ~@(map #(expand-method mo-name data-type %) fns)))

Let’s look at it in action. First, make a call to detail-modus-operandi:

(detail-modus-operandi ExpenseCalculations
   clojure.lang.IPersistentMap
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (is-category? [e some-category]
    (= (:category e) some-category)))
;=> #<MultiFn clojure.lang.MultiFn@4aad8dbc>

Now look at the ExpenseCalculations var:

ExpenseCalculations
;=> {:is-category? {:implementors (clojure.lang.IPersistentMap),
                    :args [e category]},
     :total-cents {:implementors (clojure.lang.IPersistentMap),
                   :args [e]}}

As you can see, you’ve added the new :implementors key to the inner maps, and they have a value that’s a sequence of the implementors so far. Now implement the modus operandi for the Java Expense class:

(detail-modus-operandi ExpenseCalculations
  com.curry.expenses.Expense
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))
;=> #<MultiFn clojure.lang.MultiFn@4aad8dbc>

You can now see what the ExpenseCalculations var is bound to:

ExpenseCalculations
;=> {:is-category? {:implementors (com.curry.expenses.Expense
                                   clojure.lang.IPersistentMap),
                    :args [e category]},
     :total-cents {:implementors (com.curry.expenses.Expense
                                  clojure.lang.IPersistentMap),
                   :args [e]}}

And there you have it: you’re collecting a sequence of all implementing classes inside the map bound to the modus-operandi var. You should now ensure that everything still works with the original code. Figure 9.1 shows a conceptual view of the process of defining a modus operandi and then detailing it. Listing 9.3 shows the complete code for the expense namespace.

Figure 9.1. Calling def-modus-operandi creates a var that will hold information about the modus operandi that can later be used to introspect it. The macro itself makes as many calls to defmulti as needed. The detail-modus-operandi macro is the other side of the modus operandi concept: it fills out the implementation details by expanding to as many defmethod calls as specified. It also updates the modus-operandi var to reflect the implementor information.

Listing 9.3. The expense namespace using the modus operandi multimethod syntax
(ns clj-in-act.ch9.expense-modus-operandi
  (:require [clj-in-act.ch9.modus-operandi :refer :all])
  (:import [java.text SimpleDateFormat]
           [java.util Calendar]))

(defn new-expense [date-string dollars cents category merchant-name]
  (let [calendar-date (Calendar/getInstance)]
    (.setTime calendar-date
          (.parse (SimpleDateFormat. "yyyy-MM-dd") date-string))
    {:date calendar-date
     :amount-dollars dollars
     :amount-cents cents
     :category category
     :merchant-name merchant-name}))
(def-modus-operandi ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))
(detail-modus-operandi ExpenseCalculations
  clojure.lang.IPersistentMap
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (is-category? [e some-category]
    (= (:category e) some-category)))
(detail-modus-operandi ExpenseCalculations
  com.curry.expenses.Expense
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))
(defn category-is [category]
  #(is-category? % category))
(defn total-amount
  ([expenses-list]
     (total-amount (constantly true) expenses-list))
  ([pred expenses-list]
     (->> expenses-list
         (filter pred)
         (map total-cents)
         (apply +))))

Similarly, the following listing shows the tests you’ve written so far, all in one place. You’ll run the tests next.

Listing 9.4. Testing the implementation of modus operandi calculating expense totals
(ns clj-in-act.ch9.expense-test
  (:import [com.curry.expenses Expense])
  (:require [clj-in-act.ch9.expense-modus-operandi :refer :all]
            [clojure.test :refer :all]))
(def clj-expenses [(new-expense "2009-8-20" 21 95 "books" "amazon.com")
                   (new-expense "2009-8-21" 72 43 "food" "mollie-stones")
                   (new-expense "2009-8-22" 315 71 "car-rental" "avis")
                   (new-expense "2009-8-23" 15 68 "books" "borders")])
(deftest test-clj-expenses-total
  (is (= 42577 (total-amount clj-expenses)))
  (is (=  3763 (total-amount (category-is "books") clj-expenses))))

(def java-expenses [(Expense. "2009-8-24" 44 95 "books" "amazon.com")
                    (Expense. "2009-8-25" 29 11 "gas" "shell")])
(deftest test-java-expenses-total
  (let [total-cents (map #(.amountInCents %) java-expenses)]
    (is (= 7406 (apply + total-cents)))))
(def mixed-expenses (concat clj-expenses java-expenses))
(deftest test-mixed-expenses-total
  (is (= 49983 (total-amount mixed-expenses)))
  (is (= 8258 (total-amount (category-is "books") mixed-expenses))))

These tests now all pass:

(use 'clojure.test) (run-tests 'clj-in-act.ch9.expense-test)
Testing clj-in-act.ch9.expense-test
Ran 3 tests containing 5 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 3, :pass 5, :fail 0, :error 0}

Finally, before wrapping up this section, you’ll write a couple of functions that will make it easy to query data about your modus operandi, like ExpenseCalculations.

Querying modus operandi

The first function you’ll write discerns what data types implement a particular modus operandi. Consider this code:

(defn implementors [modus-operandi method]
  (get-in modus-operandi [method :implementors]))

And this allows you to do things like this:

(implementors ExpenseCalculations :is-category?)
;=> (com.curry.expenses.Expense clojure.lang.IPersistentMap)

Now you’ll write another function that when given a class of a particular data type can tell you if it implements a particular method of a modus operandi. Here’s the code:

(defn implements? [implementor modus-operandi method]
  (some #{implementor} (implementors modus-operandi method)))

Now test it at the REPL:

(implements? com.curry.expenses.Expense ExpenseCalculations :is-category?)
;=> com.curry.expenses.Expense

Note that implements? returns the class itself, which is truthy. Here’s a negative scenario:

(implements? java.util.Date ExpenseCalculations :is-category?)
;=> nil

Now that you have a function such as implements?, you can also write a broader function to see if a class implements a modus operandi completely:

(defn full-implementor? [implementor modus-operandi]
  (->> (keys modus-operandi)
       (map #(implements? implementor modus-operandi %))
       (not-any? nil?)))

Here it is in action:

(full-implementor? com.curry.expenses.Expense ExpenseCalculations)
;=> true

To test the negative side, you’ll partially implement the modus operandi:

(detail-modus-operandi ExpenseCalculations
   java.util.Date
   (total-cents [e]
     (rand-int 1000)))
;=> #<MultiFn clojure.lang.MultiFn@746ac18c>

And now you can test what you were after:

(full-implementor? java.util.Date ExpenseCalculations)
;=> false

You can implement other functions such as these, because the value bound to the modus-operandi var is a regular map that can be inspected like any other. Next, let’s examine the downsides to the modus operandi approach to the expression problem.

9.2.4. Error handling and trouble spots in this solution

In this section, you took multimethods and wrote a little DSL on top of them that allows you to write simpler, clearer code when you want to dispatch on the class of the first argument. You were also able to group related multimethods together via this new syntax, and this allowed the code to be self-documenting by communicating that certain multimethods are related to each other.

What we haven’t touched on at all is error handling. For instance, if you eval the same detail-modus-operandi calls multiple times, the data-collection functions would add the class to your modus operandi metadata map multiple times. It’s an easy fix, but this isn’t the most robust code in the world, because it was written to demonstrate the abstraction.

There are other trouble spots as well. For instance, because you built this on top of multimethods, and multimethods support hierarchies (and Java inheritance hierarchies by default), the implements? and related functions won’t give accurate answers as they stand now.

Further, because this is such a bare-bones implementation, many other features might be desirable in a more production-ready version. The other downside is that there’s a small performance hit when using multimethods because they have to call the dispatch function and then match the dispatch value against available multimethods. After all, this approach is syntactic sugar on top of multimethods.

In the next section, you’ll see Clojure’s version of the solution.

9.3. Examining the data types side of the expression problem with protocols

You’ve already seen what the expression problem is and a variety of ways to solve it. Clojure’s multimethods are perfectly suited to writing code that allows independent extension of the supported data types and operations. You also created an abstraction called modus operandi that supports the most common use of multimethods: single dispatch (the first argument) based on the type (or class).

Clojure’s multimethods are more expressive than Java’s object methods but much slower because Java is optimized for single dispatch on type, not arbitrary multiple dispatch. In most cases the performance difference is negligible and the increased expressiveness of code more than makes up for it. But as Clojure matures and moves more of its implementation into Clojure itself, there needs to be a way to support its abstraction and data-definition facilities without this performance hit. Protocols and data types are that solution, and they also offer a high-performance solution to a commonly encountered subset of the expression problem by using Java’s extremely fast single dispatch on type.

In this section, we’ll examine what protocols and data types are and how they can be used. Keep in mind your design of modus operandi as you work through this.

9.3.1. defprotocol and extend-protocol

The term protocol means the way something is done, often predefined and followed by all participating parties. Clojure protocols are analogous to your modus operandi, and defprotocol is to protocols what def-modus-operandi is to modi operandi. Similarly, extend-protocol is to protocols what detail-modus-operandi is to modi operandi.

Listing 9.3 showed the implementation of the expense calculation, and the next listing shows the same logic implemented using Clojure protocols.

Listing 9.5. expense namespace using a Clojure protocol
(ns clj-in-act.ch9.expense-protocol
  (:import [java.text SimpleDateFormat]
           [java.util Calendar]))
(defn new-expense [date-string dollars cents category merchant-name]
  (let [calendar-date (Calendar/getInstance)]
    (.setTime calendar-date (.parse (SimpleDateFormat. "yyyy-MM-dd")
                                                          date-string))
    {:date calendar-date
     :amount-dollars dollars

     :amount-cents cents
     :category category
     :merchant-name merchant-name}))
(defprotocol ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))
(extend-protocol ExpenseCalculations
  clojure.lang.IPersistentMap
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (is-category? [e some-category]
    (= (:category e) some-category)))
(extend-protocol ExpenseCalculations
  com.curry.expenses.Expense
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))
(defn category-is [category]
  #(is-category? % category))
(defn total-amount
  ([expenses-list]
     (total-amount (constantly true) expenses-list))
  ([pred expenses-list]
     (->> expenses-list
         (filter pred)
         (map total-cents)
         (apply +))))

The only things that are different from the implementation based on your modus operandi are that the dependence on the clj-in-act.ch9.modus-operandi namespace is removed and the calls to def-modus-operandi and detail-modus-operandi are replaced with calls to defprotocol and extend-protocol. At a conceptual level, the code in listing 9.5 should make sense. We’ll get into the specifics now.

Defining new protocols

As you might have guessed, new protocols are defined using the defprotocol macro. It defines a set of named methods, along with their signatures. Here’s the official syntax:

The protocol as well as the methods that form it can accept doc strings. A call to defprotocol results in a bunch of vars being created: one for the protocol itself and one for each polymorphic function (or method) that’s a part of the protocol. These functions dispatch on the type of the first argument (and therefore must have at least one argument), and by convention, the first argument is called this. So from listing 9.5, the following snippet defines a protocol named ExpenseCalculations:

(defprotocol ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))

You’re defining a set of related methods (total-cents and is-category?) that can be implemented any number of times by any data type. A call to defprotocol also generates an underlying Java interface. So, because the previous code exists in the namespace clj-in-act.ch9.expense-protocol, it will result in a Java interface called chapter_protocols.expense_protocol.ExpenseCalculations. The methods in this interface will be the ones specified in the definition of the protocol, total _cents and is_category_QMARK. The reference to QMARK is thanks to the translated name of a Clojure function (one that ends with a question mark) into Java.[2] The fact that defprotocol generates a Java interface also means that if some other Java code wants to participate in a protocol, it can implement the generated interface and proceed as usual.

2

This translation from Clojure names to legal Java names is called munging. Normally, munging is transparently handled by Clojure, and you don’t need to know about it, but it’s important to keep in mind for two reasons: Clojure imports understand only Java names, and the filenames for your namespaces should use munged names.

Now that you’ve defined a protocol, any data type can participate in it.

Participating in protocols

Having defined a protocol, let’s see how you can use it. As an example, consider the call to extend-protocol, also from listing 9.5:

(extend-protocol ExpenseCalculations
  com.curry.expenses.Expense
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))

This means that the com.curry.expenses.Expense data type will participate in the ExpenseCalculations protocol, and when either total-cents or is-category? is called with an instance of this class as the first argument, it will be correctly dispatched to the previous implementation.

You can also specify more than one participant at a time; you can define the implementations of the protocol methods for more than a single data type. Here’s an example:

(extend-protocol ExpenseCalculations
  clojure.lang.IPersistentMap
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))

  (is-category? [e some-category]
    (= (:category e) some-category))
  com.curry.expenses.Expense
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))

We’ll now look at another way to specify how data types can participate in protocols.

extend-type macro

extend-protocol is a helper macro, defined on top of another convenient macro named extend-type. It’s sort of the other way of specifying a participant of a protocol, in that it focuses on the data type. Here’s an example of extend-type in use:

(extend-type com.curry.expenses.Expense
  ExpenseCalculations
  (total-cents [e]
    (.amountInCents e))
  (is-category? [e some-category]
    (= (.getCategory e) some-category)))

Again, because a single data type can participate in multiple protocols, extend-type lets you specify any number of protocols. Although extend-protocol and extend-type make it quite easy to use protocols, they both ultimately resolve to calls to the extend function.

extend function

The extend function lives in Clojure’s core namespace, and it’s the one that does the work of registering protocol participants and associating the methods with the right data types. Here’s an example of the extend function in action:

(extend com.curry.expenses.Expense
  ExpenseCalculations {
    :total-cents (fn [e]
                   (.amountInCents e))
    :is-category? (fn [e some-category]
                    (= (.getCategory e) some-category))})

This might look similar to the code you generated in the implementation of your modus operandi. For each protocol and data type pair, extend accepts a map that describes participation of that data type in the protocol. The keys of the map are keyword versions of the names of the methods, and the values are the function bodies that contain the implementation for each. The extend function is the most flexible in terms of building an implementation of a protocol.

Figure 9.2 shows the conceptual flow of defining and using protocols.

Figure 9.2. Calling defprotocol performs an analogous operation where a var is created to hold information about the protocol and its implementors. The underlying implementation will also result in a Java interface that pertains to the protocol being defined. Calls to extend, extend-type, and extend-protocol will update the var with implementor details and generate Java classes that implement the protocol.

We’ve covered protocols and how they’re defined and used. We’ll say another couple of things about them before moving on to the remaining topics of this chapter.

Protocols and nil

You’ve seen that protocol methods are dispatched based on the class of the first argument. A natural question arises: What will happen if the first argument to a protocol method is nil? What is the class of nil?

(class nil)
;=> nil

If you call a protocol method, say total-cents from the expense example with nil, you’ll get an error complaining that no implementation was found. Luckily, protocols can be extended on nil:

(extend-protocol ExpenseCalculations nil
  (total-cents [e] 0))

After this, calling total-cents with nil will return 0. Similarly, you can implement the is-category? function to return something appropriate for nil, perhaps false. Our last stop in this section will be to explore a few functions that help you reflect on defined protocols.

Reflecting on protocols

Sometimes it’s useful to programmatically reflect on specific protocols and their extenders. When you wrote your modus operandi, you also wrote some helper functions that let you reflect on implements?, implementors, and full-implementor. Clojure protocols also have functions that work in a similar fashion:

(extends? ExpenseCalculations com.curry.expenses.Expense)
;=> true
(extends? ExpenseCalculations clojure.lang.IPersistentMap)
;=> true
(extends? ExpenseCalculations java.util.Date)
;=> false

Needless to say, the function extends? can be used to check if a particular data type participates in a given protocol. The next function that’s useful around such querying is extenders:

(extenders ExpenseCalculations)
;=> (nil com.curry.expenses.Expense clojure.lang.IPersistentMap)

Again, the extenders function lists all the data types that participate in a particular protocol. The final function of interest is called satisfies? and it works like this:

(satisfies? ExpenseCalculations (com.curry.expenses.Expense. "10-10-2010" 20 95 "books" "amzn"))
;=> true
(satisfies? ExpenseCalculations (new-expense "10-10-2010" 20 95 "books" "amzn"))
;=> true
(satisfies? ExpenseCalculations (java.util.Random.))
;=> false

Note that the satisfies? function works on instances of extenders, not extender data types themselves. You’ll find this function even more useful once you’ve seen the reify macro in action, which we’ll explore in the next section.

We’ve now covered all the topics about protocols that we set out to cover. The next section is about the other side of this picture; we’ll review a couple of ways to define data types.

9.3.2. Defining data types with deftype, defrecord, and reify

We started this chapter by considering the expression problem, and as you might recall, there are two sides to it, namely, data types and the operations on them. So far, we’ve been looking primarily at the operations side of the situation; in this section we’ll look at a couple ways to define data types.

The mechanisms we’re going to talk about create underlying classes on the host platform (namely Java today, but it could be others tomorrow). This means that they share the same performance as the native version of such data types, as well as the same polymorphic capabilities supported by the host. We’ll first look at defrecord, followed by deftype. We’ll close the section with a look at reify.

defrecord

Let’s start with an example of using defrecord:

(defrecord NewExpense [date amount-dollars amount-cents category
                       merchant-name])

The defrecord macro call defines a named class (in this case chapter_protocols .expense_record.NewExpense) that has the specified set of fields, a class constructor, and two constructor functions (in this case ->NewExpense and map->NewExpense). Because this is a proper class on the host environment, the type of class is fully specified and known, allowing for a high-performance dispatch of fields and methods. Similarly, it has a named constructor, similar to other Java classes. Here’s how you’d create an instance of the NewExpense data type:

Notice a few things about creating a record instance. defrecord creates a real Java class, which is why you need to use import and a classpath (with Clojure-to-Java name munging) to access the record and Java instance-creation interop to construct it directly. If you hadn’t used import, you’d have gotten a ClassNotFoundException when you tried to invoke the constructor. But defrecord also creates Clojure constructor functions in the same namespace: this is the more common and idiomatic way to create instances of records from Clojure. The two constructors created are ->RECORDNAME (where RECORDNAME is the name of the record), which accepts positional parameters identical to those of the Java constructor, and map->RECORDNAME, which accepts a map. Keys in the map that match record field names become record fields; fields with no matching keys in the map will get the value nil; any extra keys will be added to the record in a spillover map.

Now that you’ve come this far, go ahead and change the implementation of the expense namespace to use this. The following listing shows the new implementation.

Listing 9.6. expense namespace using a Clojure protocol and defrecord
(ns clj-in-act.ch9.expense-record
  (:import [java.text SimpleDateFormat]
           [java.util Calendar]))
(defrecord NewExpense [date amount-dollars amount-cents
                         category merchant-name])
(defn new-expense [date-string dollars cents category merchant-name]
  (let [calendar-date (Calendar/getInstance)]
    (.setTime calendar-date (.parse (SimpleDateFormat. "yyyy-MM-dd")
                                                          date-string))
    (->NewExpense calendar-date dollars cents category merchant-name)))
(defprotocol ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))
(extend-type NewExpense
  ExpenseCalculations
  (total-cents [e]
    (-> (:amount-dollars e)
        (* 100)
        (+ (:amount-cents e))))
  (is-category? [e some-category]
    (= (:category e) some-category)))
(extend com.curry.expenses.Expense
  ExpenseCalculations {
    :total-cents (fn [e] (.amountInCents e))
    :is-category? (fn [e some-category] (= (.getCategory e)
                                            some-category))})
(extend-protocol ExpenseCalculations nil
  (total-cents [e] 0))
(defn category-is [category]
  #(is-category? % category))
(defn total-amount
  ([expenses-list]
     (total-amount (constantly true) expenses-list))
  ([pred expenses-list]
     (->> expenses-list
         (filter pred)
         (map total-cents)
         (apply +))))

Notice the call to extend-type and how you use the name of the newly defined record NewExpense instead of the previously used, more generic IPersistentMap. This shows that records can participate fully in protocols and indeed can participate in as many as needed. By the way, for completeness, it’s worth modifying the test namespace to depend on the new clj-in-act.ch9.expense-record namespace and checking to see if all tests pass. They should.

Notice how you can access the fields of the NewExpense instances using keywords. This is because defrecord creates a class that already implements several interfaces, including IPersistentMap, IKeywordLookup, and ILookup. In this manner, they work in the same way as regular Clojure maps, including with respect to destructuring, metadata, and the use of functions such as assoc and dissoc. A useful point to note is that records are extensible in that they can accept values for keys that weren’t originally specified as part of the defrecord call. The only penalty to this is that such keys have the same performance as Clojure maps. Records also implement the hashCode and equals methods, to support value-based equality out of the box. A final note is that the field specification supports type hints. By the way, it’s worth noting here that records aren’t functions, and so they can’t be used as functions when looking up values. For instance, you can look up a key inside a Clojure map by using the map as a function and the key as a parameter, but you can’t do this with records. Here’s an example to illustrate records’ maplike features:

Listing 9.6 shows how records can participate in protocols. There’s nearly no change to the code from the previous implementation in listing 9.5, but records have more direct support for protocols. They can supply the implementations of protocols inline with their definition. The following listing shows this version.

Listing 9.7. expense namespace with defrecord and inline protocol
(ns clj-in-act.ch9.expense-record-2
  (:import [java.text SimpleDateFormat]
           [java.util Calendar]))
(defprotocol ExpenseCalculations
  (total-cents [e])
  (is-category? [e category]))
(defrecord NewExpense [date amount-dollars amount-cents
                                          category merchant-name]
  ExpenseCalculations
  (total-cents [this]

    (-> amount-dollars
        (* 100)
        (+ amount-cents)))
  (is-category? [this some-category]
    (= category some-category)))
(defn new-expense [date-string dollars cents category merchant-name]
  (let [calendar-date (Calendar/getInstance)]
    (.setTime calendar-date (.parse (SimpleDateFormat. "yyyy-MM-dd")
                                                          date-string))
    (->NewExpense calendar-date dollars cents category merchant-name)))
(extend com.curry.expenses.Expense
  ExpenseCalculations {
    :total-cents (fn [e] (.amountInCents e))
    :is-category? (fn [e some-category] (= (.getCategory e)
                                               some-category))})
(extend-protocol ExpenseCalculations nil
  (total-cents [e] 0))
(defn category-is [category]
  #(is-category? % category))
(defn total-amount
  ([expenses-list]
     (total-amount (constantly true) expenses-list))
  ([pred expenses-list]
     (->> expenses-list
         (filter pred)
         (map total-cents)
         (apply +))))

The main change is in the following snippet:

(defrecord NewExpense [date amount-dollars amount-cents category
                       merchant-name]
  ExpenseCalculations
  (total-cents [this]
    (-> amount-dollars
        (* 100)
        (+ amount-cents)))
  (is-category? [this some-category]
    (= category some-category)))

Notice that the field names are followed with the protocol name that you want to implement. The protocol name is followed by the implementations of the protocol methods. You can similarly follow that with more protocol specifications (the protocol name followed by the implementation).

Java support

What’s more, this isn’t restricted to protocols; you can also specify and implement Java interfaces. The code would look similar to the previous protocol specification: you’d specify the interface name followed by the implementation of the interface methods. Instead of a protocol or an interface, you can also specify Object to override methods from the Object class. Recall that the first parameter of all protocol methods is the implementor instance itself, so you must pass the conventionally named this parameter as before. This means that there will be one more parameter for each interface or object method when compared to the corresponding definition. Finally, if the method implementation needs to call recur, the this parameter shouldn’t be passed, because it will be passed automatically.

With this discussion, we’ve covered records. They can be used in all places where maps might have been used because they’re faster and also support protocols. Note that the implementation of protocol methods didn’t result in closures, and when this functionality is needed, you can use the reify macro. You’ll see that shortly, but our next stop is the deftype macro.

deftype

When you use defrecord, you get a whole bunch of functionality for free. You get maplike behavior of using keywords to look up stuff, you get value-based equality behavior, you get metadata support, and you get serialization. This is usually exactly what you need when developing application domain data types, such as the expense data type from the previous few sections.

But there are times when you don’t need any of this; indeed, at times you want to specify your own implementations for some of these interfaces. It’s for these times that Clojure also provides the deftype macro:

(deftype Mytype [a b])

This generates an underlying Java class that looks like the following:

public final class Mytype {
    public final Object a;
    public final Object b;
    public Mytype(Object obj, Object obj1) {
        a = obj;
        b = obj1;
    }
}

As you can see, the fundamental difference between defrecord and deftype is that the latter produces a bare-metal class that you can do whatever you want with. The most common use case of deftype is to build infrastructure abstractions. Examples of such an abstraction might be a special collection to hold your domain-specific objects or a custom transaction manager. When you do need such a data type, with the performance characteristics of the native host, you can use deftype. In most other cases, defrecord should suffice.

We’re nearly finished! In the previous section, we briefly mentioned closures. In the next section, we’ll show how to create anonymous data types and instances of them using the reify macro.

reify

Reification means to bring something into being or to turn something into a concrete form. The reify macro takes a protocol, which by itself is an abstract set of methods, and creates a concrete instance of an anonymous data type that implements that protocol or interface. It does so with the full power of Clojure’s lexical closures. For example, you might implement the new-expense function as follows:

(defn new-expense [date-string dollars cents category merchant-name]
  (let [calendar-date (Calendar/getInstance)]
    (.setTime calendar-date
               (.parse (SimpleDateFormat. "yyyy-MM-dd") date-string))
    (reify ExpenseCalculations
      (total-cents [this]
        (-> dollars
          (* 100)
          (+ cents)))
      (is-category? [this some-category]
        (= category some-category)))))

In a pattern that’s similar to one you’ve seen before, reify accepts one or more protocols or interfaces and their implementations. In this example, reify was passed the ExpenseCalculations protocol along with the implementations of the total-cents and is-category? methods. The object returned by reify is a closure; in the case of new-expense, the lexically bound closure includes the parameters passed to new-expense, along with the names created in the let form.

You’ve now learned enough about protocols and data types to use them in your own programs. To round off this chapter, we’ll make a few observations about protocols and compare them to multimethods.

9.4. Summary

Protocols were originally introduced to satisfy the need for low-level implementation techniques that would be fast enough to implement the language itself in, a la Clojure in Clojure. They also serve to solve 90% of the expression problem cases, where class-based single dispatch is acceptable. In this way, they’re less powerful than multimethods.

Even with that, protocols have several advantages. Similar to multimethods, they don’t tie polymorphism to inheritance. They allow grouping of related methods into a conceptual unit, which makes for clearer, self-documenting code. Because protocols generate interfaces from the underlying host, they’re able to provide performance that’s on par with the host itself. Similar to multimethods, they’re an open way of solving the expression problem. This means that new data types and operations can be added while making minimum changes to existing code. Similarly, openness is maintained with respect to who is allowed to participate in protocols. Any number of data types can implement a single protocol, and a data type can implement any number of protocols. Finally, because protocols belong to the namespace they’re defined in, there’s no danger of name collisions if someone defines a protocol with the same name that you chose.

As a parting note, it’s worth mentioning that even before the introduction of defrecord, using maps to store information was the idiomatic way to implement things. Hiding information behind nongeneric interfaces (such as getters/setters or even more custom methods) makes such information less reusable by code that wasn’t designed to access such an API. Maps provide far more generic manipulability, and records take that one step further by making it perform as fast as the host platform can make it.

When coupled with protocols, your Clojure code will be built on abstractions. This will ensure that it’s more flexible and easier to maintain, as well as being easy for other people to work with. In this manner, protocols realize a huge benefit even beyond solving the most common case of the expression problem.

In the next chapter you’ll learn how to use Clojure’s built-in testing library and how to write Clojure using a test-driven method of development.

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

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