After several months of working in Clojure, I noticed that the deeper I thought about the problem I was solving, the shorter my code became. Working in object-oriented languages, I’ve generally found the opposite to be true: the more you analyze a problem, the more classes you wind up with. This is particularly true in statically-typed object-oriented languages.
Abstractions in Object-Oriented Languages
In object-oriented languages, classes are supposed to be abstractions. In a Human Resources application, an Employee class is essentially a template to hold information about an employee, such as the employee’s name, date of hire, department, manager, etc. The class is an abstraction to the extent that it can represent any employee in the organization, and to the extent that it allows various parts of the application to deal with employees in a uniform way.
Compared to basic data structures like lists and hash maps, however, classes are not particularly abstract. In fact, they are very specific. The Employee object has a name, a hire date, a department (which is likely an instance of class Department) and a manager (which is likely another instance of class Employee). Methods that interact with Employee objects must be written specifically for Employee objects, or for some base class that Employee descends from, or for some interface the class implements.
Of course, this is completely natural in the object-oriented world. This is by design. When you design your application, you model out the problem first. You can start by describing in plain English the problem you’re going to solve. The nouns become classes and verbs become methods.
You find commonalities among the nouns, and you build a class hierarchy based on those commonalities. For example, in the HR application, an Employee is a Person, and a JobCandidate is a Person. So you start with a base class of Person, and your application can do many of the same things with a JobCandidate that it can do with an Employee. This design also make it easy for a JobCandidate to become an Employee.
In the design process, you also try to find commonalities in ways the system will need to manipulate different classes of objects. When you discover these commonalities, you design interfaces– sets of common verbs or methods– and make sure that each class implements the necessary interface.
Interfaces are in some ways a more flexible abstraction than classes because while classes must promise that object adheres to an exact structure, interfaces merely promise that an object will respond to a set of method calls, regardless of their structure.
The explicit definitions that classes and interfaces require provide some benefits:
- The programmer knows exactly what he is dealing with, because a class’s properties and methods are explicitly defined before it is ever used.
- In statically-typed languages in particular, an Integrated Development Environment (IDE) can provide tremendous assistance in writing, understanding and debugging code. Again, this is because every property and method is explicitly defined before it is ever used. The IDE can analyze the code before it even runs, and can help the developer with tools like code completion and the ability to jump directly to the definition of a class or method.
- In statically-typed languages, the compiler can produce highly-optimized code because all classes, methods, and data types are specified in the code itself. The runtime can skip most of the work of figuring out exactly which method needs to be called, or whether an object is a string or a number, because the compiler figured all that out ahead of time.
Each of these three properties of object-oriented languages (particularly statically-typed object-oriented languages) is particularly valuable to large organizations developing large well-defined systems with large teams of engineers.
A large organization with a large engineering team will naturally have a lot of turnover. If, for example, the average engineer on a team of 100 remains in his position for 5 years, then the team will be bringing on an average of 20 new members each year.
It takes a great deal of time and effort for each engineer to learn the project’s code base. A good IDE can greatly reduce the amount of time it takes each new engineer to come up to speed. And a good IDE can do this because the underlying statically-typed object-oriented language requires everything to be declared explicitly and in great detail.
If you’ve worked exclusively with IDEs for the past few years, you may take for granted how much assistance they provide. Try navigating an unfamiliar C# or Java project that has a few thousand files using Notepad or Nano. This remind you of how much assistance the IDE provides.
The other great benefit of statically-typed object-oriented languages, runtime performance, is also invaluable to large organizations running large code bases. These organizations, and the applications they produce, tend to serve many users, processing large volumes of data. They have to be efficient.
Alongside these benefits, statically-typed object-oriented have some drawbacks, particularly in the realm of agility. By agility, I mean the ability for developers to change characteristics or behaviors of the system, to adapt the system to new requirements or apply it to new sets of problems.
The lack of agility in statically-typed object-oriented systems comes in part from static typing and in part from the very features that are supposed to provide abstraction and flexibility: classes and interfaces.
If you change some property of your Employee class– for instance, allowing an employee to work in two departments– you must change the department property of the class from a single instance of Department to a collection of Department. Then you must make corresponding changes to every part of your code base that touches the Employee.department property.
Though the refactoring tools of most current IDEs can assist in this process, making the required changes can still be quite time-consuming, disruptive, and difficult. And as you make the changes, you will need to rewrite existing tests and write new regression tests to make sure the system continues to behave as expected.
This is the downside of using classes and interfaces as abstractions: they are not really that abstract. In fact, they are quite specific. They define explicitly every property and every method, along with the types of every property, and parameter types and return types of every method. And once the classes are defined and start interacting with each other, they create dependencies that are very hard to undo.
While the best object-oriented architects are careful to avoid object dependencies, even many experienced object-oriented developers don’t do enough to avoid dependencies. In extreme cases, you wind up with dependency graphs like this:
Object dependencies not only make code difficult to change, they make it difficult to understand. In many object-oriented code bases, you may need to understand a dozen different classes before you can understand how a single class is instantiated, or transformed, or serialized or saved.
Object dependencies also vastly complicate the testing process. You often need to instantiate mocks several classes, and coerce each one into some particular state, just so you can test a single method in a single class.
Abstractions in Clojure
Abstractions in Clojure tend to be profoundly abstract, and therefore extremely flexible. This level of abstraction, which is generally unavailable in statically-typed object oriented languages, encourages a different way of thinking.
Unlike object-oriented languages, in which data structures and the method that operate on those structures are bound up into classes, Clojure separates data structures (the nouns) from functions (the verbs). While object-oriented languages allow and encourage developers to model custom classes to represent specific elements of a problem domain, Clojure provides two basic collection types capable of representing any elements of any problem domain: the sequence and the map.
Maps are special sequences that have keys and values (other sequences have only values). Maps are similar to Hashes in Ruby and Perl, or Dictionaries in Python.
In addition to these fundamental data types, Clojure is built upon a relatively small set of core functions. John While McCarthy’s original LISP may have theoretically required only five functions as summarized here, Clojure’s core has a few more functions than that. Still, the number is small.
In an object-oriented design, you first model the problem domain, then create custom classes to represent concepts in the problem domain. Then you create custom components or sub-systems that contain specially-designed APIs to work specifically (and often exclusively) with your custom classes.
In Clojure, you write general functions to work with general data types. To accomplish specific tasks, you compose functions– that is, you write functions consisting of other functions– and this process is fairly straight forward because of the limited number of data types. Your specific function takes a list as its parameter. It in turn calls 5 other functions, each of which takes a list as a parameter and returns a list as its output.
These two approaches are profoundly different. The object-oriented model is similar to ideographic languages like Chinese, in which every word requires a custom symbol. The functional model is similar to an alphabetic language, in which any new word can be represented by composing letters from the same alphabet.
The alphabetic model simplifies many things. It allows computers to have relatively small keyboards. It allows readers to at least know how to pronounce words they’ve never seen before. It allows writers (in languages other than English) to spell words they’ve never written before. It allows writers to create new words simply by combining the letters necessary to make the sounds.
And these new words can immediately have meaning, even when the reader has never seen them before. For example, everyone understood exactly what George W. Bush meant when he said people misunderestimate him.
But if the Chinese premiere said the same thing, how would the newspapers report it? Who would be responsible for creating the ideogram to represent a word no one had ever seen before, and how would the readers know what the new ideogram represented?
Like ideograms, classes often try to model in some detail the things they represent. For example, take a look at some of these Chinese characters, a few of which appear below:
Up or Above:
Down or Below:
The symbols for sun and bird bear some resemblance to the sun and a bird. The symbol for up shows a line going up from a baseline. The symbol for down shows a line going down from a baseline.
Alphabetic representations are more abstract. They don’t try to represent the things in the material world. Instead, they merely represent the sound of the spoken language. The letters a-p-p-l-e bear no relation to any fruit; they merely represent the sound of the fruit’s name in spoken English.
Object-oriented languages have many of the pitfalls of ideographic languages. You’ll often find a class for every concept. Understanding a complex system often means understanding thousands of classes. And understanding the classes themselves often requires understanding the though process of the person who architected the original system.
For example, the Chinese language includes a an ideogram depicting two women under a single roof. What do you suppose this means?
This is the ideogram for “trouble.” It may be obvious to the inventor of the ideogram, whose wife did not get along with his mother, but its not at all obvious to a newcomer to the language.
Similarly, the Apache HTTP Client library includes a huge number of classes, many of which you must study and understand before you can even open a simple connection to a remote host.
Object-Oriented Complexity vs. Functional Simplicity
The fundamental differences in the abstraction mechanisms of object-oriented and functional languages lead to fundamental differences in the way programmers model the problems they must solve, and the solutions they invent.
Because the primary abstraction mechanism in object-oriented languages is the class, large systems tend to include large numbers of classes. And because each class requires explicitly defined properties and methods, the process of modeling an object-oriented system is a process in which the architect moves from the specific to the general, then back to the specific.
For example, in modeling an HR system, the architect first considers what types of the data the system will be working with– Persons, Employees, JobCandidates, Departments, etc. This is a process of abstraction. Then the architect finds commonalities. Employee and JobCandidate are special instances of the more general concept of Person, so they will both derive from this base class.
Finally, the architect moves back to specifics: Employee will have these attributes and methods; JobCandidate will have this other set of attributes and methods. And at this point, before the application ever runs, what an Employee can do and what a JobCandidate can do are limited by the class definitions. What any part of the system can do with an Employee or a JobCandidate is limited by the definitions of those objects.
Contrast this to the process of modeling in a dynamically-typed functional language like Clojure. The nouns in your problem domain become maps or sequences. There is no need to fix the set of properties belonging to an Employee or a JobCandidate. You simply make them maps (a.k.a. hashes or dictionaries) and assign properties as needed.
The actions your system will need to implement– the verbs– are functions, and can work with virtually any objects, since they are all just maps. The limitations that appear in the object-oriented design as soon as it is written are not there in the functional design. The number of verbs that can operate on your nouns is not limited by the architect’s model.
This makes software written in functional languages like Clojure vastly more flexible than software written in statically-typed object-oriented languages like C++, Java and C#.
Unlike the object-oriented design process, in which the mind moves from specifics of functional requirements to generalities of the object model and back to specifics of class implementation, the fundamental abstractions available in Clojure encourage you to think in broad and general terms.
Because the language itself is based on a few fundamental data types and a small number of core functions, you start to think of all problems in terms of those few data types and those few functions. The amazing thing is that, once you get comfortable with Clojure, you find that those few data types and functions can indeed represent virtually every problem computers can solve.
And as you begin to conceive all problems in terms of a few fundamental structures and functions, you’ll find that as you re-think your design and refine your application, your code gets shorter. This process can be deeply gratifying.
Again, this process of simplification and reduction in code size is the opposite of my experience with object-oriented languages. While the occasional painful refactoring does simplify an object-oriented design, the general tendency seems to be toward writing more and more classes.
Proponents of functional languages in general, and Clojure in particular, often proclaim the benefits of having to write and maintain less code. The great benefit of a smaller code base is that it is easier to reason about what the code is actually doing.
If you have read many legal documents, you’ve probably noticed that mediocre lawyers tend to think like mediocre programmers, first considering every conceivable scenario, and then treating each one as a special case. In programming, this tendency manifests in architectures that include huge numbers of classes. In legal documents, it appears in long-winded sentences that contain a clause to address every conceivable situation.
Back in the 1980’s, the monthly statement for Citibank cardholders included a 360-word statement describing the conditions under which card holders would be considered in default. The statement included numerous clauses, each describing a complex set of conditions that would constitute default.
Even intelligent, well-educated cardholders had trouble reasoning through the 360 word statement. Citibank got so tired of answering customer questions about what constituted default that they hired a more intelligent lawyer to distill the 360 word statement to its essence:
“You are in default if you have not made your minimum monthly payment in the past 90 days.”
The 18 words above express the same thing as the 360 words they replaced. It’s easy to reason about this short, direct statement.
When Clojure programmers tout the benefit of “less code,” they are not praising the virtue of laziness (as Larry Wall did) or expressing the relief they feel at not having to type so much. They are talking about the benefits of expressing clearly and concisely what their program is doing.
Such concise expression is not necessarily easy. When a programmer first approaches a problem, in Clojure or any other language, it’s common for the first iteration of code to look something like Citibank’s 360 word definition of default. Only after much consideration and revision can the programmer reduce the original solution to something as concise and simple as the sentence above.
Rich Hickey, who created the Clojure language, has an excellent presentation called Simple Made Easy in which he describes the amount of work required to arrive a simple solution. One of the great advantages of Clojure over languages like Java and C# is that the Clojure language permits simple elegant solutions, and the Clojure way of thinking leads you toward them.
If Clojure is so great, why isn’t everyone using it?
For one thing, it’s a difficult language to learn, particularly if you have an extensive background in object-oriented development. One of the most difficult adjustments in moving from an object oriented language to Clojure is Clojure’s lack of state. You can’t simply initialize a bunch of objects and leave them hanging around for convenient use at a later time.