Java, Groovy, Clojure

2022 04 01 head

Professional software developers shall master their primary technology stack and programming language.

I am a proficient and experienced programmer in Java.

I have written production code in Java for the last twenty years.

I decided to study the language in depth and acquired various Java developer certifications.

I truly love the modern Java language and the available development environments.

I want to understand the strengths and weaknesses of Java.

Knowing the weaknesses of my technology stack empowers me to select alternative design approaches to mitigate the drawbacks.

An exploring approach is to learn more about Groovy and Clojure. I want to understand the various strengths of different programming languages. I still want to stay in the Java ecosystem to better apply my learnings.

Groovy and Clojure are available on the Java virtual machine and have good interoperability with it. One major interest is the functional programming approach [1] and how it impacts design and development activities.

Clojure

2022 04 01 clojure

Clojure is a functional programming language and a Lisp(1) dialect [2]. The language compiles to and runs on the JVM and interfaces nicely with Java and API libraries.

It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures.

— Alan J. Perlis

Clojure is a Lisp dialect not constrained by backward compatibility.

  • Extends the code-as-data paradigm to maps, sets, and vectors

  • Defaults to immutability

  • Core data structures are extensible abstractions

  • Provides approaches to concurrent programming

  • Embraces the JVM platform

Expressions and Statements

Expressions are as expected in a programming language.

The and and or operators provide shortcut evaluation. Beware that they are implemented as macros and do not always behave as functions. You will realize it when you try to pass them as function parameters.

; binds a name to a value. If value is an expression, it is first evaluated.
(def <name> <value>)

; binds a name to a function.
(defn <name>[{<parameter} ] (<expression>))

; else is optional, if not present the expression returns null in the false case
(if (<condition>) (<true-expression>) (<false-expression))

; conceptually when is an if with only a true path
(when (<condition>) (<true-expression>))

; conceptually when not is an if with only a false path
(when-not (<condition>) (<false-expression>))

; cond process on condition after another, the expression of the first condition being true is executed, and the expression completes.
(cond
  (<condition-1) (expression-1)
  (condition-2) (expression-2)
  :else (expression-else))

(case <value>
 <value-1> <expression>
 <value-2> <expression>
 <expression-else>)

(loop [] (<expression-loop>))

(do {<expression>})

The truth axioms in Clojure are a refreshing view of the programming world. The numerical tower and implicit type conversions are well-thought of and minimize boilerplate code.

Languages with implicit conversion function support have similar quality. Sadly, Java does not support an extensible numerical tower or implicit type conversions. The dreadful way to use BigInteger, BigDecimal, and the missing complex type are proofs.

Functions and Closures

Functions are a key construct for a functional language. You can define functions and closures using:

(def <name> (fn [] (<expressions)))

(defn name [] (<expression>))

(let [{<name> <expression}] (<expression>))

letfn[{(fn <name> [<parameters>] (<expression>))}] (<expression>)

Higher-order Functions and Sequences

Sequence abstraction permeates the design and API of Clojure. Functional programming and higher order functions emphasize the design of functions working on generic data structures.

(filter predicate collection)    ; (1)

(map f)(map f collection)(map f c1 c2)(map f c1 c2 c3)(map f c1 c2 c3 & collections) ; (2)

(reduce f collection)(reduce f value collection) ; (3)

(apply f args)(apply f x args)(apply f x y args) (apply f a b c d & arguments) ; (4)
1 Returns a lazy sequence of the items in coll for which (predicate item) returns logical true. predicate must be free of side effects. Returns a transducer when no collection is provided.
2 Returns a lazy sequence consisting of the result of applying f to the set of first items of each collection, followed by applying f to the set of second items in each collection, until any one of the collections is exhausted. Any remaining items in other collections are ignored. Function f should accept number-of-collections arguments.
3 The function f should have two arguments. If value is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If collection contains no items, f must accept no arguments as well and reduce returns the result of calling f with no arguments. If a collection has only 1 item, it is returned and f is not called. If value is supplied, returns the result of applying f to val and the first item in the collection, then applying f to that result and the 2nd item, etc. If a collection contains no items, returns value and f is not called.
4 Applies fn f to the argument list formed by prepending intervening arguments to args.

The powerful advantage of dynamic typed language is that algorithms just work with various parameters. As long as the parameters support the required functions, there is no need to provide a specific interface.

Recursion, Tail Recursion, Currying, Memoization, Destructuring

Recursion is implemented as expected. Recursion is preferred over loops.

You can explicitly optimize tail recursion with the operator recur. [1].

Currying approach is the partial operator. The operator maps a set of parameters to value and returns a function which arity is the number of free parameters.

Memoization is provided with the memoize operator wrapping the function in need of. The documentation is very shallow about the costs of memoization and the behavior of the cache. No functions are provided to influence or clear the cached data.

Destructuring is well-supported for sequences, structures, and in keyword arguments.

Thoughts

Clojure provides type hints to help the compiler to find the correct method. This feature is necessary to cleanly interface with Java and support edge cases.

Clojure has a spec library to express constraints on the parameters and return types. This approach allows you to define a dynamic type system over your abstractions. You are in charge of maintaining and verifying the coherence of this type system.

I prefer to delegate such verification and validation to the compiler. I would postulate that if your type system grows in complexity, it is type to reflect and start using object-orientation and object modelization.

Java

2022 04 01 java

Java is the work horse for implementing enterprise and departmental software solutions. The language and libraries are regularly improved and a new version is currently delivered every six months [3]. Early adaptor distributions are provided to smooth the migration of your code.

Too many Java developers did not realize that their programming language is evolving. They still write archaic and plainly suboptimal code instead of using the new syntax and constructs.

Expressions and Statements

The newer and rediscovered features are:

(<boolean-expression>) ? true-expression : false-expression (1)

switch value  {    (2)
    case null -> expression;
    case value instanceof Type && boolean-condition -> expression;
    case value instanceof Type -> expression;
    default -> expression;
}
1 The ternary operator always provided a conditional expression in the Java language.
2 The switch expression returns a value upon completion.

A better idiom for equals without any conditional statement is now recommended:

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof MyType o) && super.equals(o) && Objects.equals(someValue(), o.someValue());
    }

Functions and Lambdas

Lambda expressions are well-supported in Java. The java.util.function package provides the expected abstractions. Function composition is supported through the provided abstractions.

Transparent support of functions as a first-class citizen is still not completed. The concise method bodies proposal is still not implemented in the year 2022.

Higher-order Functions and Streams

Lambdas and streams were introduced in Java 8 and released in March 2014. The newer versions of Java have refined the abstractions and added methods to simplify frequent use cases.

The stream library is a well-implemented approach to manipulating sequences with higher-order functions.

Recursion, Tail Recursion, Currying, Memoization, Destructuring

Recursion is well-supported in Java. The environment still does not support tail recursion optimization. I almost feel sorry that a modern programming language is missing such a well-known optimization. The Java community somewhat promised us that in the future we could get this optimization through the project Loom.

Currying is cumbersome to implement in Java.

Memoization and destructuring are currently not supported.

Thoughts

Java roots are object-orientation and mutability.

Lambda and stream features are the first serious effort to support functional programming idioms. The approach is well-designed and powerful enough to handle all algorithms manipulating sequences and collections.

Functions and expressions are still not handled as first-class citizens. The current improvements in the switch expression acknowledge the cliff. These improvements also slowly empower us to write more functional code.

Groovy

2022 04 01 groovy

Of course, Groovy is not a perfect tool for every application. Great in script-like, Groovy is not necessarily equally useful in normal, production coding. While dynamic typing gives you a productivity boost, it slows down refactoring afterward when writing code. That is a huge drawback if you have dozens of classes in a project tangled by dependencies.

Expressions and Statements

One cool feature of Groovy is the support of operators as syntactic sugar. I would welcome this feature in Java. I dream of writing legible expressions with BigInt or BigDecimal types. The current method call approach destroys the legibility of the source code.

assert 3.2 == 1.2G + 2G   (1)
assert 10 == 10.1g - 0.1G
1 To create a BigDecimal, we can use the G suffix.

Functions and Lambdas

Closures are built-in abstractions.

Higher-order Functions and Sequences

Sequences and higher-order functions are built-in constructs. Groovy syntax is certainly groovy and less verbose than Java. But Groovy is not functional style-friendly.

Recursion, Tail Recursion, Currying, Memoization

Functional programming prefers recursion to iteration. Groovy provides a tail recursion optimization mechanism the programmer has to explicitly call. The approach eliminates stack overflow exceptions. The performance of the generated code is still below regular recursive Java solutions.

@TailRecursive  (1)
static BigInteger factorial(int number, BigInteger acc = 1) {
    if (number == 1) {
        return acc
    }
    return factorial(number - 1, acc.multiply(BigInteger.valueOf(number)))
}

def last (2)
last = { it.size() == 1 ? it.head() : last.trampoline(it.tail()) }

last = last.trampoline()
1 The annotation is used for tail-recursion optimization of methods.
2 The trampoline operator is used for tail-recursion optimization of closures. The transformation avoids stack overflow but makes the code slightly slower.

Memoization is also supported through an operator.

def fib = { n ->
    if(n == 0) 0
    else if(n == 1) 1
    else fib(n-1) + fib(n-2)
}.memoize()

Thoughts

Clojure is brilliant in the support of dynamic dispatch at runtime. The refined design of their collections is a proof of their support for functional approaches. I am not convinced that dynamic inheritance and protocols promote the maintainability of software products.

Java is slowly moving to improve their support:

  • Switch expressions are now more functional. In Spring 2022, deconstruction of records, objects, and arrays is still not available.

  • First class citizen status for functions is still ongoing. You still do not define the implementation of a virtual method using the lambda notation or a method reference. I agree it is a detail. But it is a signal if you want to truly support functional approaches.

  • Nice is the effort to improve the collections. The sequence interface would improve the legibility of the standard collections. It is still a hack that an unmodifiable collection throws an exception if your code calls a modifier. It is time to introduce an unmodifiable interface for the various collections.

Groovy is awesome in how operator support enhances the legibility of the source code. The implementation is syntactic sugar.

Java libraries are the best of breed. Backward compatibility and static type checks are immensely helpful for enterprise applications developed over decades. You do not want to throw away such a huge investment.

I like properties and the elegance of grouping the declaration of the field with the methods. I am still not certain it makes the code quite more legible or maintainable.

If you want to write a functional code with immutable objects, the availability of a with construct is a huge improvement.

Functional programming with sequences is well-supported in all three environments. Clojure has the edge. The language is dynamically typed and provides sequence handling for all collections.

Java is a statically type language. You need first to convert a collection into a stream. Easy to do, but you have to write the code.

And as an engineer, I truly acknowledge the performance edge of Java against Clojure or Groovy. Newer approaches based on GraalVM should close the performance gap in the future.

Recommendations for A Java Developer

Java 8 and functional features were introduced in March 2014. Every Java programmer should now be a functional programming expert. Here are some idioms you should use:

  • Embrace functional programming and use the stream approach for all collection traversals.

  • Stop using iterations to process sequences. Please use streams. Declare your intent, do not describe the implementation.

  • Return Optional<T> or an empty list Collections.emptyList(). Please do not return null values.

  • Create predicates using java.util.function.predicate<T> instead of complex boolean conditions.

  • Learn to compose functions through the helper methods of the java.util.function package.

  • Use the switch expression and the ternary operator ?:. Please avoid if and switch statements.

  • Throw only unchecked exceptions. Stop creating and throwing checked exceptions. Beware that stream and lambda expressions cannot handle checked exceptions.

  • Embrace immutability and the record construct.

  • Understand deconstruction patterns and how you could apply them in our code.

References

[1] N. Ford, Functional Thinking: Paradigm Over Syntax. O’Reilly Media [Online]. Available: https://www.amazon.com/dp/B00LEX6SP8

[2] S. D. Halloway, Programming Clojure. 2012 [Online]. Available: https://www.amazon.com/dp/B07BN4C92X

[3] J. Boyarsky and S. Selikoff, OCP Oracle Certified Professional Java SE 17 Study Guide. Wiley & Sons, Limited, John, 2022 [Online]. Available: https://www.amazon.com/dp/B09WJP11JL


1. The trampoline operator is available for indirect recursion cases.