Simplifying the AST of Set Manipulations via Foreach and Mocks

In order to work on objects inside collections in Java 8 it is common to use lambdas that do work on these items. Multiple lambdas can be applied to a collection in a sequential code style where the results of a lambda is passed to the next lambda. Depending on the used implementation not all results of a lambda has to be processed before these can be passed to the next one.

    d:todo
    • Demonstration

I experimented with an alternative approach for cases when the downsides reduce the legibility significantly. The basic idea is to take a collection and create a representative object. This object has the same type as an element of the collection.

One can interact with it as it was a single element of that collection. However, every interaction with that object is also applied onto all objects of the set in question. This object is not suited for retrieving information from the objects directly.

package net.splitcells.dem.merger.basic.experimental;

import static org.mockito.Mockito.withSettings;
import static org.powermock.api.mockito.PowerMockito.mock;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.IntStream;

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import lombok.val;

/**
 * TODOC
 *
 */
public class ForEach {

	/**
	 *
	 * TODO Better configuration
	 */
	public static boolean IS_ONLY_STUBBING = new Boolean(
			System.getProperty("net.splitcells.den.merger.basic.experimental.ForEach.stubOnly", "true"));

	/**
	 * TODOC
	 *
	 * FIXME Empty collections are currently not supported:
	 * https://coderanch.com/t/383648/java/java-reflection-element-type-List
	 */
	@SuppressWarnings("unchecked")
	public static <T> T forEach(Collection<T> arg) {
		if (arg.isEmpty()) {
			throw new UnsupportedOperationException();
		}
		if (IS_ONLY_STUBBING) {
			return (T) mock(arg.iterator().next().getClass(),
					withSettings().stubOnly().defaultAnswer(interceptor(arg)));
		}
		return (T) mock(arg.iterator().next().getClass(), interceptor(arg));
	}

	/**
	 * TODOC
	 *
	 * TEST
	 */
	private static <T> Answer<Object> interceptor(Collection<T> arg) {
		return new Answer<Object>() {
			@Override
			public Object answer(InvocationOnMock invocation) throws Throwable {
				List<Object> rVal = new ArrayList<>(arg.size());
				for (T e : arg) {
					try {
						val method = e.getClass().getMethod(invocation.getMethod().getName(),
								convert(invocation.getArguments(), i -> i.getClass(),
										new Class<?>[invocation.getArguments().length]));
						Object rInvocation = method.invoke(e, invocation.getArguments());
						if (rInvocation != null) {
							rVal.add(rInvocation);
						}
					} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
						throw new RuntimeException(ex);
					}
				}
				if (rVal.isEmpty()) {
					return null;
				}
				return forEach(rVal);
			}

		};
	}

	/**
	 * TEST
	 *
	 * TODO Move to not experimental part.
	 */
	private static <A, R> R[] convert(A[] from, Function<A, R> converter, R[] to) {
		assert from.length == to.length;
		IntStream.range(0, from.length).forEach(i -> to[i] = converter.apply(from[i]));
		return to;
	}

}

When using this construct, 2 things have to be considered.

    d:todo
    • The current implementation currently only works on non empty collections in order to extract the type of the collection's elements.
Although Java does indeed have type erasure, one can extract the generic parameter types of empty collections under most. 
    d:todo
    • Secondly, methods with primitive arguments are currently not supported which can be seen in the
    • quote
      • testMethodsNotReturnThisViaNonPrimitiveParameters
    • Test.

package net.splitcells.dem.merger.basic.experimental;

import static java.util.Arrays.asList;
import static net.splitcells.dem.merger.basic.experimental.ForEach.forEach;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import lombok.val;

/**
 * TODO Benchmark the effects that ForEach.IS_ONLY_STUBBING has on memory
 * consumption and runtime performance.
 *
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({ ForEach.class, ForEachTest.class })
public class ForEachTest {

	private static final String RANDOM_STRING = "RANDOM_STRING";
	private static final String RANDOM_STRING_CONCATINATION = RANDOM_STRING + RANDOM_STRING;

	/**
	 * TODO File bug ticket for Powermockito/mockito: If the methods
	 * testStaticOnlyStubOption and testStaticOnlyStubOption2 have the same name
	 * than "java.lang.IllegalArgumentException: wrong number of arguments" is
	 * thrown at attr
	 * org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.runTestMethod(PowerMockJUnit44RunnerDelegateImpl.java:326).
	 */
	@Test
	public void testStaticOnlyStubOption() {
		testThatForEachReturnsMock2(true);
		testThatForEachReturnsMock2(false);
	}

	public void testThatForEachReturnsMock2(boolean stubOnly) {
		ForEach.IS_ONLY_STUBBING = stubOnly;
		// By default a mock returns "" then toString() is invoked on that mock.
		assertThat(ForEach.forEach(asList(new StringBuffer(), new StringBuffer())).toString()).isEmpty();
	}

	/**
	 * FIXME Primitive parameter of get is causing problems.
	 */
	@Test
	public void testPrimitiveParameter() {
		List<List<StringBuffer>> testData = new ArrayList<>(asList(new ArrayList<>(), new ArrayList<>()));
		testData.get(0).add(new StringBuffer(RANDOM_STRING));
		testData.get(1).add(new StringBuffer());
		forEach(testData).get(0).append(RANDOM_STRING);
		assertThat(testData.get(0).toString()).isEqualTo(RANDOM_STRING_CONCATINATION);
		assertThat(testData.get(1).toString()).isEqualTo(RANDOM_STRING);
	}

	/**
	 * An {@link List} implementation that has get method with no primitive
	 * parameters.
	 */
	public static class ListI<T> extends ArrayList<T> {
		public ListI() {
		}

		public ListI(List<T> values) {
			super(values);
		}

		public T getValue(Integer index) {
			return get(index);
		}
	}

	@Test
	public void testMethodsNotReturnThisViaNonPrimitiveParameters() {
		ListI<ListI<StringBuffer>> testData = new ListI<>(asList(new ListI<>(), new ListI<>()));
		testData.get(0).add(new StringBuffer(RANDOM_STRING));
		testData.get(1).add(new StringBuffer());
		forEach(testData).getValue(0).append(RANDOM_STRING);
		assertThat(testData.getValue(0).getValue(0).toString()).isEqualTo(RANDOM_STRING_CONCATINATION);
		assertThat(testData.getValue(0).getValue(0).toString()).isEqualTo(RANDOM_STRING);
	}

	private static void appendRandomString(StringBuffer arg) {
		arg.append(RANDOM_STRING);
	}

	private static Consumer<StringBuffer> appendString(String string) {
		return i -> i.append(string);
	}

	/**
	 * TODO Use an alternative to multiple string concatenation for better
	 * demonstration.
	 *
	 * This test show cases alternatives. Note that this is not about the actual
	 * StringBuffer operations.
	 */
	@Test
	public void testStandardLibraryClass() {
		val testSubjects = new ArrayList<>(asList(new StringBuffer(), new StringBuffer()));
		val controlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer()));
		val anotherControlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer()));
		val anotherOneControlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer()));
		{
			forEach(testSubjects).append(RANDOM_STRING).append(RANDOM_STRING);
			controlGroup.forEach(i -> i.append(RANDOM_STRING).append(RANDOM_STRING));
			{
				anotherControlGroup.forEach(ForEachTest::appendRandomString);
				anotherControlGroup.forEach(ForEachTest::appendRandomString);
			}
			{
				anotherOneControlGroup.forEach(appendString(RANDOM_STRING));
				anotherOneControlGroup.forEach(appendString(RANDOM_STRING));
			}
		}
		controlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION));
		testSubjects.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION));
		anotherControlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION));
		anotherOneControlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION));
	}
}

One major downside of this method is probably its performance.

    d:todo
    • This is currently not benchmarked or profiled.
This could be compensated via compiler optimizations or preprocessing using code replacement.

Another downside of the currently implemented approach is that actions that are applied to a set of objects are only marked by a single entry point for each iteration level. This may especially reduce legibility when processing of collections by static methods is not that common.

Also, methods to manipulate single objects are used to manipulate a range of objects. This may come unexpected to the code reader.

At last the implemented approach does not work well for nested iterations because the static function is not an attribute of the collections types. Every nested usage of ForEach causes an additional nesting in the AST of the source code. This would not be the case if ForEach would be an attribute of the collections. In order to further increase the legibility of the code one could add an optional String and/or Class argument in order to identify the objects that are currently iterated. This argument would only be used for debugging and logging purposes. For normal execution this parameter would be ignored. With these improvements the values of a matrix could be incremented this way: matrix.forEach("row").forEach("column").add(1) .

Note that the additional parenthesis could be omitted in alternative languages. An imaginary object oriented language that uses round brackets for String constants and square brackets for lists could allow following syntax: matrix.forEach(row).forEach(column).add[1] .

An imaginary language could further remove syntax noise by omitting dots in places where it is clear that an attribute is accessed: matrix.forEach(row)forEach(column)add[1]

This style can create more readable and fluent code under some circumstances. It requires relatively much development resource in order to be implement effectively and efficiently. Additionally, it may have disadvantages if not used carefully and may be unexpected to new but experienced programmers.

In short a tool with high initial costs, that may easily be used in a confusing way and improves the code only a little. It also compensates an imperfect (but not bad) Stream API that could be enhanced in later versions of Java.

This approach may have merit on the whole but seems to solve a currently unimportant problem with an high amount of resources.