3 minute read

Another day, another assignment to overthink. Last time, we used a recursive flatMap to generate a stream of files with a given parent directory. In today’s episode, we want to replace characters of a string with more readable strings, the former being a depth first search traversal, and the latter the console output my professor reads. Once again, our solution will somehow manage to increase in both quality and crappiness as we try to get Java 8 to play nice.

To set the scene, this project was the classic “find a path through a two dimensional maze of characters” problem. The instructions said nothing about finding the shortest path, so I solved it with a simple depth first search. The path is stored in the form of a string of four unique characters, one for each direction we stepped towards on the path. This path is useful in other parts of the code, but reading it is kinda hard, so we expand it:

// path = "NWNNENWWSZ"
System.out.println( path
  .replace("S", "down, ")
  .replace("E", "right, ")
  .replace("N", "up, ")
  .replace("W", "left, ")
  .replace("Z", "exit.")
); // "up, left, up, up, right, up, left, left, down, exit."

Nothing special, which is a problem. Look at all of those replaces. Repetitive, borderline redundant replaces. Disgusting. So disgusting that I took the worst possible approach to replacing the replaces, at least at first.

Before I make those mistakes, I should mention that we declared some final variables to make making mistakes more manageable:

final char SOUTH = 'S', EAST = 'E', NORTH = 'N', WEST = 'W';
final String compass = new String(new char[] {SOUTH, EAST, NORTH, WEST});
// no new String(char...) ;-;

Alas, Java has no convenient pair or tuple types. If it did, I could probably just “zip” the array of direction characters with the array of direction words and pass each resulting tuple to replace. The laziest way to imitate this pattern in Java is with parallel arrays, or in this case, a parallel string and string array:

String[] names = {"down", "right", "up", "left", "exit"};
System.out.println( IntStream.range(0, names.length)
  .boxed()
  .reduce( path,
    (s,i) -> s.replace(
      Character.toString((compass+EXIT).charAt(i)),
      names[i] + (i+1==names.length ? "." : ", ")
    ),
    String::concat
  )
);

Bleh. The idea here was to see the path as the identity of a reduction, and each replacement as another step in the accumulation. The problem is that we are dealing with three different types of data; the String path, the char directions, and the int indices of our arrays. Java is very picky about its reductions, and wants the input and output data types to be the same:

int reduce(int identity, IntBinaryOperator op)
T reduce(T identity, BinaryOperator<T> accumulator)

so, we had to box and use the partial reduction meant for parallel streams:

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

which left us with a combiner argument that doesn’t really do anything, and a silly ternary expression to split and join each word. That seems like a lot of work for comma separation…

wait a minute…

comma, separation

System.out.println( path.chars()
  .mapToObj(i -> Character.toString((char)i))
  .map((compass + "E")::indexOf)
  .map(i -> names[i])
  .collect(Collectors.joining(", "))
  + "."
);

Right, that seems pretty obvious in hindsight. I was so caught up on mimicking the behavior of my first method with a reduction, I forgot the whole point of functional programming was to manipulate data, and I completely forgot about Collectors.joining, just chillin in java.util.stream waiting to solve all my problems.

Of course, pretty collectors don’t exempt us from the Java tax. Today, we paid in the form of i -> Character.toString((char)i) and abusing indexOf to get from a String to a char to an int and back again, functioning as a tiny, sad map. If our traversal was stored as a collection of int indices, or even cheesier, a String of indices as char digits, we could get away with only using one map. But c’mon, using the + operator to recursively accumulate the path is way too slick to pass up!


This post came up a little lacking. That is because I excluded two of my other methods that were so misguided, I decided they wouldn’t be helpful to share, even as a learning experience. (hint: they involved StringBuffer and collect.) Nonetheless, I think I finally discovered the ultimate lesson I can learn from a maze project: that shortest path to your goal doesn’t always lie straight ahead. In my next post, I’ll show that it lies beneath .parallel().

Comments