14 September 2021

Contrast

Java: equal parts comforting and clunky.

I recently did several implementations of the "Game of Life" kata in Clojure.

Here's all the code necesary to provide awareness of a cell's neighbors (active or not):

(defn neighbors-of [[x y]]
  (for [Y (range (dec y) (+ 2 y))
        X (range (dec x) (+ 2 x))
        :when (not= [x y] [X Y])] [X Y]))

(defn count-active-neighbors [cell grid]
  (->> cell neighbors-of set (filter grid) count))

I'm now working on a project written in Java. As I researched how to complete certain common operations in Java I noticed many usages of Java Streams, which somewhat resemble functional operations very common in Clojure. So, I thought I'd try my had at implementing "Game of Life" in Java, trying to stick to a functional paradigm as much as possible.

It's...clunky. While it was somewhat comforting to be back in a procedural/object-oriented language, it really did require a great deal of code to achieve results equivalent to the Clojure code above.

To illustrate, here's the corresponding code I wrote in Java:

import java.util.HashSet;
import java.util.Set;

public class Cell {
    private final int x;
    private final int y;

    public Cell(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public String toString() {
        return String.format("[%d %d]", getX(), getY());
    }

    public int hashCode() {
        return toString().hashCode();
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Cell)) {
            return false;
        }

        Cell other = (Cell) o;

        return getX() == other.getX() &&
                getY() == other.getY();
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public HashSet<Cell> neighbors() {
        HashSet<Cell> result = new HashSet<>();
        for (int y = getY() - 1; y <= getY() + 1; y++) {
            for (int x = getX() - 1; x <= getX() + 1; x++) {
                Cell c = new Cell(x, y);
                if (!c.equals(this))
                    result.add(c);
            }
        }
        return result;
    }

    public int countActiveNeighbors(Set<Cell> grid) {
        var neighbors = neighbors();
        neighbors.retainAll(grid);
        return neighbors.size();
    }
}

Dealing with Set and HashSet each require their own import (ugh). I had to implement equals() and toString() and hashCode() just to get all the test assertions working (ugh). I wanted the x/y fields to be final so there would be no temptation to modify them after creation, so that required the creation of 'getter' methods (ugh). Then, finally, the actual methods that implement awareness of neighbors, don't look too bad, but notice that the only way to get a set intersection between two sets is to mutate one of them (ugh).

I'm not sure I have the stamina to continue this exercise...