diff --git a/CHANGELOG.md b/CHANGELOG.md index e04438485..ee919be2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [Unreleased] +### Added +- `MergeMaps`, a `Monoid` on `Map` formed by `Map#merge` +- `CheckedEffect` is now a `CheckedFn1` +- `CheckedSupplier` is now a `CheckedFn1` +- `CheckedFn1` now overrides all possible methods with covariant return type +- `MapLens#asCopy` has overload taking copy function +- `MapLens#valueAt` has overload taking copy function +- `SortWith` for sorting an `Iterable` given a `Comparator` over its elements +- `IO#externallyManaged`, for supplying an `IO` with externally-managed futures +- test jar is now published +- `Monad#join` static alias for `flatMap(id())` +- `Effect#effect` static factory method taking `Fn1` +- `IO`s automatically encode parallelism in composition + +### Fixed +- issue where certain ways to compose `Effect`s unintentionally nullified the effect + +### Removed +- `AddAll` semigroup, deprecated in previous release +- Dyadic `Either#flatMap()`, deprecated in previous release + +## [3.2.0] - 2018-12-08 ### Changed - ***Breaking Change***: `Difference` and `Intersection` no longer instances of `Semigroup` and moved to `functions.builtin.fn2` package - ***Breaking Change***: `Absent` moved to `semigroup.builtin` package @@ -404,7 +426,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - `Monadic/Dyadic/TriadicFunction`, `Predicate`, `Tuple2`, `Tuple3` - `Functor`, `BiFunctor`, `ProFunctor` -[Unreleased]: https://github.com/palatable/lambda/compare/lambda-3.1.0...HEAD +[Unreleased]: https://github.com/palatable/lambda/compare/lambda-3.2.0...HEAD +[3.2.0]: https://github.com/palatable/lambda/compare/lambda-3.1.0...lambda-3.2.0 [3.1.0]: https://github.com/palatable/lambda/compare/lambda-3.0.3...lambda-3.1.0 [3.0.3]: https://github.com/palatable/lambda/compare/lambda-3.0.2...lambda-3.0.3 [3.0.2]: https://github.com/palatable/lambda/compare/lambda-3.0.1...lambda-3.0.2 diff --git a/README.md b/README.md index 1d2dc60b0..65501c222 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,14 @@ Add the following dependency to your: com.jnape.palatable lambda - 3.1.0 + 3.2.0 ``` `build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)): ```gradle -compile group: 'com.jnape.palatable', name: 'lambda', version: '3.1.0' +compile group: 'com.jnape.palatable', name: 'lambda', version: '3.2.0' ``` Examples diff --git a/pom.xml b/pom.xml index a117dcaf0..145522c52 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ lambda - 3.2.0 + 3.3.0 jar Lambda @@ -57,6 +57,7 @@ 1.2 3.3 1.3 + 3.1.1 @@ -103,6 +104,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + test-jar + + + + diff --git a/src/main/java/com/jnape/palatable/lambda/adt/Either.java b/src/main/java/com/jnape/palatable/lambda/adt/Either.java index dfcb85973..00f3bf787 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/Either.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/Either.java @@ -127,24 +127,6 @@ public Either flatMap(FunctionrightFn to the unwrapped right value and return the resulting - * Either; otherwise, apply the unwrapped left value to leftFn and return the resulting - * Either. - * - * @param leftFn the function to apply if a left value - * @param rightFn the function to apply if a right value - * @param the new left parameter type - * @param the new right parameter type - * @return the result of either rightFn or leftFn, depending on whether this is a right or a left - * @deprecated in favor of {@link Either#match(Function, Function)} - */ - @Deprecated - public final Either flatMap(Function> leftFn, - Function> rightFn) { - return match(leftFn, rightFn); - } - @Override public final Either invert() { return match(Either::right, Either::left); diff --git a/src/main/java/com/jnape/palatable/lambda/functions/Effect.java b/src/main/java/com/jnape/palatable/lambda/functions/Effect.java index 8a341d9a3..5c36ff861 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/Effect.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/Effect.java @@ -8,6 +8,7 @@ import static com.jnape.palatable.lambda.functions.Fn0.fn0; import static com.jnape.palatable.lambda.functions.IO.io; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; /** * A function returning "no result", and therefore only useful as a side-effect. @@ -26,22 +27,22 @@ default IO apply(A a) { @Override default Effect diMapL(Function fn) { - return Fn1.super.diMapL(fn)::apply; + return effect(Fn1.super.diMapL(fn)); } @Override default Effect contraMap(Function fn) { - return Fn1.super.contraMap(fn)::apply; + return effect(Fn1.super.contraMap(fn)); } @Override default Effect compose(Function before) { - return Fn1.super.compose(before)::apply; + return effect(Fn1.super.compose(before)); } @Override default Effect discardR(Applicative> appB) { - return Fn1.super.discardR(appB)::apply; + return effect(Fn1.super.discardR(appB)); } @Override @@ -67,6 +68,17 @@ static Effect effect(Consumer effect) { * @return the effect */ static Effect effect(Runnable runnable) { - return effect(__ -> runnable.run()); + return effect(constantly(io(runnable))); + } + + /** + * Create an {@link Effect} from an {@link Fn1} that yields an {@link IO}. + * + * @param fn the function + * @param the effect argument type + * @return the effect + */ + static Effect effect(Fn1> fn) { + return a -> fn.apply(a).unsafePerformIO(); } } \ No newline at end of file diff --git a/src/main/java/com/jnape/palatable/lambda/functions/IO.java b/src/main/java/com/jnape/palatable/lambda/functions/IO.java index 8e05dfcaf..cb0ce4c07 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/IO.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/IO.java @@ -1,33 +1,111 @@ package com.jnape.palatable.lambda.functions; +import com.jnape.palatable.lambda.adt.Try; import com.jnape.palatable.lambda.adt.Unit; import com.jnape.palatable.lambda.functor.Applicative; import com.jnape.palatable.lambda.monad.Monad; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.function.Function; +import java.util.function.Supplier; import static com.jnape.palatable.lambda.adt.Unit.UNIT; +import static com.jnape.palatable.lambda.functions.specialized.checked.CheckedSupplier.checked; +import static java.util.concurrent.CompletableFuture.supplyAsync; /** - * A {@link Monad} representing some effectful computation to be performed. + * A {@link Monad} representing some side-effecting computation to be performed. Note that because {@link IO} inherently + * offers an interface supporting parallelism, the optimal execution strategy for any given {@link IO} is encoded in + * its composition. * * @param the result type */ public interface IO extends Monad> { /** - * Run the effect represented by this {@link IO} instance + * Run the effect represented by this {@link IO} instance, blocking the current thread until the effect terminates. * * @return the result of the effect */ A unsafePerformIO(); + /** + * Returns a {@link CompletableFuture} representing the result of this eventual effect. By default, this will + * immediately run the effect in terms of the implicit {@link Executor} available to {@link CompletableFuture} + * (usually the {@link java.util.concurrent.ForkJoinPool}). Note that specific {@link IO} constructions may allow + * this method to delegate to externally-managed {@link CompletableFuture} instead of synthesizing their own. + * + * @return the {@link CompletableFuture} representing this {@link IO}'s eventual result + * @see IO#unsafePerformAsyncIO(Executor) + */ + default CompletableFuture unsafePerformAsyncIO() { + return supplyAsync(this::unsafePerformIO); + } + + /** + * Returns a {@link CompletableFuture} representing the result of this eventual effect. By default, this will + * immediately run the effect in terms of the provided {@link Executor}. Note that specific {@link IO} + * constructions may allow this method to delegate to externally-managed {@link CompletableFuture} instead of + * synthesizing their own. + * + * @param executor the {@link Executor} to run the {@link CompletableFuture} from + * @return the {@link CompletableFuture} representing this {@link IO}'s eventual result + * @see IO#unsafePerformAsyncIO() + */ + default CompletableFuture unsafePerformAsyncIO(Executor executor) { + return supplyAsync(this::unsafePerformIO, executor); + } + + /** + * Given a function from any {@link Throwable} to the result type A, if this {@link IO} successfully + * yields a result, return it; otherwise, map the {@link Throwable} to the result type and return that. + * + * @param recoveryFn the recovery function + * @return the guarded {@link IO} + */ + default IO exceptionally(Function recoveryFn) { + return new IO() { + @Override + public A unsafePerformIO() { + return Try.trying(IO.this::unsafePerformIO).recover(recoveryFn); + } + + @Override + public CompletableFuture unsafePerformAsyncIO() { + return IO.this.unsafePerformAsyncIO().exceptionally(recoveryFn::apply); + } + + @Override + public CompletableFuture unsafePerformAsyncIO(Executor executor) { + return IO.this.unsafePerformAsyncIO(executor).exceptionally(recoveryFn::apply); + } + }; + } + /** * {@inheritDoc} */ @Override default IO flatMap(Function>> f) { - return () -> f.apply(unsafePerformIO()).>coerce().unsafePerformIO(); + return new IO() { + @Override + public B unsafePerformIO() { + return f.apply(IO.this.unsafePerformIO()).>coerce().unsafePerformIO(); + } + + @Override + public CompletableFuture unsafePerformAsyncIO() { + return IO.this.unsafePerformAsyncIO() + .thenCompose(a -> f.apply(a).>coerce().unsafePerformAsyncIO()); + } + + @Override + public CompletableFuture unsafePerformAsyncIO(Executor executor) { + return IO.this.unsafePerformAsyncIO(executor) + .thenCompose(a -> f.apply(a).>coerce().unsafePerformAsyncIO(executor)); + } + }; } /** @@ -51,7 +129,24 @@ default IO fmap(Function fn) { */ @Override default IO zip(Applicative, IO> appFn) { - return Monad.super.zip(appFn).coerce(); + IO ioA = this; + IO> ioF = appFn.coerce(); + return new IO() { + @Override + public B unsafePerformIO() { + return ioF.unsafePerformIO().apply(ioA.unsafePerformIO()); + } + + @Override + public CompletableFuture unsafePerformAsyncIO() { + return ioF.unsafePerformAsyncIO().thenCompose(ioA.unsafePerformAsyncIO()::thenApply); + } + + @Override + public CompletableFuture unsafePerformAsyncIO(Executor executor) { + return ioF.unsafePerformAsyncIO(executor).thenCompose(ioA.unsafePerformAsyncIO(executor)::thenApply); + } + }; } /** @@ -115,4 +210,35 @@ static IO io(Runnable runnable) { static IO io(Fn1 fn1) { return io(() -> fn1.apply(UNIT)); } + + /** + * Static factory method for creating an {@link IO} from an externally managed source of + * {@link CompletableFuture completable futures}. + *

+ * Note that constructing an {@link IO} this way results in no intermediate futures being constructed by either + * {@link IO#unsafePerformAsyncIO()} or {@link IO#unsafePerformAsyncIO(Executor)}, and {@link IO#unsafePerformIO()} + * is synonymous with invoking {@link CompletableFuture#get()} on the externally managed future. + * + * @param supplier the source of externally managed {@link CompletableFuture completable futures} + * @param the result type + * @return the {@link IO} + */ + static IO externallyManaged(Supplier> supplier) { + return new IO() { + @Override + public A unsafePerformIO() { + return checked(() -> unsafePerformAsyncIO().get()).get(); + } + + @Override + public CompletableFuture unsafePerformAsyncIO() { + return supplier.get(); + } + + @Override + public CompletableFuture unsafePerformAsyncIO(Executor executor) { + return supplier.get(); + } + }; + } } diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Sort.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Sort.java index ce297dd9b..96fb7056d 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Sort.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Sort.java @@ -1,6 +1,8 @@ package com.jnape.palatable.lambda.functions.builtin.fn1; import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.builtin.fn2.SortBy; +import com.jnape.palatable.lambda.functions.builtin.fn2.SortWith; import java.util.List; @@ -12,7 +14,8 @@ * this is both eager and monolithic. * * @param the input Iterable and output List element type - * @see com.jnape.palatable.lambda.functions.builtin.fn2.SortBy + * @see SortBy + * @see SortWith */ public final class Sort> implements Fn1, List> { diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortBy.java index de395356d..b92628141 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortBy.java @@ -2,12 +2,12 @@ import com.jnape.palatable.lambda.functions.Fn1; import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functions.builtin.fn1.Sort; -import java.util.ArrayList; import java.util.List; import java.util.function.Function; -import static com.jnape.palatable.lambda.functions.builtin.fn2.ToCollection.toCollection; +import static com.jnape.palatable.lambda.functions.builtin.fn2.SortWith.sortWith; import static java.util.Comparator.comparing; /** @@ -17,7 +17,8 @@ * * @param the input Iterable and output List element type * @param the mapped Comparable type - * @see com.jnape.palatable.lambda.functions.builtin.fn1.Sort + * @see Sort + * @see SortWith */ public final class SortBy> implements Fn2, Iterable, List> { @@ -28,9 +29,7 @@ private SortBy() { @Override public List apply(Function fn, Iterable as) { - List result = toCollection(ArrayList::new, as); - result.sort(comparing(fn)); - return result; + return sortWith(comparing(fn), as); } @SuppressWarnings("unchecked") diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWith.java new file mode 100644 index 000000000..813fb2e53 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWith.java @@ -0,0 +1,48 @@ +package com.jnape.palatable.lambda.functions.builtin.fn2; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functions.builtin.fn1.Sort; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static com.jnape.palatable.lambda.functions.builtin.fn2.ToCollection.toCollection; + +/** + * Given an {@link Iterable} and a {@link java.util.Comparator} over the {@link Iterable} element type, produce a + * sorted {@link List} of the original elements based on sorting applied by the {@link java.util.Comparator}. Note that + * this is both eager and monolithic. + * + * @param the input Iterable and output List element type + * @see Sort + * @see SortBy + */ +public final class SortWith implements Fn2, Iterable, List> { + + private static final SortWith INSTANCE = new SortWith(); + + private SortWith() { + } + + @Override + public List apply(Comparator comparator, Iterable as) { + List result = toCollection(ArrayList::new, as); + result.sort(comparator); + return result; + } + + @SuppressWarnings("unchecked") + public static SortWith sortWith() { + return INSTANCE; + } + + public static Fn1, List> sortWith(Comparator comparator) { + return SortWith.sortWith().apply(comparator); + } + + public static List sortWith(Comparator comparator, Iterable as) { + return SortWith.sortWith(comparator).apply(as); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffect.java b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffect.java index 0197295ee..a4dc0c820 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffect.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffect.java @@ -1,8 +1,16 @@ package com.jnape.palatable.lambda.functions.specialized.checked; +import com.jnape.palatable.lambda.adt.Unit; import com.jnape.palatable.lambda.functions.Effect; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functor.Applicative; +import com.jnape.palatable.lambda.functions.IO; + +import java.util.function.Consumer; +import java.util.function.Function; import static com.jnape.palatable.lambda.functions.specialized.checked.Runtime.throwChecked; +import static com.jnape.palatable.lambda.functions.IO.io; /** * Specialized {@link Effect} that can throw any {@link Throwable}. @@ -14,8 +22,11 @@ * @see Effect */ @FunctionalInterface -public interface CheckedEffect extends Effect { +public interface CheckedEffect extends Effect, CheckedFn1> { + /** + * {@inheritDoc} + */ @Override default void accept(A a) { try { @@ -25,6 +36,62 @@ default void accept(A a) { } } + /** + * {@inheritDoc} + */ + @Override + default IO apply(A a) { + return io(() -> accept(a)); + } + + /** + * {@inheritDoc} + */ + @Override + default IO checkedApply(A a) throws T { + return apply(a); + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedEffect diMapL(Function fn) { + return Effect.super.diMapL(fn)::accept; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedEffect contraMap(Function fn) { + return Effect.super.contraMap(fn)::accept; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedEffect compose(Function before) { + return Effect.super.compose(before)::accept; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedEffect discardR(Applicative> appB) { + return Effect.super.discardR(appB)::accept; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedEffect andThen(Consumer after) { + return Effect.super.andThen(after)::accept; + } + /** * A version of {@link Effect#accept} that can throw checked exceptions. * diff --git a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1.java b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1.java index e35dd70da..4b5bd93a7 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1.java @@ -1,6 +1,12 @@ package com.jnape.palatable.lambda.functions.specialized.checked; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functor.Applicative; +import com.jnape.palatable.lambda.monad.Monad; + +import java.util.function.Function; import static com.jnape.palatable.lambda.functions.specialized.checked.Runtime.throwChecked; @@ -17,6 +23,9 @@ @FunctionalInterface public interface CheckedFn1 extends Fn1 { + /** + * {@inheritDoc} + */ @Override default B apply(A a) { try { @@ -26,6 +35,119 @@ default B apply(A a) { } } + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 fmap(Function f) { + return Fn1.super.fmap(f)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 flatMap(Function>> f) { + return Fn1.super.flatMap(f).>coerce()::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 discardL(Applicative> appB) { + return Fn1.super.discardL(appB)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 discardR(Applicative> appB) { + return Fn1.super.discardR(appB)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 zip(Applicative, Fn1> appFn) { + return Fn1.super.zip(appFn)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 zip(Fn2 appFn) { + return Fn1.super.zip(appFn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 diMapL(Function fn) { + return Fn1.super.diMapL(fn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 diMapR(Function fn) { + return Fn1.super.diMapR(fn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 diMap(Function lFn, + Function rFn) { + return Fn1.super.diMap(lFn, rFn)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1, Tuple2> strengthen() { + return Fn1.super.strengthen()::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1> carry() { + return Fn1.super.carry()::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 contraMap(Function fn) { + return Fn1.super.contraMap(fn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 compose(Function before) { + return Fn1.super.compose(before)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFn1 andThen(Function after) { + return Fn1.super.andThen(after)::apply; + } + /** * A version of {@link Fn1#apply} that can throw checked exceptions. * diff --git a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplier.java b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplier.java index b32f1e4b8..627da8c91 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplier.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplier.java @@ -1,7 +1,16 @@ package com.jnape.palatable.lambda.functions.specialized.checked; +import com.jnape.palatable.lambda.adt.Unit; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functor.Applicative; +import com.jnape.palatable.lambda.monad.Monad; + +import java.util.function.Function; import java.util.function.Supplier; +import static com.jnape.palatable.lambda.adt.Unit.UNIT; import static com.jnape.palatable.lambda.functions.specialized.checked.Runtime.throwChecked; /** @@ -13,16 +22,7 @@ * @see CheckedRunnable */ @FunctionalInterface -public interface CheckedSupplier extends Supplier { - - @Override - default A get() { - try { - return checkedGet(); - } catch (Throwable t) { - throw throwChecked(t); - } - } +public interface CheckedSupplier extends Supplier, CheckedFn1 { /** * A version of {@link Supplier#get()} that can throw checked exceptions. @@ -41,6 +41,95 @@ default CheckedRunnable toRunnable() { return this::get; } + @Override + default A checkedApply(Unit unit) throws T { + return checkedGet(); + } + + /** + * {@inheritDoc} + */ + @Override + default A get() { + try { + return checkedGet(); + } catch (Throwable t) { + throw throwChecked(t); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier fmap(Function f) { + return CheckedFn1.super.fmap(f).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier flatMap(Function>> f) { + return CheckedFn1.super.flatMap(f).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier discardL(Applicative> appB) { + return CheckedFn1.super.discardL(appB).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier discardR(Applicative> appB) { + return CheckedFn1.super.discardR(appB).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier zip(Applicative, Fn1> appFn) { + return CheckedFn1.super.zip(appFn).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier zip(Fn2 appFn) { + return CheckedFn1.super.zip(appFn).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier diMapR(Function fn) { + return CheckedFn1.super.diMapR(fn).thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier> carry() { + return CheckedFn1.super.carry().thunk(UNIT)::apply; + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier andThen(Function after) { + return CheckedFn1.super.andThen(after).thunk(UNIT)::apply; + } + /** * Convenience static factory method for constructing a {@link CheckedSupplier} without an explicit cast or type * attribution at the call site. diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java index d01bfbd05..e6df4f236 100644 --- a/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java @@ -2,6 +2,8 @@ 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.IO; import com.jnape.palatable.lambda.functions.builtin.fn2.Filter; import com.jnape.palatable.lambda.lens.Iso; import com.jnape.palatable.lambda.lens.Lens; @@ -12,12 +14,15 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Function; import static com.jnape.palatable.lambda.adt.Maybe.maybe; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Alter.alter; import static com.jnape.palatable.lambda.functions.builtin.fn2.Map.map; import static com.jnape.palatable.lambda.functions.builtin.fn2.ToCollection.toCollection; import static com.jnape.palatable.lambda.functions.builtin.fn2.ToMap.toMap; import static com.jnape.palatable.lambda.lens.Lens.Simple.adapt; +import static com.jnape.palatable.lambda.lens.Lens.lens; import static com.jnape.palatable.lambda.lens.Lens.simpleLens; import static com.jnape.palatable.lambda.lens.functions.View.view; import static com.jnape.palatable.lambda.lens.lenses.MaybeLens.unLiftA; @@ -31,6 +36,21 @@ public final class MapLens { private MapLens() { } + /** + * A lens that focuses on a copy of a {@link Map} as a subtype M. Useful for composition to avoid + * mutating a map reference. + * + * @param the map subtype + * @param the key type + * @param the value type + * @param copyFn the copy function + * @return a lens that focuses on copies of maps as a specific subtype + */ + public static , K, V> Lens, M, M, M> asCopy( + Function, ? extends M> copyFn) { + return lens(copyFn, (__, copy) -> copy); + } + /** * A lens that focuses on a copy of a Map. Useful for composition to avoid mutating a map reference. * @@ -39,7 +59,27 @@ private MapLens() { * @return a lens that focuses on copies of maps */ public static Lens.Simple, Map> asCopy() { - return simpleLens(HashMap::new, (__, copy) -> copy); + return adapt(asCopy(HashMap::new)); + } + + /** + * A lens that focuses on a value at a key in a map, as a {@link Maybe}, and produces a subtype M on + * the way back out. + * + * @param the map subtype + * @param the key type + * @param the value type + * @param k the key to focus on + * @param copyFn the copy function + * @return a lens that focuses on the value at key, as a {@link Maybe} + */ + public static , K, V> Lens, M, Maybe, Maybe> valueAt( + Function, ? extends M> copyFn, K k) { + return lens(m -> maybe(m.get(k)), (m, maybeV) -> maybeV + .>>fmap(v -> alter(updated -> updated.put(k, v))) + .orElse(alter(updated -> updated.remove(k))) + .apply(copyFn.apply(m)) + .unsafePerformIO()); } /** @@ -51,16 +91,7 @@ public static Lens.Simple, Map> asCopy() { * @return a lens that focuses on the value at key, as a {@link Maybe} */ public static Lens.Simple, Maybe> valueAt(K k) { - return simpleLens(m -> maybe(m.get(k)), (m, maybeV) -> { - Map updated = new HashMap<>(m); - return maybeV.fmap(v -> { - updated.put(k, v); - return updated; - }).orElseGet(() -> { - updated.remove(k); - return updated; - }); - }); + return adapt(valueAt(HashMap::new, k)); } /** @@ -75,7 +106,6 @@ public static Lens.Simple, Maybe> valueAt(K k) { * @param the value type * @return a lens that focuses on the value at the key */ - @SuppressWarnings("unchecked") public static Lens.Simple, V> valueAt(K k, V defaultValue) { return adapt(unLiftB(unLiftA(valueAt(k), defaultValue))); } @@ -150,8 +180,7 @@ public static Lens.Simple, Map> inverted() { /** * A lens that focuses on a map while mapping its values with the mapping {@link Iso}. *

- * Note that for this lens to be lawful, iso must be bijective: that is, every V must - * uniquely and invertibly map to exactly one V2. + * Note that for this lens to be lawful, iso must be lawful. * * @param iso the mapping {@link Iso} * @param the key type diff --git a/src/main/java/com/jnape/palatable/lambda/monad/Monad.java b/src/main/java/com/jnape/palatable/lambda/monad/Monad.java index 11b18d84c..4c3e2e18d 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/Monad.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/Monad.java @@ -1,9 +1,12 @@ package com.jnape.palatable.lambda.monad; +import com.jnape.palatable.lambda.functions.builtin.fn1.Id; import com.jnape.palatable.lambda.functor.Applicative; import java.util.function.Function; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; + /** * Monads are {@link Applicative} functors that support a flattening operation to unwrap M<M<A>> * -> M<A>. This flattening operation, coupled with {@link Applicative#zip(Applicative)}, gives rise to @@ -53,7 +56,7 @@ default Monad fmap(Function fn) { */ @Override default Monad zip(Applicative, M> appFn) { - return appFn., M>>coerce().flatMap(ab -> fmap(ab::apply)); + return appFn., M>>coerce().flatMap(this::fmap); } /** @@ -71,4 +74,17 @@ default Monad discardL(Applicative appB) { default Monad discardR(Applicative appB) { return Applicative.super.discardR(appB).coerce(); } + + /** + * Convenience static method equivalent to {@link Monad#flatMap(Function) flatMap}{@link Id#id() (id())}; + * + * @param mma the outer monad + * @param the monad type + * @param the nested type parameter + * @param the nested monad + * @return the nested monad + */ + static > MA join(Monad mma) { + return mma.flatMap(id()).coerce(); + } } \ No newline at end of file diff --git a/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeMaps.java b/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeMaps.java new file mode 100644 index 000000000..104d6f7d5 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeMaps.java @@ -0,0 +1,61 @@ +package com.jnape.palatable.lambda.monoid.builtin; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.specialized.BiMonoidFactory; +import com.jnape.palatable.lambda.functions.specialized.MonoidFactory; +import com.jnape.palatable.lambda.monoid.Monoid; +import com.jnape.palatable.lambda.semigroup.Semigroup; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +/** + * A {@link Monoid} instance formed by {@link java.util.Map#merge(Object, Object, BiFunction)} and a semigroup over + * V. Combines together multiple maps using the provided semigroup for key collisions. + * + * @param The key parameter type of the Map + * @param The value parameter type of the Map + * @see Monoid + * @see java.util.Map + */ +public final class MergeMaps implements BiMonoidFactory>, Semigroup, Map> { + + private static final MergeMaps INSTANCE = new MergeMaps(); + + private MergeMaps() { + } + + @Override + public Monoid> apply(Supplier> mSupplier, Semigroup semigroup) { + return Monoid.>monoid((x, y) -> { + Map copy = mSupplier.get(); + copy.putAll(x); + y.forEach((k, v) -> copy.merge(k, v, semigroup.toBiFunction())); + return copy; + }, mSupplier); + } + + @SuppressWarnings("unchecked") + public static MergeMaps mergeMaps() { + return INSTANCE; + } + + public static MonoidFactory, Map> mergeMaps(Supplier> mSupplier) { + return MergeMaps.mergeMaps().apply(mSupplier); + } + + public static Monoid> mergeMaps(Supplier> mSupplier, Semigroup semigroup) { + return mergeMaps(mSupplier).apply(semigroup); + } + + public static Fn1, Map> mergeMaps(Supplier> mSupplier, Semigroup semigroup, + Map x) { + return mergeMaps(mSupplier, semigroup).apply(x); + } + + public static Map mergeMaps(Supplier> mSupplier, Semigroup semigroup, Map x, + Map y) { + return mergeMaps(mSupplier, semigroup, x).apply(y); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/AddAll.java b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/AddAll.java deleted file mode 100644 index fe22ba5c2..000000000 --- a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/AddAll.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.jnape.palatable.lambda.semigroup.builtin; - -import com.jnape.palatable.lambda.functions.Fn1; -import com.jnape.palatable.lambda.monoid.Monoid; -import com.jnape.palatable.lambda.semigroup.Semigroup; - -import java.util.Collection; - -/** - * The {@link Semigroup} instance formed under mutative concatenation for an arbitrary {@link Collection}. The - * collection subtype (C) must support {@link Collection#addAll(Collection)}. - *

- * For the {@link Monoid}, see {@link com.jnape.palatable.lambda.monoid.builtin.AddAll}. - * - * @see Semigroup - * @deprecated in favor of the now non-modifying {@link com.jnape.palatable.lambda.monoid.builtin.AddAll monoid} - */ -@Deprecated -public final class AddAll> implements Semigroup { - - private static final AddAll INSTANCE = new AddAll(); - - private AddAll() { - } - - @Override - public C apply(C xs, C ys) { - xs.addAll(ys); - return xs; - } - - @SuppressWarnings("unchecked") - public static > AddAll addAll() { - return INSTANCE; - } - - public static > Fn1 addAll(C xs) { - return AddAll.addAll().apply(xs); - } - - public static > C addAll(C xs, C ys) { - return addAll(xs).apply(ys); - } -} diff --git a/src/test/java/com/jnape/palatable/lambda/functions/EffectTest.java b/src/test/java/com/jnape/palatable/lambda/functions/EffectTest.java index 1b22b349b..9f5dabd60 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/EffectTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/EffectTest.java @@ -1,22 +1,51 @@ package com.jnape.palatable.lambda.functions; +import com.jnape.palatable.lambda.adt.Unit; import org.junit.Test; -import java.util.function.Consumer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import static com.jnape.palatable.lambda.adt.Unit.UNIT; +import static com.jnape.palatable.lambda.functions.Effect.effect; import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; -import static com.jnape.palatable.lambda.functions.specialized.Noop.noop; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; public class EffectTest { @Test - @SuppressWarnings("unused") public void covariantReturns() { - Effect effect = noop(); - Effect diMapL = effect.diMapL(constantly("1")); - Effect contraMap = effect.contraMap(constantly("1")); - Effect compose = effect.compose(constantly("1")); + List results = new ArrayList<>(); + + Effect effect = results::add; + Effect diMapL = effect.diMapL(Object::toString); + Effect contraMap = effect.contraMap(Object::toString); + Effect compose = effect.compose(Object::toString); Effect stringEffect = effect.discardR(constantly("1")); - Effect andThen = effect.andThen((Consumer) noop()); + Effect andThen = effect.andThen(effect); + + effect.accept("1"); + diMapL.accept("2"); + contraMap.accept("3"); + compose.accept("4"); + stringEffect.accept("5"); + andThen.accept("6"); + + assertEquals(asList("1", "2", "3", "4", "5", "6", "6"), results); + } + + @Test + public void staticFactoryMethods() { + AtomicInteger counter = new AtomicInteger(); + + Effect runnableEffect = effect(counter::incrementAndGet); + runnableEffect.apply(UNIT).unsafePerformIO(); + assertEquals(1, counter.get()); + + Effect fnEffect = effect(AtomicInteger::incrementAndGet); + fnEffect.apply(counter).unsafePerformIO(); + assertEquals(2, counter.get()); } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/IOTest.java b/src/test/java/com/jnape/palatable/lambda/functions/IOTest.java index e9038fa6f..cab2595c0 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/IOTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/IOTest.java @@ -1,5 +1,6 @@ package com.jnape.palatable.lambda.functions; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; import com.jnape.palatable.traitor.annotations.TestTraits; import com.jnape.palatable.traitor.runners.Traits; import org.junit.Test; @@ -9,9 +10,20 @@ import testsupport.traits.FunctorLaws; import testsupport.traits.MonadLaws; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.function.Function; + import static com.jnape.palatable.lambda.adt.Unit.UNIT; import static com.jnape.palatable.lambda.functions.Fn0.fn0; +import static com.jnape.palatable.lambda.functions.IO.externallyManaged; import static com.jnape.palatable.lambda.functions.IO.io; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Tupler2.tupler; +import static com.jnape.palatable.lambda.functions.specialized.checked.CheckedFn1.checked; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.Executors.newFixedThreadPool; +import static java.util.concurrent.ForkJoinPool.commonPool; import static org.junit.Assert.assertEquals; @RunWith(Traits.class) @@ -29,4 +41,83 @@ public void staticFactoryMethods() { assertEquals((Integer) 1, io(fn0(() -> 1)).unsafePerformIO()); assertEquals(UNIT, io(() -> {}).unsafePerformIO()); } + + @Test(timeout = 100) + public void unsafePerformAsyncIOWithoutExecutor() { + assertEquals((Integer) 1, io(() -> 1).unsafePerformAsyncIO().join()); + } + + @Test(timeout = 100) + public void unsafePerformAsyncIOWithExecutor() { + assertEquals((Integer) 1, io(() -> 1).unsafePerformAsyncIO(newFixedThreadPool(1)).join()); + } + + @Test + public void zipAndDerivativesComposesInParallel() { + String a = "foo"; + Fn1> f = tupler(1); + + ExecutorService executor = newFixedThreadPool(2); + CountDownLatch advanceFirst = new CountDownLatch(1); + CountDownLatch advanceSecond = new CountDownLatch(1); + + IO ioA = io(checked(__ -> { + advanceFirst.countDown(); + advanceSecond.await(); + return a; + })); + IO>> ioF = io(checked(__ -> { + advanceFirst.await(); + advanceSecond.countDown(); + return f; + })); + + IO> zip = ioA.zip(ioF); + assertEquals(f.apply(a), zip.unsafePerformAsyncIO().join()); + assertEquals(f.apply(a), zip.unsafePerformAsyncIO(executor).join()); + assertEquals(f.apply(a), zip.unsafePerformAsyncIO(executor).join()); + + IO>> discardL = ioA.discardL(ioF); + assertEquals(f, discardL.unsafePerformAsyncIO().join()); + assertEquals(f, discardL.unsafePerformAsyncIO(executor).join()); + + IO discardR = ioA.discardR(ioF); + assertEquals(a, discardR.unsafePerformAsyncIO().join()); + assertEquals(a, discardR.unsafePerformAsyncIO(executor).join()); + } + + @Test + public void delegatesToExternallyManagedFuture() { + CompletableFuture future = completedFuture(1); + IO io = externallyManaged(() -> future); + assertEquals((Integer) 1, io.unsafePerformIO()); + assertEquals((Integer) 1, io.unsafePerformAsyncIO().join()); + assertEquals((Integer) 1, io.unsafePerformAsyncIO(commonPool()).join()); + } + + @Test + public void exceptionallyRecoversThrowableToResult() { + IO io = io(() -> { throw new UnsupportedOperationException("foo"); }); + assertEquals("foo", io.exceptionally(Throwable::getMessage).unsafePerformIO()); + + IO externallyManaged = externallyManaged(() -> new CompletableFuture() {{ + completeExceptionally(new UnsupportedOperationException("foo")); + }}).exceptionally(e -> e.getCause().getMessage()); + assertEquals("foo", externallyManaged.unsafePerformIO()); + } + + @Test + public void exceptionallyRescuesFutures() { + ExecutorService executor = newFixedThreadPool(2); + + IO io = io(() -> { throw new UnsupportedOperationException("foo"); }); + assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO().join()); + assertEquals("foo", io.exceptionally(e -> e.getCause().getMessage()).unsafePerformAsyncIO(executor).join()); + + IO externallyManaged = externallyManaged(() -> new CompletableFuture() {{ + completeExceptionally(new UnsupportedOperationException("foo")); + }}).exceptionally(e -> e.getCause().getMessage()); + assertEquals("foo", externallyManaged.unsafePerformIO()); + + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWithTest.java new file mode 100644 index 000000000..d1b64b603 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/SortWithTest.java @@ -0,0 +1,22 @@ +package com.jnape.palatable.lambda.functions.builtin.fn2; + +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import org.junit.Test; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn2.SortWith.sortWith; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class SortWithTest { + + @Test + public void sortsWithGivenComparator() { + assertEquals(asList(tuple("bar", 4), tuple("baz", 3), tuple("foo", 1), tuple("foo", 2)), + sortWith(Comparator., String>comparing(Tuple2::_1) + .thenComparing(Tuple2::_2), + asList(tuple("foo", 1), tuple("foo", 2), tuple("bar", 4), tuple("baz", 3)))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffectTest.java b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffectTest.java new file mode 100644 index 000000000..8472dc067 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedEffectTest.java @@ -0,0 +1,34 @@ +package com.jnape.palatable.lambda.functions.specialized.checked; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class CheckedEffectTest { + + @Test + public void assignment() { + List results = new ArrayList<>(); + + CheckedEffect effect = results::add; + CheckedEffect diMapL = effect.diMapL(Object::toString); + CheckedEffect contraMap = effect.contraMap(Object::toString); + CheckedEffect compose = effect.compose(Object::toString); + CheckedEffect stringEffect = effect.discardR(constantly("1")); + CheckedEffect andThen = effect.andThen(effect); + + effect.accept("1"); + diMapL.accept("2"); + contraMap.accept("3"); + compose.accept("4"); + stringEffect.accept("5"); + andThen.accept("6"); + + assertEquals(asList("1", "2", "3", "4", "5", "6", "6"), results); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1Test.java b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1Test.java new file mode 100644 index 000000000..a8818d684 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedFn1Test.java @@ -0,0 +1,47 @@ +package com.jnape.palatable.lambda.functions.specialized.checked; + +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import org.junit.Test; + +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static org.junit.Assert.assertEquals; + +public class CheckedFn1Test { + + @Test + public void assignment() { + CheckedFn1 parseInt = Integer::parseInt; + CheckedFn1 fmap = parseInt.fmap(Object::toString); + CheckedFn1 flatMap = parseInt.flatMap(i -> constantly(i + "")); + CheckedFn1 discardL = parseInt.discardL(constantly("0")); + CheckedFn1 discardR = parseInt.discardR(constantly("0")); + CheckedFn1 zipApp = parseInt.zip(constantly(id())); + CheckedFn1 zipF = parseInt.zip(constantly()); + CheckedFn1 diMapL = parseInt.diMapL(id()); + CheckedFn1 diMapR = parseInt.diMapR(id()); + CheckedFn1 diMap = parseInt.diMap(id(), id()); + CheckedFn1, Tuple2> strengthen = parseInt.strengthen(); + CheckedFn1> carry = parseInt.carry(); + CheckedFn1 contraMap = parseInt.contraMap(id()); + CheckedFn1 compose = parseInt.compose(id()); + CheckedFn1 andThen = parseInt.andThen(id()); + + assertEquals((Integer) 1, parseInt.apply("1")); + assertEquals("1", fmap.apply("1")); + assertEquals("1", flatMap.apply("1")); + assertEquals("0", discardL.apply("1")); + assertEquals((Integer) 1, discardR.apply("1")); + assertEquals((Integer) 1, zipApp.apply("1")); + assertEquals("1", zipF.apply("1")); + assertEquals((Integer) 1, diMapL.apply("1")); + assertEquals((Integer) 1, diMapR.apply("1")); + assertEquals((Integer) 1, diMap.apply("1")); + assertEquals(tuple("foo", 1), strengthen.apply(tuple("foo", "1"))); + assertEquals(tuple("1", 1), carry.apply("1")); + assertEquals((Integer) 1, contraMap.apply("1")); + assertEquals((Integer) 1, compose.apply("1")); + assertEquals((Integer) 1, andThen.apply("1")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplierTest.java b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplierTest.java new file mode 100644 index 000000000..804b77ca0 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/specialized/checked/CheckedSupplierTest.java @@ -0,0 +1,42 @@ +package com.jnape.palatable.lambda.functions.specialized.checked; + +import com.jnape.palatable.lambda.adt.Unit; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.functions.builtin.fn1.Constantly; +import org.junit.Test; + +import static com.jnape.palatable.lambda.adt.Unit.UNIT; +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static com.jnape.palatable.lambda.functions.specialized.checked.CheckedSupplier.checked; +import static org.junit.Assert.assertEquals; + +public class CheckedSupplierTest { + + @Test + public void assignment() { + CheckedSupplier intSupplier = checked(() -> 1); + CheckedSupplier fmap = intSupplier.fmap(Object::toString); + @SuppressWarnings("RedundantTypeArguments") + CheckedSupplier flatMap = intSupplier.flatMap(Constantly::constantly); + CheckedSupplier discardL = intSupplier.discardL(constantly(1)); + CheckedSupplier discardR = intSupplier.discardR(constantly(2)); + CheckedSupplier zipA = intSupplier.zip(constantly(id())); + CheckedSupplier zipF = intSupplier.zip(constantly()); + CheckedSupplier diMapR = intSupplier.diMapR(id()); + CheckedSupplier> carry = intSupplier.carry(); + CheckedSupplier andThen = intSupplier.andThen(id()); + + assertEquals((Integer) 1, intSupplier.get()); + assertEquals("1", fmap.get()); + assertEquals((Integer) 1, flatMap.get()); + assertEquals((Integer) 1, discardL.get()); + assertEquals((Integer) 1, discardR.get()); + assertEquals((Integer) 1, zipA.get()); + assertEquals(UNIT, zipF.get()); + assertEquals((Integer) 1, diMapR.get()); + assertEquals(tuple(UNIT, 1), carry.get()); + assertEquals((Integer) 1, andThen.get()); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java index dc18edd90..7a83e934a 100644 --- a/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import static com.jnape.palatable.lambda.adt.Maybe.just; @@ -15,7 +16,6 @@ import static com.jnape.palatable.lambda.lens.functions.View.view; import static com.jnape.palatable.lambda.lens.lenses.MapLens.keys; import static com.jnape.palatable.lambda.lens.lenses.MapLens.mappingValues; -import static com.jnape.palatable.lambda.lens.lenses.MapLens.valueAt; import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; @@ -27,11 +27,12 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertThat; import static testsupport.assertion.LensAssert.assertLensLawfulness; +import static testsupport.matchers.IterableMatcher.iterates; public class MapLensTest { @Test - public void asCopyFocusesOnMapThroughCopy() { + public void asCopy() { assertLensLawfulness(MapLens.asCopy(), asList(emptyMap(), singletonMap("foo", 1), new HashMap() {{ put("foo", 1); @@ -46,8 +47,29 @@ public void asCopyFocusesOnMapThroughCopy() { } @Test - public void valueAtFocusesOnValueAtKey() { - assertLensLawfulness(valueAt("foo"), + public void asCopyWithCopyFn() { + assertLensLawfulness(MapLens.asCopy(LinkedHashMap::new), + asList(emptyMap(), singletonMap("foo", 1), new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}), + asList(emptyMap(), singletonMap("foo", 1), new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }})); + + assertThat(view(MapLens.asCopy(LinkedHashMap::new), new LinkedHashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}).keySet(), iterates("foo", "bar", "baz")); + } + + @Test + public void valueAt() { + assertLensLawfulness(MapLens.valueAt("foo"), asList(emptyMap(), singletonMap("foo", 1), new HashMap() {{ put("foo", 1); put("bar", 2); @@ -57,8 +79,20 @@ public void valueAtFocusesOnValueAtKey() { } @Test - public void valueAtWithDefaultValueFocusedOnValueAtKey() { - Lens.Simple, Integer> atFoo = valueAt("foo", -1); + public void valueAtWithCopyFn() { + assertLensLawfulness(MapLens.valueAt("foo"), + asList(emptyMap(), singletonMap("foo", 1), new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}), + asList(nothing(), just(1))); + } + + + @Test + public void valueAtWithDefaultValue() { + Lens.Simple, Integer> atFoo = MapLens.valueAt("foo", -1); assertEquals((Integer) 1, view(atFoo, new HashMap() {{ put("foo", 1); diff --git a/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeMapsTest.java b/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeMapsTest.java new file mode 100644 index 000000000..be871be21 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeMapsTest.java @@ -0,0 +1,43 @@ +package com.jnape.palatable.lambda.monoid.builtin; + +import com.jnape.palatable.lambda.monoid.Monoid; +import com.jnape.palatable.lambda.semigroup.Semigroup; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn2.ToMap.toMap; +import static com.jnape.palatable.lambda.monoid.builtin.MergeMaps.mergeMaps; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MergeMapsTest { + private static final Semigroup ADD = (x, y) -> x + y; + private Monoid> merge; + + @Before + public void setUp() { + merge = mergeMaps(HashMap::new, ADD); + } + + @Test + public void identity() { + assertTrue(merge.identity().isEmpty()); + } + + @Test + public void monoid() { + assertEquals(singletonMap("foo", 1), merge.apply(emptyMap(), singletonMap("foo", 1))); + assertEquals(singletonMap("foo", 1), merge.apply(singletonMap("foo", 1), emptyMap())); + assertEquals(singletonMap("foo", 2), + merge.apply(singletonMap("foo", 1), singletonMap("foo", 1))); + assertEquals(toMap(HashMap::new, asList(tuple("foo", 1), tuple("bar", 1))), + merge.apply(singletonMap("foo", 1), singletonMap("bar", 1))); + } +} \ No newline at end of file diff --git a/src/test/java/testsupport/traits/MonadLaws.java b/src/test/java/testsupport/traits/MonadLaws.java index da7ba9e66..038fc6789 100644 --- a/src/test/java/testsupport/traits/MonadLaws.java +++ b/src/test/java/testsupport/traits/MonadLaws.java @@ -12,6 +12,8 @@ import static com.jnape.palatable.lambda.adt.Maybe.nothing; import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Upcast.upcast; +import static com.jnape.palatable.lambda.monad.Monad.join; import static java.util.Arrays.asList; public class MonadLaws implements Trait> { @@ -22,7 +24,8 @@ public void test(Monad m) { ., Maybe>>foldMap(f -> f.apply(m), asList( this::testLeftIdentity, this::testRightIdentity, - this::testAssociativity)) + this::testAssociativity, + this::testJoin)) .peek(s -> { throw new AssertionError("The following Monad laws did not hold for instance of " + m.getClass() + ": \n\t - " + s); }); @@ -32,21 +35,29 @@ private Maybe testLeftIdentity(Monad m) { Object a = new Object(); Fn1> fn = id().andThen(m::pure); return m.pure(a).flatMap(fn).equals(fn.apply(a)) - ? nothing() - : just("left identity (m.pure(a).flatMap(fn).equals(fn.apply(a)))"); + ? nothing() + : just("left identity (m.pure(a).flatMap(fn).equals(fn.apply(a)))"); } private Maybe testRightIdentity(Monad m) { return m.flatMap(m::pure).equals(m) - ? nothing() - : just("right identity: (m.flatMap(m::pure).equals(m))"); + ? nothing() + : just("right identity: (m.flatMap(m::pure).equals(m))"); } private Maybe testAssociativity(Monad m) { Fn1> f = constantly(m.pure(new Object())); Function> g = constantly(m.pure(new Object())); return m.flatMap(f).flatMap(g).equals(m.flatMap(a -> f.apply(a).flatMap(g))) - ? nothing() - : just("associativity: (m.flatMap(f).flatMap(g).equals(m.flatMap(a -> f.apply(a).flatMap(g))))"); + ? nothing() + : just("associativity: (m.flatMap(f).flatMap(g).equals(m.flatMap(a -> f.apply(a).flatMap(g))))"); + } + + private Maybe testJoin(Monad m) { + Monad, M> mma = m.pure(m.fmap(upcast())); + boolean equals = mma.flatMap(id()).equals(join(mma)); + return equals + ? nothing() + : just("join: (m.pure(m).flatMap(id())).equals(Monad.join(m.pure(m)))"); } }