Browse Source

Enhance Either to support functional programming with map, unify, and consume behaviors.

Sam Jaffe 7 years ago
parent
commit
5db6eeb409

+ 1 - 1
pom.xml

@@ -3,7 +3,6 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.leumasjaffe</groupId>
 	<artifactId>container</artifactId>
-	<version>0.1</version>
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -114,4 +113,5 @@
       <type>maven-plugin</type>
     </dependency>
 	</dependencies>
+	<version>0.2</version>
 </project>

+ 59 - 6
src/main/lombok/org/leumasjaffe/container/Either.java

@@ -1,35 +1,88 @@
 package org.leumasjaffe.container;
 
+import java.util.function.Consumer;
+import java.util.function.Function;
+
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.NonNull;
 
 @AllArgsConstructor(access=AccessLevel.PRIVATE)
-@Getter
+@Getter(AccessLevel.PACKAGE) // For JUnit tests only
 public class Either<T1, T2> {
 	public enum State { LEFT, RIGHT }
 	T1 left;
 	T2 right;
 	
-	public static <T1, T2> Either<T1, T2> ofLeft(T1 left) {
+	public static <T1, T2> Either<T1, T2> ofLeft(@NonNull T1 left) {
 		return new Either<>(left, null);
 	}
 	
-	public static <T1, T2> Either<T1, T2> ofRight(T2 right) {
+	public static <T1, T2> Either<T1, T2> ofRight(@NonNull T2 right) {
 		return new Either<>(null, right);
 	}
 	
-	public State getState() {
+	State getState() {
 		return left == null ? State.RIGHT : State.LEFT;
 	}
 	
-	public void setLeft(T1 left) {
+	public void setLeft(@NonNull T1 left) {
 		this.right = null;
 		this.left = left;
 	}
 
-	public void setRight(T2 right) {
+	public void setRight(@NonNull T2 right) {
 		this.right = right;
 		this.left = null;
 	}
+	
+	public void consume(@NonNull Consumer<T1> fLeft, @NonNull Consumer<T2> fRight) {
+		switch (getState()) {
+		case LEFT:   fLeft.accept(left);  return;
+		case RIGHT: fRight.accept(right); return;
+		default: throw new IllegalStateException();
+		}
+	}
+
+	public <R1, R2> Either<R1, R2> map(@NonNull Function<T1, R1> fLeft,
+			@NonNull Function<T2, R2> fRight) {
+		switch (getState()) {
+		case LEFT:  return Either.ofLeft(fLeft.apply(left));
+		case RIGHT: return Either.ofRight(fRight.apply(right));
+		default: throw new IllegalStateException();
+		}
+	}
+	
+	public <R> Either<R, T2> mapLeft(@NonNull Function<T1, R> fLeft) {
+		switch (getState()) {
+		case LEFT:  return Either.ofLeft(fLeft.apply(left));
+		case RIGHT: return Either.ofRight(right);
+		default: throw new IllegalStateException();
+		}
+	}
+
+	public <R> Either<T1, R> mapRight(@NonNull Function<T2, R> fRight) {
+		switch (getState()) {
+		case LEFT:  return Either.ofLeft(left);
+		case RIGHT: return Either.ofRight(fRight.apply(right));
+		default: throw new IllegalStateException();
+		}
+	}
+	
+	public static <T> Function<T, T> identity() {
+		return (x) -> x;
+	}
+	
+	public static <T, R> Function<T, R> discard() {
+		return (x) -> null;
+	}
+	
+	public <R> R unify(@NonNull Function<T1, R> fLeft, @NonNull Function<T2, R> fRight) {
+		switch (getState()) {
+		case LEFT:  return fLeft.apply(left);
+		case RIGHT: return fRight.apply(right);
+		default: throw new IllegalStateException();
+		}
+	}
 }

+ 89 - 0
src/test/java/org/leumasjaffe/container/EitherFunctionalTest.java

@@ -0,0 +1,89 @@
+package org.leumasjaffe.container;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class EitherFunctionalTest {
+	String str;
+	Integer i;
+	
+	@Before
+	public void setUp() {
+		str = "testing";
+		i = Integer.valueOf(500);
+	}
+
+	@Test
+	public void consumeLeftWithRightFailFunctionPasses() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.consume(s -> assertThat(s, is(anything())),
+				i -> fail());
+	}
+
+	@Test
+	public void consumeRightWithLeftFailFunctionPasses() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.consume(s -> fail(),
+				i -> assertThat(i, is(anything())));
+	}
+	
+	@Test
+	public void mapWithLeftDataChangesLeftSide() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		Either<Integer, String> mapped = either.map(s -> i, i -> str);
+		assertThat(mapped.getLeft(), is(equalTo(i)));
+	}
+
+	@Test
+	public void mapWithRightDataChangesRightSide() {
+		Either<String, Integer> either = Either.ofRight(i);
+		Either<Integer, String> mapped = either.map(s -> i, i -> str);
+		assertThat(mapped.getRight(), is(equalTo(str)));
+	}
+	
+	@Test
+	public void mapLeftWithLeftDataChanges() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		Either<Integer, Integer> mapped = either.mapLeft(s -> i);
+		assertThat(mapped.getLeft(), is(equalTo(i)));
+	}
+	
+	@Test
+	public void mapRightWithRightDataChanges() {
+		Either<String, Integer> either = Either.ofRight(i);
+		Either<String, String> mapped = either.mapRight(i -> str);
+		assertThat(mapped.getRight(), is(equalTo(str)));
+	}
+	
+	@Test
+	public void mapLeftWithRightDataDoesNothin() {
+		Either<String, Integer> either = Either.ofRight(i);
+		Either<Integer, Integer> mapped = either.mapLeft(s -> i);
+		assertThat(mapped.getRight(), is(sameInstance(i)));
+	}
+	
+	@Test
+	public void mapRightWithLeftDataDoesNothin() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		Either<String, String> mapped = either.mapRight(i -> str);
+		assertThat(mapped.getLeft(), is(sameInstance(str)));
+	}
+	
+	@Test
+	public void unifyOnLeftTerminatesEitherChain() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		String data = either.unify(s -> "str:" + s, i -> "int:" + i);
+		assertThat(data, is(equalTo("str:testing")));
+	}
+
+	@Test
+	public void unifyOnRightTerminatesEitherChain() {
+		Either<String, Integer> either = Either.ofRight(i);
+		String data = either.unify(s -> "str:" + s, i -> "int:" + i);
+		assertThat(data, is(equalTo("int:500")));
+	}
+}

+ 84 - 0
src/test/java/org/leumasjaffe/container/EitherStateTest.java

@@ -0,0 +1,84 @@
+package org.leumasjaffe.container;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class EitherStateTest {
+	String str;
+	Integer i;
+	
+	@Before
+	public void setUp() {
+		str = "testing";
+		i = Integer.valueOf(500);
+	}
+
+	@Test
+	public void testEitherLeftCapturesPointer() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		assertThat(either.getLeft(), is(sameInstance(str)));
+	}
+	
+	@Test
+	public void testEitherRightCapturesPointer() {
+		Either<String, Integer> either = Either.ofRight(i);
+		assertThat(either.getRight(), is(sameInstance(i)));
+	}
+
+	@Test
+	public void testEitherLeftSetsState() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		assertThat(either.getState(), is(equalTo(Either.State.LEFT)));
+	}
+	
+	@Test
+	public void testEitherRightSetsState() {
+		Either<String, Integer> either = Either.ofRight(i);
+		assertThat(either.getState(), is(equalTo(Either.State.RIGHT)));
+	}
+	
+	@Test
+	public void testEitherLeftRightIsNull() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		assertThat(either.getRight(), is(nullValue()));
+	}
+
+	@Test
+	public void testEitherRightLeftIsNull() {
+		Either<String, Integer> either = Either.ofRight(i);
+		assertThat(either.getLeft(), is(nullValue()));
+	}
+
+	@Test
+	public void testEitherSetLeftNullsRight() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.setLeft(str);
+		assertThat(either.getLeft(), is(not(nullValue())));
+		assertThat(either.getRight(), is(nullValue()));
+	}
+
+	@Test
+	public void testEitherSetRightNullsLeft() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.setRight(i);
+		assertThat(either.getRight(), is(not(nullValue())));
+		assertThat(either.getLeft(), is(nullValue()));
+	}
+
+	@Test
+	public void testEitherSetLeftAlsoSetsState() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.setLeft(str);
+		assertThat(either.getState(), is(equalTo(Either.State.LEFT)));
+	}
+
+	@Test
+	public void testEitherSetRightAlsoSetsState() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.setRight(i);
+		assertThat(either.getState(), is(equalTo(Either.State.RIGHT)));
+	}
+}

+ 13 - 0
src/test/java/org/leumasjaffe/container/EitherTestSuite.java

@@ -0,0 +1,13 @@
+package org.leumasjaffe.container;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses(value = {
+		EitherStateTest.class,
+		EitherValidationTest.class,
+		EitherFunctionalTest.class
+})
+public class EitherTestSuite {
+}

+ 99 - 0
src/test/java/org/leumasjaffe/container/EitherValidationTest.java

@@ -0,0 +1,99 @@
+package org.leumasjaffe.container;
+
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class EitherValidationTest {
+	String str;
+	Integer i;
+	
+	@Before
+	public void setUp() {
+		str = "testing";
+		i = Integer.valueOf(500);
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void testEitherLeftNullThrows() {
+		Either.ofLeft(null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void testEitherRightNullThrows() {
+		Either.ofRight(null);
+		fail();
+	}
+
+	@Test(expected=NullPointerException.class)
+	public void testEitherSetLeftWithNullThrows() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.setLeft(null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void testEitherSetRightWithNullThrows() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.setRight(null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void consumeWithNullLeftFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.consume(null, i -> fail());
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void consumeWithNullRightFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.consume(s -> fail(), null);
+		fail();
+	}
+
+	@Test(expected=NullPointerException.class)
+	public void mapWithNullLeftFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.map(null, i -> str);
+		fail();
+	}
+
+	@Test(expected=NullPointerException.class)
+	public void mapWithNullRightFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.map(s -> i, null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void mapLeftWithNullFunctionThrows() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.mapLeft(null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void mapRightWithNullFunctionThrows() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.mapRight(null);
+		fail();
+	}
+	
+	@Test(expected=NullPointerException.class)
+	public void unifyWithNullLeftFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofRight(i);
+		either.unify(null, i -> "int:" + i);
+		fail();
+	}
+
+	@Test(expected=NullPointerException.class)
+	public void unifyWithNullRightFunctionThrowsEvenIfUnneeded() {
+		Either<String, Integer> either = Either.ofLeft(str);
+		either.unify(s -> "str:" + s, null);
+		fail();
+	}
+}