Java.next: Common Ground

This is Part One of a series of articles on Java.next. In Part One, I will explore the common ground shared by the Java.next languages.

I have chosen four languages which together represent "Java.next": Clojure, Groovy, JRuby, and Scala. At first glance, these languages are wildly different. Clojure is a Lisp. Groovy is the "almost Java" choice. JRuby has the beauty of Ruby, and the mindshare of Rails. Scala, unlike the others, brings the notion that we need more static typing.

As you might imagine, there is heated debate about which of these languages is best for some purpose, or best in general. Lost in the debate is the fact that these languages share a ton of common ground. They all evolved against a shared background, the Java language. Their design decisions are all influenced by what has worked well in Java, and what has failed.

In this article I will demonstrate two important points about the common ground these languages share:

  • Over the last decade of coding in object-oriented, VM-based languages, we have learned a lot about writing expressive, maintainable applications. Java.next incorporates this knowledge, enabling essence over ceremony.
  • The "essence vs. ceremony" design choices add up to a very different way of programming. The mental shift from Java to Java.next is a bigger shift than the previous shift from C/C++ to Java.

I have distilled the shared advantages of Java.next to eight points, which are explored in more detail below.

  • everything is an object
  • low-ceremony property definitions
  • expressive collections
  • functional programming
  • overriding operators
  • maintainable exception handling
  • adding methods to existing objects
  • roll-your-own constructs

Everything is an object

In Java, we live every day with the distinction between objects and primitives. This causes three practical problems:

  1. APIs must be duplicated: one method for objects, and another for primitives. Or worse, septlicated. One method for objects, and one each for different primitive types.
  2. The default (efficient, easy-to-use) numeric types have range limitations. Exceed them and your program breaks in mysterious ways.
  3. You cannot use intuitive math operators (+,-,etc.) with accurate numeric types.

In Java.next, everything is an object. You can invoke methods on all types using the same syntax.

  ; clojure
  (. 1 floatValue)
  1.0

  // groovy
  1.floatValue()
  ===> 1.0

  # ruby
  1.to_f
  => 1.0
  
  // scala
  1.floatValue
  res1: Float = 1.0

Low-ceremony property definitions

In Java, to create a property, you must define a field, a getter, a setter, and (often) a constructor, all with appropriate protection modifiers. In Java.next, you can define all of these in a single step.

  ; clojure
  (defstruct person :first-name :last-name)

  // groovy
  class Person {
      def firstName
      def lastName
  }

    # ruby
    Person = Struct.new(:first_name, :last_name)
    
    // scala
    case class Person(firstName: String, lastName: String) {}

If you need to override (or omit) a getter, setter, or constructor for a class, you can also do that, without having to spell out all boilerplate versions of the other pieces.

And that's not all. All of these languages embrace TMTOWTDI (There's More Than One Way To Do It), so there are multiple variants on the approaches shown above.

Expressive collections

Java.next provides a convenient literal syntax for the most important collections: arrays and maps. In addition, you can string together multiple operations by passing function arguments, without having to write explicit iterators or loops. For example, to find the all the squares under 100 that are also odd:

  ; clojure
  (filter (fn [x] (= 1 (rem x 2))) (map (fn [x] (* x x)) (range 10)))
  (1 9 25 49 81)
  
  // groovy
  (1..10).collect{ it*it }.findAll { it%2 == 1}
  ===> [1, 9, 25, 49, 81]

  # ruby
  (1..10).collect{ |x| x*x }.select{ |x| x%2 == 1}
  => [1, 9, 25, 49, 81]

  // scala
  (1 to 10).map(x => x*x).filter(x => x%2 == 1)
  res20: Seq.Projection[Int] = RangeMF(1, 9, 25, 49, 81)

There are similar conveniences for name/value collections, a.k.a. hashes or dictionaries.

Functional programming

The convenient collections described above are a special case of a more general idea: functional programming. Java.next supports functions as first class objects, allowing function arguments, functions that create new functions, and closures over the current scope. As a simple example, consider creating an adder function that adds some value chosen at runtime:

  ; clojure
  (defn adder [x] (fn [y] (+ x y)))

  // groovy
  adder = { add -> { val -> val + add } } 

  # ruby
  def adder(add)
    lambda { |x| x + add }
  end

  // scala
  def sum(a: Int)(b: Int) = a + b

Overriding operators

In Java, you cannot override operators. Math looks like this, for types like BigDecimal:

  // Java math
  balance.add(balance.multiply(interest));

Java.next allows you to override operators. This allows you to do create new types that feel like built-in types, e.g. you could write a ComplexNumber or RationalNumber that supports +, -, *, and /.

  ; Clojure
  (+ balance (* balance interest))

  // Groovy
  balance + (balance * interest)

  # JRuby
  balance + (balance * interest)

  // Scala (See [1])
  balance + (balance * interest)

Maintainable exception handling

Checked exceptions are a failed experiment. Java code is bloated with checked exception handling code that tends to obscure intent without improving error handling. Worse yet, checked exceptions are a maintenance headache at abstraction boundaries. (New kinds of unrecoverable failures down the dependency chain should not necessitate recompilation!)

Java.next does not require you to declare checked exceptions, or to explicitly deal with checked exceptions from other code. It is a testimony to the power of Java (the platform) that other languages are free to ignore the ugliness of checked exceptions in Java (the language).

Adding methods to existing types

In Java, you cannot add methods to existing types. This leads to absurd object-mismodeling, as developers create utility classes that defy the point of OO:

  // Java (from the Jakarta Commons)
  public class StringUtils { 
    public static boolean isBlank(String str) { 
    int strLen; 
  	if (str == null || (strLen = str.length()) == 0) { 
  	  return true; 
  	}  
  	for (int i = 0; i < strLen; i++) { 
    	if ((Character.isWhitespace(str.charAt(i)) == false)) { 
    	  return false; 
    	} 
    }
  }

In Java.next, you can add methods to existing types:

  ; Clojure
  (defmulti blank? class)
  (defmethod blank? String [s] (every? #{\space} s))
  (defmethod blank? nil [_] true)
  
  // Groovy
  String.metaClass.isBlank = {
    length() == 0 || every { Character.isWhitespace(it.charAt(0)) }
  }

  # Ruby (from Rails)
  class String 
    def blank? 
    	empty? || strip.empty? 
    end 
  end 

  // Scala
  class CharWrapper(ch: Char) {
    def isWhitespace = Character.isWhitespace(ch)
  }
  implicit def charWrapper(ch: Character) = new CharWrapper(ch)
  class BlankWrapper(s: String) {
    def isBlank = s.isEmpty || s.forall(ch => ch.isWhitespace)
  }
  implicit def stringWrapper(s: String) = new BlankWrapper(s)

Roll-your-own constructs

In Java, you have the language and the libraries. The two are clearly distinct: you can write new libraries, but you cannot add language features.

In Java.next, the line between language and libraries is blurry. You can create new constructs that work like core language features. For example, Clojure provides an and function.

; clojure
(and 1 2) => 2

But maybe your problem domain isn't so binary. You need a most function, that returns true if most of its arguments evaluate to true. Clojure doesn't have this, but you can write one:

  ; clojure
  (most 1 2) => true
  (most 1 2 nil) => true
  (most 1 nil nil) => false

The point here is not "Does my language need a 'most' conditional?" Probably not. The point is that different domains have different needs. In Java.next, the boundary between the language and the libraries is a minimized. You can adapt the language to your domain, instead of the other way around.

As another example, consider Ruby's attribute syntax:

  # Ruby
  class Account
    attr_accessor :name
    dsl_attribute :type
  end

attr_accessor is built into the language. dsl_attribute is a library method that I wrote, which allows you to omit the "=" when assigning values, e.g.

  # normal attributes
  account.name = "foo"

  # equals-free attributes
  account.type checking

Conclusions

The Java.next languages share a ton of common ground. Although I've used small isolated examples for explanation, the real power comes from using these features together. Combining all the Java.next features leads to an entirely different style of coding.

  • You do not have to code defensively, using a slew of factories, patterns, and dependency injection to keep your code testable and adaptable. Instead, you can build a minimal solution and evolve it.
  • Instead of coding in your Java.next language, you can develop internal Domain-Specific Languages (DSLs) that better match your problem domain.

In my experience, this style of coding tends to reduce the size of a codebase by an order of magnitude, while improving readability.

Many people are looking for the "next big language." The next big language is already here, but it isn't a single language. It is the collection of ideas above (plus probably some I missed) as manifested in Java.next.

Does the transition to Java.next deserve the name "big"? Absolutely. In my experience, the move from Java to Java.next is every bit as big as the previous tectonic shifts in the industry, both in learning curve and in productivity advantages once you make the transition.

As an industry, we need to reset the bar to include Java.next. Once we have, we can have a conversation about the differences in these languages. I will take up the unique aspects of the Java.next languages in future installments of this series.


Notes

  • This article is taken from the first half of the JVM Language Shootout talk that I wrote for NFJS. Check the schedule for a talk near you.
  • Suggestions for improving the code samples above are most welcome.
  • Thanks to Justin Gehtland, Jason Rudolph, Rob Sanheim, Glenn Vanderburg, and Greg Vaughn for reading an earlier draft of this article.

Footnotes

  1. The BigDecimal example does not work as I would expect on the Scala build I have (2.7.1.final). But the important point is that I could make it work by adding an implicit conversion. I am not dependent on the language designers, I can improve the language myself.

Revisions

  • 2008/08/04: fixed errata, better Clojure example for expressive collections.
  • 2008/08/12: added Rich Hickey's improved blank? example for Clojure.
Get In Touch