Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AboutApplicatives koan #6

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions src/test/java/com/jnape/palatable/lambdakoans/AboutApplicatives.java
Original file line number Diff line number Diff line change
@@ -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<Integer, Integer> 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.<Integer>nothing().zip(just(inc)), equalTo(__));
t-rutten marked this conversation as resolved.
Show resolved Hide resolved

Fn1<? super String, ? extends Integer> getStringLength = String::length;
assertThat(right("Hello").zip(right(getStringLength)), equalTo(__));
assertThat(right("World!").zip(right(getStringLength)), equalTo(__));
assertThat(Either.<String, String>left("whoops").zip(right(getStringLength)), equalTo(left(__)));

// Moving on to LambdaIterables, where "zipping" is more obvious
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to point out that LambdaIterable is a specific choice in terms of how to implement Zip for iterables. It could be pairwise or cross-product. Alternatively, it might be worth putting it in a separate "test" to make that explicit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment about that possible alternative in 5d1d6da.

LambdaIterable<Integer> oneThroughThree = LambdaIterable.wrap(asList(1, 2, 3));
LambdaIterable<Fn1<? super Integer, ? extends Integer>> 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<Integer, Integer> dec = x -> x - 1;
LambdaIterable<Fn1<? super Integer, ? extends Integer>> twoFunctions = LambdaIterable.wrap(asList(inc, dec));
assertThat(oneThroughThree.zip(twoFunctions).unwrap(), iterates(2, 0, 3, 1, 4, __()));

Fn1<Integer, Integer> times3 = x -> x * 3;
LambdaIterable<Fn1<? super Integer, ? extends Integer>> allFunctions = LambdaIterable.wrap(asList(inc, dec, times3));
assertThat(oneThroughThree.zip(allFunctions).unwrap(), iterates(__()));
}

@Test
public void functionIsApplicative() {
Fn1<String, Integer> strLen = String::length;
Fn1<String, String> toUpper = String::toUpperCase;

// Result of unary function calls are passed to mapping function as arguments
Fn1<String, Tuple2<Integer, String>> lengthAndUppercase = LiftA2.liftA2(Tuple2::tuple, strLen, toUpper);
assertThat(lengthAndUppercase.apply("hello world"), equalTo(tuple(11, "HELLO WORLD")));

Fn1<Integer, Integer> mod3 = i -> i % 3;
Fn1<Integer, Integer> div3 = i -> i / 3;

Fn1<Integer, String> showDivision = LiftA2.liftA2((divided, remainder) -> String.format("%d * 3 + %d", divided, remainder), div3, mod3);
assertThat(showDivision.apply(10), equalTo(__));


Fn1<String, Integer> findStart = s -> s.indexOf('j');
Fn1<String, Integer> findEnd = s -> s.indexOf(' ');
Fn3<String, Integer, Integer, String> cutString = String::substring;

Fn1<String, String> 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<Integer> infiniteOnes = LambdaIterable.wrap(repeat(1));
Fn1<Integer, Integer> inc = x -> x + 1;

LambdaIterable<Fn1<? super Integer, ? extends Integer>> wrappedInc = LambdaIterable.wrap(asList(inc));
LambdaIterable<Integer> zippedOnes = infiniteOnes.zip(wrappedInc);
assertThat(take(3, zippedOnes.unwrap()), iterates(__()));

// We might lazily get a mapping function...
AtomicInteger computed = new AtomicInteger(0);

Fn1<Integer, Maybe<Fn1<? super Integer, ? extends String>>> expensiveWayToGetMaybeToString = fn1((Integer n) -> {
for (int i = 0; i < n; i++) {
computed.incrementAndGet();
}
return just(Object::toString);
});

Lazy<Maybe<Fn1<? super Integer, ? extends String>>> lazyGetToString = lazy(() ->
expensiveWayToGetMaybeToString.apply(100_000_000));

// ...then apply it with lazyZip.
Maybe<Integer> nothing = nothing();
// Note: unlike LambdaIterables, the Maybe inside is not itself lazy
Lazy<Maybe<String>> lazyNothingToString = nothing.lazyZip(lazyGetToString);

assertThat(lazyNothingToString.value(), equalTo(__));
assertThat(computed.get(), equalTo(__));

// zip, however, eagerly generates a mapping function
Maybe<String> nothingToString = nothing.zip(expensiveWayToGetMaybeToString.apply(100_000));
assertThat(nothingToString, equalTo(__));
assertThat(computed.get(), equalTo(__));
}

@Test(timeout = 6500)
public void applicativeRepresentsParallelism() throws ExecutionException, InterruptedException {
t-rutten marked this conversation as resolved.
Show resolved Hide resolved
IO<Integer> foo = IO.io(() -> {
Thread.sleep(2_000);
return 14;
});

IO<Integer> bar = IO.io(() -> {
Thread.sleep(2_000);
return 28;
});

IO<Integer> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ public void fn1sAreAlsoFunctorsOverTheirOutputType() {
Fn1<Integer, Integer> inc = x -> x + 1;

Fn1<String, Integer> lengthThenInc = length.fmap(inc); // fmap for Fn1 is just left-to-right composition!
assertThat(lengthThenInc.apply("13 characters"), __());
assertThat(lengthThenInc.apply("13 characters"), equalTo(__));
}
}