Creator of the Apache Tapestry web application framework and the Apache HiveMind dependency injection container. Howard has been an active member of the Java community since 1997. He specializes in all things Tapestry, including on-site Tapestry training and mentoring, but has lately been spreading out into fun new areas including functional programming (with Clojure), and NodeJS. Howard is a DZone MVB and is not an employee of DZone and has posted 79 posts at DZone. You can read more from them at their website. View Full User Profile

Crafting Code in Clojure

02.11.2013
| 3489 views |
  • submit to reddit

The other day, I was working on a little bit of code in Clojure, just touching up some exception reporting, when I was suddenly struck by one of the fundamental reasons that Clojure is so enjoyable to code in. Clojure is craftable: that is, in Clojure you have the option to craft at your code to make it more concise, easier to read, and easier to maintain. That is not the case for all, or perhaps even most, programming languages.

In my case, I was constructing an error message where I needed to convert the keys of two maps into a comma-seperated string (I don't like to say "you guessed wrong" without saying "here's what you could have said").

What I want my code to do is easily expressed as an informal recipe:

  • Extract all the keys from both maps
  • Remove any duplicates
  • Convert the keys to strings
  • Sort the strings into ascending order
  • Build and return one big string, by concatinating all the key strings, using ", " as a seperator
  • Return "<none>" if both maps are empty

If I was writing this in Java, it would look something like this:

package com.howardlewisship;
 
import java.util.*;
 
public class MapUtils {
public static String sortedKeyList(Map<?, ?> map1, Map<?, ?> map2) {
 
Set<Object> allKeys = new HashSet<Object>(map1.keySet());
allKeys.addAll(map2.keySet());
 
if (allKeys.isEmpty()) {
return "<none>";
}
 
List<String> sortableKeys = new ArrayList<String>();
 
for (Object k : allKeys) {
sortableKeys.add(k.toString());
}
 
Collections.sort(sortableKeys);
 
StringBuilder builder = new StringBuilder(100);
boolean first = true;
 
for (String s : sortableKeys) {
if (!first) {
builder.append(", ");
}
 
builder.append(s);
 
first = false;
}
 
return builder.toString();
}
}

There's enough looping and conditionals in this code (along with tip-toeing around Java Generics) that its easier to look at its test specifiction (written in Spock) to see what it is supposed to do:

import com.howardlewisship.MapUtils
import spock.lang.Specification
 
class SortedKeysSpec extends Specification {
 
def "<none> returned when both maps are empty"() {
 
expect:
MapUtils.sortedKeyList([:], [:]) == "<none>"
}
 
def "keys from both maps are merged into sorted list"() {
expect:
MapUtils.sortedKeyList([fred: true], [barney: true, wilma: true]) == "barney, fred, wilma"
}
 
def "no separator for single key"() {
expect:
MapUtils.sortedKeyList([fred: true], [:]) == "fred"
}
 
def "duplicates between maps are ignored"() {
expect:
MapUtils.sortedKeyList([fred:true], [fred: false, barney: true]) == "barney, fred"
}
 
def "keys may be other than string"() {
when:
def map1 = [:]
def map2 = [:]
 
map1[200] = true
map2[[1, 2]] = true
map2[Collection] = true
 
then:
MapUtils.sortedKeyList(map1, map2) == "200, [1, 2], interface java.util.Collection"
}
}

The first pass at a Clojure version is already simpler than the Java version ...

(ns com.howardlewisship.map-utils
(import [java.util Collection])
(use clojure.test)
(require [clojure.string :as s]))
 
(defn sorted-key-list-1
[map1 map2]
(let [all-keys (set (concat (keys map1) (keys map2)))]
(if (empty? all-keys)
"<none>"
(let [key-names (map str all-keys)
sorted-names (sort key-names)]
(s/join ", " sorted-names)))))
 
(deftest sorted-key-list
(are [map1 map2 result]
(= (sorted-key-list-1 map1 map2) result)
 
{} {} "<none>"
 
{:fred true} {:barney true, :wilma true} ":barney, :fred, :wilma"
 
{:fred true} {} ":fred"
 
{:fred true} {:fred true, :barney true} ":barney, :fred"
 
{200 true, [1, 2] true, Collection true} {} "200, [1 2], interface java.util.Collection")))
 
(run-tests)

I couldn't resist using the clojure.string/join function, rather than building the string directly (which would be slightly tedious in Clojure). In many ways, this is a lot like the Java version; we're using let to create local symbols for each step in the process in just the same way that the Java version defines local variables for each step.

However, there's room for improvement here. Let's start to craft.

For example, let's assume that both maps being empty is rare, or at least, that the cost of sorting an empty list is low (it is!). Our code becomes much more readable if we merge it into one big let:

(defn sorted-key-list-2
[map1 map2]
(let [all-keys (set (concat (keys map1) (keys map2)))
key-names (map str all-keys)
sorted-names (sort key-names)]
(if (empty? sorted-names)
"<none>"
(s/join ", " sorted-names))))

Now we're getting somewhere. I think this version makes it much more clear what is going on that the prior Clojure version, or the Java version.

However, if you've written enough code, you know one of the basic rules of all programming: names are hard. Anything that frees you from having to come up with names is generally a Good Thing. In Java, we have endless names: not just for methods and variables, but for classes and interfaces ... even packages. Long years of coding Java has made me dread naming things, because names never quite encompass what a thing does, and often become outdated as code evolves.

So, what names can we get rid of, and how? Well, if we look at the structure of our code, we can see that each step creates a value that is passed to the next expression as the final parameter. So all-keys is passed as the last parameter of the (map) expression, resulting in key-names, and then key-names is passed as the last parameter of the (sort) expression. In fact, ignoring the empty check for a moment, the sorted-names value is passed to the (s/join) expression as the last parameter as well.

This is a very important concept in Clojure; you may have heard people trying to express that you code in Clojure in terms of a "flow" of data through a series of expressions. We'll, you've just seen a very small example of this.

In fact, it is no simple coincidence that the last parameter is so important; this represents a careful and reasoned alignment of the parameters of many different functions in clojure.core and elsewhere, to ensure that flow can be passed as that final parameter, because it becomes central to the ability to combine functions and expressions together with minimal fuss.

We can use the ->> macro (pronounced "thread last") to rebuild our flow without having to come up with names for each step:
(defn sorted-key-list-3
[map1 map2]
(let [sorted-names
(->>
(set (concat (keys map1) (keys map2)))
(map str)
sort)]
(if (empty? sorted-names)
"<none>"
(s/join ", " sorted-names))))

The ->> macro juggles our expressions into an appropriate order; without it we'd have to deeply nest our expressions in an unreadable way: (sort (map str (set (concat (keys map1) (keys map2))))). Even with a short flow of expressions, that's hard to parse and interpret, so ->> is an invaluable and frequently used tool in the Clojure toolbox.

We can continue to craft; the first expression (that builds the set from the keys), can itself be broken apart into a few smaller steps. This is really to get us ready to do something a bit more dramatic:

(defn sorted-key-list-4
[map1 map2]
(let [sorted-names
(->>
(keys map1)
(concat (keys map2))
set
(map str)
sort)]
(if (empty? sorted-names)
"<none>"
(s/join ", " sorted-names))))

This is getting ever closer to our original recipe; you can more clearly see the extraction of keys from the maps before building the set (which is only used to ensure key uniqueness), before continuing on to convert keys from objects to strings, sort them, and combine the final result.

In fact, we're going to go beyond our original brief, and support any number of input maps, not just two:

(defn sorted-key-list-5
[& maps]
(let [sorted-names
(->>
(mapcat keys maps)
set
(map str)
sort)]
(if (empty? sorted-names)
"<none>"
(s/join ", " sorted-names))))

The mapcat function is like map, but expects that each invocation will create a collection; mapcat concatinates all those collections together ... just what we want to assemble a collection of all the keys of all the input maps.

At this point, we don't have much more to go ... but can we get rid of the sorted-names symbol? In fact, we can: what if part of our flow replaced the empty list with a list containing just the string "<none>"? It would look like this:

(defn replace-empty
[replacement coll]
(if (empty? coll)
replacement
coll))
 
(defn sorted-key-list-6
[& maps]
(->>
(mapcat keys maps)
set
(map str)
sort
(replace-empty ["<none>"])
(s/join ", ")))

... and that's about as far as I care to take it; a clean flow starting with the maps, and going through a series of expressions to transform those input maps into a final result. But what's really important here is just how fast and easy it is to start with an idea in Clojure and refine it from something clumsy (such as the initial too-much-like-Java version) into something elegant and surgically precise, such as the final version.

That's simply not something you can do in less expressive languages such as Java. For example, Tapestry certainly does quite a number of wonderful things, and supports some very concise and elegant code (especially in green code) ... but that is the result of organizing large amounts of code in service of specific goals. We're talking tons of interfaces, a complete Inversion-Of-Control container, and runtime bytecode manipulation to support that level of conciseness. That's the hallmark of a quite consequential framework.

That isn't crafting code; that's a big engineering effort. It isn't local and invisible, it tends to be global and intrusive.

In Java, your only approach to simplifying code in one place is build up a lot of complexity somewhere else.

That is simply not the case in Clojure; by adopting, leveraging, and extending the wonderful patterns already present in the language and its carefully designed standard library, you can reach a high level of readability. You are no longer coding to make the compiler happy, you are in control, because the Clojure languge gives you the tools you need to be in control. And that can be intoxicating.

The source code for this blog post is available on GitHub.









 

Published at DZone with permission of Howard Lewis Ship, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

matt inger replied on Mon, 2013/02/11 - 8:30am

I'm not going to examine the merits of clojure here, as i'm not nearly familiar enough with it, but there are some issues with the java code you posted.

1.  You can easily remove a bunch of code by using the proper set class which deals with sorting order (such as TreeSet).

2.  You're not taking advantage of existing libraries like commons-lang

I can replace your entire method content with 4 lines of code, assuming you have the commons-lang library in your classpath:


        SortedSet<Object> sortableKeys = new TreeSet<Object>(map1.keySet());
        sortableKeys.addAll(map2.keySet());
        String keys = StringUtils.join(sortableKeys, ",");
        return StringUtils.defaultIfEmpty(keys, "<none>");

view source

You can craft your code to be concise in most languages by proper use of utility classes, etc...  This is a prime example of writing raw java code where you could have included 1 library (in particular 1 class) and made your code much more readable and concise.

Howard Lewis Ship replied on Mon, 2013/02/11 - 8:05pm

Matt,

It's true that on a very small example like this one there are ways in any language to "golf" the code (a term I learned from the comments on the original blog post). There is a difference, though, in that the code I created in my example relied only on the standard Clojure library, and not any external libraries.

In many of the real world applications of this kind of "flow" coding, there isn't anything off the shelf to reach for ... but in Clojure, the result is still concise.

Java has a been a fertile ground for libraries and frameworks, but also fertile ground for instance dependency management and huge amounts of "glue" code to make things work together. The Clojure community is focused on providing libraries of truly reusable and context-free code ... and the nature of the Clojure language and runtime makes that a reasonable goal.  That was a large part of what I was attempting to capture.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.