diff --git a/src/test/java/com/jnape/palatable/lambdakoans/AboutApplicatives.java b/src/test/java/com/jnape/palatable/lambdakoans/AboutApplicatives.java new file mode 100644 index 0000000..741c28f --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambdakoans/AboutApplicatives.java @@ -0,0 +1,161 @@ +package com.jnape.palatable.lambdakoans; + +import com.jnape.palatable.lambda.adt.Either; +import com.jnape.palatable.lambda.adt.Maybe; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.builtin.fn3.LiftA2; +import com.jnape.palatable.lambda.functions.builtin.fn4.LiftA3; +import com.jnape.palatable.lambda.functor.builtin.Lazy; +import com.jnape.palatable.lambda.io.IO; +import com.jnape.palatable.lambda.traversable.LambdaIterable; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.jnape.palatable.lambda.adt.Either.left; +import static com.jnape.palatable.lambda.adt.Either.right; +import static com.jnape.palatable.lambda.adt.Maybe.just; +import static com.jnape.palatable.lambda.adt.Maybe.nothing; +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.Fn1.fn1; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Repeat.repeat; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Take.take; +import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy; +import static com.jnape.palatable.lambdakoans.Koans.__; +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static testsupport.matchers.IterableMatcher.iterates; + +public class AboutApplicatives { + + @Test + public void applicativesZipEmbeddedValuesWithEmbeddedFunctions() { + Fn1 inc = x -> x + 1; + + assertThat(just(9).zip(just(inc)), equalTo(just(10))); + assertThat(just(15).zip(just(inc)), equalTo(just(__))); + assertThat(just(7).zip(nothing()), equalTo(__)); + // Explicit types help the compiler + assertThat(Maybe.nothing().zip(just(inc)), equalTo(__)); + + Fn1 getStringLength = String::length; + assertThat(right("Hello").zip(right(getStringLength)), equalTo(__)); + assertThat(right("World!").zip(right(getStringLength)), equalTo(__)); + assertThat(Either.left("whoops").zip(right(getStringLength)), equalTo(left(__))); + + // Moving on to LambdaIterables, where "zipping" is more obvious + LambdaIterable oneThroughThree = LambdaIterable.wrap(asList(1, 2, 3)); + LambdaIterable> wrappedInc = LambdaIterable.wrap(asList(inc)); + assertThat(oneThroughThree.zip(wrappedInc).unwrap(), iterates(2, 3, 4)); + + // Zipping a LambdaIterable computes the Cartesian product of values with functions + // rather than apply the functions to values pairwise + Fn1 dec = x -> x - 1; + LambdaIterable> twoFunctions = LambdaIterable.wrap(asList(inc, dec)); + assertThat(oneThroughThree.zip(twoFunctions).unwrap(), iterates(2, 0, 3, 1, 4, __())); + + Fn1 times3 = x -> x * 3; + LambdaIterable> allFunctions = LambdaIterable.wrap(asList(inc, dec, times3)); + assertThat(oneThroughThree.zip(allFunctions).unwrap(), iterates(__())); + } + + @Test + public void functionIsApplicative() { + Fn1 strLen = String::length; + Fn1 toUpper = String::toUpperCase; + + // Result of unary function calls are passed to mapping function as arguments + Fn1> lengthAndUppercase = LiftA2.liftA2(Tuple2::tuple, strLen, toUpper); + assertThat(lengthAndUppercase.apply("hello world"), equalTo(tuple(11, "HELLO WORLD"))); + + Fn1 mod3 = i -> i % 3; + Fn1 div3 = i -> i / 3; + + Fn1 showDivision = LiftA2.liftA2((divided, remainder) -> String.format("%d * 3 + %d", divided, remainder), div3, mod3); + assertThat(showDivision.apply(10), equalTo(__)); + + + Fn1 findStart = s -> s.indexOf('j'); + Fn1 findEnd = s -> s.indexOf(' '); + Fn3 cutString = String::substring; + + Fn1 transformAndCut = LiftA3.liftA3(cutString, toUpper, findStart, findEnd); + assertThat(transformAndCut.apply("hellojava world"), equalTo(__)); + } + + @Test + public void lazyApplicatives() { + // Zipping LambdaIterables is lazy because LambdaIterables are lazy + LambdaIterable infiniteOnes = LambdaIterable.wrap(repeat(1)); + Fn1 inc = x -> x + 1; + + LambdaIterable> wrappedInc = LambdaIterable.wrap(asList(inc)); + LambdaIterable zippedOnes = infiniteOnes.zip(wrappedInc); + assertThat(take(3, zippedOnes.unwrap()), iterates(__())); + + // We might lazily get a mapping function... + AtomicInteger computed = new AtomicInteger(0); + + Fn1>> expensiveWayToGetMaybeToString = fn1((Integer n) -> { + for (int i = 0; i < n; i++) { + computed.incrementAndGet(); + } + return just(Object::toString); + }); + + Lazy>> lazyGetToString = lazy(() -> + expensiveWayToGetMaybeToString.apply(100_000_000)); + + // ...then apply it with lazyZip. + Maybe nothing = nothing(); + // Note: unlike LambdaIterables, the Maybe inside is not itself lazy + Lazy> lazyNothingToString = nothing.lazyZip(lazyGetToString); + + assertThat(lazyNothingToString.value(), equalTo(__)); + assertThat(computed.get(), equalTo(__)); + + // zip, however, eagerly generates a mapping function + Maybe nothingToString = nothing.zip(expensiveWayToGetMaybeToString.apply(100_000)); + assertThat(nothingToString, equalTo(__)); + assertThat(computed.get(), equalTo(__)); + } + + @Test(timeout = 6500) + public void applicativeRepresentsParallelism() throws ExecutionException, InterruptedException { + IO foo = IO.io(() -> { + Thread.sleep(2_000); + return 14; + }); + + IO bar = IO.io(() -> { + Thread.sleep(2_000); + return 28; + }); + + IO applicativeInIo = LiftA2.liftA2(Integer::sum, foo, bar); + + long singleThreadStart = System.currentTimeMillis(); + + applicativeInIo + .flatMap(result -> IO.io(() -> assertThat(result, equalTo(__)))) + .unsafePerformIO(); + + System.out.printf("Single thread execution took %d seconds%n", (System.currentTimeMillis() - singleThreadStart) / 1000); + + // If we have multiple threads available, function evaluation is done in parallel + long multipleThreadStart = System.currentTimeMillis(); + + applicativeInIo + .flatMap(result -> IO.io(() -> assertThat(result, equalTo(__)))) + // How many threads should we use? + .unsafePerformAsyncIO(Executors.newFixedThreadPool(__())) + .get(); + + System.out.printf("Multiple thread execution took %d seconds%n", (System.currentTimeMillis() - multipleThreadStart) / 1000); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambdakoans/AboutFunctors.java b/src/test/java/com/jnape/palatable/lambdakoans/AboutFunctors.java index 304886e..4934b6f 100644 --- a/src/test/java/com/jnape/palatable/lambdakoans/AboutFunctors.java +++ b/src/test/java/com/jnape/palatable/lambdakoans/AboutFunctors.java @@ -68,6 +68,6 @@ public void fn1sAreAlsoFunctorsOverTheirOutputType() { Fn1 inc = x -> x + 1; Fn1 lengthThenInc = length.fmap(inc); // fmap for Fn1 is just left-to-right composition! - assertThat(lengthThenInc.apply("13 characters"), __()); + assertThat(lengthThenInc.apply("13 characters"), equalTo(__)); } }