August 23, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Clojure: Immutability at the Language Level

  • April 2, 2010
  • By Michael Fogus, Chris Houser
  • Send Email »
  • More Articles »

Thinking in Maps -- A Fluent Builder for Chess Moves

People have said that Java is an exceptionally verbose programming language. Although this may be true when compared to the Lisp family of languages, there has been considerable mindshare devoted to devising ways to mitigate Java's verbosity. One popular technique is known as the fluent builder (based on the original concept "Fluent Interface," coined by Martin Fowler) and can be summed up as the chaining of Java methods to form a more readable, and agile, instance construction technique. In this section we'll show a simple example of a fluent builder supporting the construction of chess-move descriptions. We'll then explain how such a technique is unneeded within Clojure and present an alternative approach that is simpler, more concise, and more extensible. We'll leverage Clojure's maps in the final solution, illustrating that Java's class-based paradigm is counter to Clojure's basic principles -- and often overkill for Java programs.

Imagine that we wish to represent a chess move in Java. Typically, the first approach would be to identify all of the component parts of our Move class including from and to squares, a flag indicating whether the move is a castling move, and perhaps also the desired promotion piece if applicable. Other possible elements might be applicable, but in order to constrain the problem, we'll limit our idea of a Move to those elements listed here. The next step would be to create a simple class with its properties and a set of constructors, each taking some combination of the expected properties. We'd then generate a set of accessors for the properties, but not their corresponding mutators, because it's probably best for the move instances to be immutable. Having created this simple class and rolled it out to the customers of our chess-move API, we begin to notice that our users are sending into the constructor the to string before the from string, which is sometimes placed after the promotion string, and so on. But thanks to a design pattern called a fluent builder, we can make the chess-move API simpler to read, unambiguous, and operation order independent. After some months of intense design and weeks of development and testing, we release the following elided chess-move class:

public class FluentMove {
    String from, to, promotion = "";
    boolean castlep;

    public static MoveBuilder desc() { return new MoveBuilder(); }

    public String toString() {
        return "Move " + from +
           " to " + to +
           (castlep ? " castle" : "") +
           (promotion.length() != 0 ? " promote to " + promotion : "");
    }

    public static final class MoveBuilder {
        FluentMove move = new FluentMove();

        public MoveBuilder from(String from) {
            move.from = from; return this;
        }

        public MoveBuilder to(String to) {
            move.to = to; return this;
        }

        public MoveBuilder castle() {
            move.castlep = true; return this;
        }

        public MoveBuilder promoteTo(String promotion) {
            move.promotion = promotion; return this;
        }

        public FluentMove build() { return move; }
    }
}


Obviously for brevity's sake, our code has a lot of holes, such as missing checks for fence-posting errors, null, empty strings, and invariants; however, it does allow us to illustrate that the code provides a fluent builder given the following main method:

public static void main(String[] args) {
    FluentMove move = FluentMove.desc()
        .from("e2")
        .to("e4").build();

    System.out.println(move);

    move = FluentMove.desc()
        .from("a1")
        .to("c1")
        .castle().build();

    System.out.println(move);

    move = FluentMove.desc()
        .from("a7")
        .to("a8")
        .promoteTo("Q").build();

    System.out.println(move);
}

/* prints the following:
    Move e2 to e4
    Move a1 to c1 castle
    Move a7 to a8 promote to Q
*/


Our constructor ambiguities have disappeared with the only trade-off being a slight increase in complexity of the implementation and the breaking of the common Java getter/setter idioms, both of which we're willing to deal with. But if we had started our chess-move API as a Clojure project, the code would likely be a different experience for the end user.

A Clojure Chess Move

To start building our chess-move structure for Clojure, we may make a common assumption that we need a Move class for representation. But as we have been talking about throughout this article, Clojure provides a core set of composite data types, and as you can guess from the title of this section, its map type is a perfect candidate for our move representation.

{:from "e7", :to "e8", :castle? false, :promotion Q}


Simple, no?

Coming from a background in object-oriented methodologies, we have a tendency to view problems through the lens of cumbersome class hierarchies. But Clojure prefers simplification, providing a set of composite types perfect for representing most categories of problems typically handled by class systems. If you take a step back and think about the class hierarchies that you've built or dealt with in the past, how many of the constituent classes could have been replaced with a simple map? If your past projects are anything like ours, it's likely that the number is significant. In a language like Java, it's common to represent every entity as a class, and to do otherwise is either not efficiently supported, non-idiomatic, or outright taboo. But to be a good Clojure citizen, it's advisable to attempt to leverage its composite types for one simple reason: Existing functions, built on a sequence abstraction, work.

(defn build-move [& pieces]
  (apply hash-map pieces))

(build-move
  :from "e7"
  :to "e8"
  :promotion Q)

;=> {:from "e2", :to "e4", :promotion Q}


In two lines, we've effectively replaced the previous Java implementation with an analogous yet more flexible representation. The term domain-specific language (DSL) is often thrown around to describe code like build-move, but to Clojure (and Lisps in general) the line between DSL and API is blurred. This function makes a lot of assumptions about the form of the move pieces supplied, but its implications can't be denied. In our original FluentMove class we required a cornucopia of code in order to ensure the API was agnostic of the ordering of move elements; using a map we get that for free. In addition, FluentMove, although relatively concise, was still bound by fundamental Java syntactical and semantic constraints. The Clojure build-move function on the other hand was artificially constrained as requiring alternating key/value pairs for brevity's sake. If we'd instead written build-move as a macro, then there'd be virtually no limit to the form that our move description might have taken, leaning closer to the classic notion of a DSL. Finally, as author Rich Hickey himself proclaimed, any new class in general is itself an island, unusable by any existing code written by anyone, anywhere. So our point is this: Consider throwing the baby out with the bath water.

Separation of Concerns

Before we end this article we'd like to address an issue with both implementations that until now we've been ignoring: verification. Both FluentMove and build-move make enormous assumptions about the form of the data supplied to them and do no validation of the input. FluentMove object-oriented principles dictate that the validation of a well-formed move (not a legal move, mind you) should be determined by the class itself. There are a number of problems with this approach, the most obvious being that in order for FluentMove to determine if it's a well-formed move, it needs some information about the rules of chess; for example, a move can't be both a castle and a piece promotion. We can certainly rewrite FluentMove to throw an exception to prevent such a case from being entered, but the root problem still remains: FluentMove instances are too smart. Conversely, you could take the opposite position and require the client to check the validity of the moves, but if they could do that in the first place, then they likely wouldn't need your code at all. Perhaps you don't see this as a problem, but if we were to extend our API to include other aspects of the game of chess, then we'd find that using this approach requires that little bits of overlapping chess rules need to be scattered throughout the class hierarchy. What we wanted from the start is for our move representation to be a value, devoid of associated validation behavior.

By viewing the move structure as a simple value map, our Clojure code provides some freedom in the implementation of a total solution. That is, we decouple the value and its validation behavior, allowing for the latter to be composed from numerous functions, each swapped in and out as necessary.

We've covered the basics of Clojure maps in this section, including common usage and construction techniques. Clojure maps, minus a couple implementation details, should not be surprising to anyone. It will take some time to grow accustomed to dealing with immutable maps, but in time even this nuance will become second nature.

Clojure favors simplicity in the face of growing software complexity. If our problems are easy solved by sequential and collection abstractions, then those exact abstractions should be used. Many of the problems of software can indeed be modeled on such simple abstractions, yet we continue to build monolithic class hierarchies in attempts to deceive ourselves into believing that we're mirroring the real world -- whatever that means. Perhaps it's time to realize that we no longer need to layer our self-imposed complexities on top of software solutions that are already inherently complex. Not only does Clojure provide the sequential, set, and map types useful for pulling ourselves from the doldrums of software complexity, but it's also optimized for dealing with them.

About the Authors

Michael Fogus is software developer with experience in distributed simulation, machine vision, and expert systems construction. He's actively involved in the Clojure and Scala communities.

Chris Houser is a primary contributor to Clojure and has implemented several features for the language.





Page 5 of 5



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel