diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..66b54c05c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [jnape] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a96405d..8561f1f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [Unreleased] + +### Changed +- `HList#cons` static factory method auto-promotes to specialized `HList` if there is one +- `EitherT` gains a `MonadError` instance + +### Added +- `MergeHMaps`, a `Monoid` that merges `HMap`s by merging the values via key-specified `Semigroup`s +- `Id#id` overload that accepts an argument and returns it +- `MaybeT#or`, choose the first `MaybeT` that represents an effect around `just` a value +- `MaybeT#filter`, filter a `Maybe` inside an effect +- `StateMatcher, StateTMatcher, WriterTMatcher` +- `ReaderT#and`, category composition between `ReaderT` instances: `(a -> m b) -> (b -> m c) -> (a -> m c)` +- `IterateT`, [`ListT` done right](https://wiki.haskell.org/ListT_done_right) +- `Comparison`, a type-safe sum of `LT`, `EQ`, and `GT` orderings +- `Compare`, a function taking a `Comparator` and returning a `Comparison` +- `Min/Max/...With` variants for inequality testing with a `Comparator` + +## [5.1.0] - 2019-10-13 ### Changed - All monad transformers that can support composable parallelism do support it @@ -539,7 +557,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-5.0.0...HEAD +[Unreleased]: https://github.com/palatable/lambda/compare/lambda-5.1.0...HEAD +[5.1.0]: https://github.com/palatable/lambda/compare/lambda-5.0.0...lambda-5.1.0 [5.0.0]: https://github.com/palatable/lambda/compare/lambda-4.0.0...lambda-5.0.0 [4.0.0]: https://github.com/palatable/lambda/compare/lambda-3.3.0...lambda-4.0.0 [3.3.0]: https://github.com/palatable/lambda/compare/lambda-3.2.0...lambda-3.3.0 diff --git a/README.md b/README.md index be008c911..edaf4dde9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ Functional patterns for Java - [CoProducts](#coproducts) - [Either](#either) - [Lenses](#lenses) - - [Notes](#notes) + - [Notes](#notes) + - [Community](#community) - [License](#license) Background @@ -60,14 +61,14 @@ Add the following dependency to your: com.jnape.palatable lambda - 5.0.0 + 5.1.0 ``` `build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)): ```gradle -compile group: 'com.jnape.palatable', name: 'lambda', version: '5.0.0' +compile group: 'com.jnape.palatable', name: 'lambda', version: '5.1.0' ``` Examples @@ -739,6 +740,13 @@ Wherever possible, _lambda_ maintains interface compatibility with similar, fami Unfortunately, due to Java's type hierarchy and inheritance inconsistencies, this is not always possible. One surprising example of this is how `Fn1` extends `j.u.f.Function`, but `Fn2` does not extend `j.u.f.BiFunction`. This is because `j.u.f.BiFunction` itself does not extend `j.u.f.Function`, but it does define methods that collide with `j.u.f.Function`. For this reason, both `Fn1` and `Fn2` cannot extend their Java counterparts without sacrificing their own inheritance hierarchy. These types of asymmetries are, unfortunately, not uncommon; however, wherever these situations arise, measures are taken to attempt to ease the transition in and out of core Java types (in the case of `Fn2`, a supplemental `#toBiFunction` method is added). I do not take these inconveniences for granted, and I'm regularly looking for ways to minimize the negative impact of this as much as possible. Suggestions and use cases that highlight particular pain points here are particularly appreciated. +Community +----- +There are some open-sourced community projects that depend on _lambda_ for their own functionality: these projects are listed below (note that these projects are _not_ affiliated with lambda, and have their own maintainers). If you use _lambda_ in your own open-sourced project, feel free to create an issue and I'll be happy to review the project and add it to this section! + +- [Enhanced Iterables](https://github.com/kschuetz/enhanced-iterables) - Kevin Schuetz [@kschuetz](https://github.com/kschuetz) +- [Collection Views](https://github.com/kschuetz/collection-views) - Kevin Schuetz [@kschuetz](https://github.com/kschuetz) + License ------- diff --git a/pom.xml b/pom.xml index 0a12b1146..f89d14be0 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ lambda - 5.1.0 + 5.2.0 jar Lambda @@ -74,6 +74,7 @@ org.mockito mockito-core 2.28.2 + test com.jnape.palatable diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/HList.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/HList.java index 91f19edbe..421855a10 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/HList.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/HList.java @@ -1,5 +1,7 @@ package com.jnape.palatable.lambda.adt.hlist; +import com.jnape.palatable.lambda.functions.builtin.fn1.Downcast; + import java.util.Objects; /** @@ -63,7 +65,7 @@ public static HNil nil() { * @return the newly created HList */ public static HCons cons(Head head, Tail tail) { - return new HCons<>(head, tail); + return Downcast., HCons>downcast(tail.cons(head)); } /** @@ -271,7 +273,7 @@ public final boolean equals(Object other) { if (other instanceof HCons) { HCons that = (HCons) other; return this.head.equals(that.head) - && this.tail.equals(that.tail); + && this.tail.equals(that.tail); } return false; } diff --git a/src/main/java/com/jnape/palatable/lambda/functions/Fn1.java b/src/main/java/com/jnape/palatable/lambda/functions/Fn1.java index 3f2be1e92..f21e70e23 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/Fn1.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/Fn1.java @@ -123,7 +123,7 @@ default Fn1 flatMap(Fn1>> f) { } /** - * Also left-to-right composition (sadly). + * Left-to-right composition. * * @param the return type of the next function to invoke * @param f the function to invoke with this function's return value diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Id.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Id.java index c2f8c06ee..c92cf0c81 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Id.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn1/Id.java @@ -23,4 +23,8 @@ public A checkedApply(A a) { public static Id id() { return (Id) INSTANCE; } + + public static A id(A a) { + return Id.id().apply(a); + } } diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqBy.java index 3b483aeaf..a6e04baa9 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqBy.java @@ -6,7 +6,9 @@ import com.jnape.palatable.lambda.functions.specialized.BiPredicate; import com.jnape.palatable.lambda.functions.specialized.Predicate; +import static com.jnape.palatable.lambda.functions.builtin.fn3.CmpEqWith.cmpEqWith; import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; +import static java.util.Comparator.comparing; /** * Given a mapping function from some type A to some {@link Comparable} type B and two values @@ -16,6 +18,7 @@ * @param the value type * @param the mapped comparison type * @see CmpEq + * @see CmpEqWith * @see LTBy * @see GTBy */ @@ -28,7 +31,7 @@ private CmpEqBy() { @Override public Boolean checkedApply(Fn1 compareFn, A x, A y) { - return compareFn.apply(x).compareTo(compareFn.apply(y)) == 0; + return cmpEqWith(comparing(compareFn.toFunction()), x, y); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWith.java new file mode 100644 index 000000000..ff95cd2fc --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWith.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.specialized.BiPredicate; +import com.jnape.palatable.lambda.functions.specialized.Predicate; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.Compare.compare; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.equal; +import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; + +/** + * Given a {@link Comparator} from some type A and two values of type A, return + * true if the first value is strictly equal to the second value (according to + * {@link Comparator#compare(Object, Object)} otherwise, return false. + * + * @param the value type + * @see CmpEqBy + * @see LTBy + * @see GTBy + * @see Compare + */ +public final class CmpEqWith implements Fn3, A, A, Boolean> { + + private static final CmpEqWith INSTANCE = new CmpEqWith<>(); + + private CmpEqWith() { + } + + @Override + public BiPredicate apply(Comparator compareFn) { + return Fn3.super.apply(compareFn)::apply; + } + + @Override + public Predicate apply(Comparator compareFn, A x) { + return predicate(Fn3.super.apply(compareFn, x)); + } + + @Override + public Boolean checkedApply(Comparator comparator, A a, A a2) { + return compare(comparator, a, a2).equals(equal()); + } + + @SuppressWarnings("unchecked") + public static CmpEqWith cmpEqWith() { + return (CmpEqWith) INSTANCE; + } + + public static BiPredicate cmpEqWith(Comparator comparator) { + return CmpEqWith.cmpEqWith().apply(comparator); + } + + public static Predicate cmpEqWith(Comparator comparator, A x) { + return CmpEqWith.cmpEqWith(comparator).apply(x); + } + + public static Boolean cmpEqWith(Comparator comparator, A x, A y) { + return cmpEqWith(comparator, x).apply(y); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/Compare.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/Compare.java new file mode 100644 index 000000000..b388feca9 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/Compare.java @@ -0,0 +1,56 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.ordering.ComparisonRelation; + +import java.util.Comparator; + +/** + * Given a {@link Comparator} from some type A and two values of type A, return a + * {@link ComparisonRelation} of the first value with reference to the second value (according to + * {@link Comparator#compare(Object, Object)}. The order of parameters is flipped with respect to + * {@link Comparator#compare(Object, Object)} for more idiomatic partial application. + *

+ * Example: + *

+ * {@code
+ *  Compare.compare(naturalOrder(), 1, 2); // ComparisonRelation.GreaterThan
+ *  Compare.compare(naturalOrder(), 2, 1); // ComparisonRelation.LessThan
+ *  Compare.compare(naturalOrder(), 1, 1); // ComparisonRelation.Equal
+ * }
+ * 
+ * + * @param
the value type + * @see Comparator + * @see Compare + */ +public final class Compare implements Fn3, A, A, ComparisonRelation> { + private static final Compare INSTANCE = new Compare<>(); + + private Compare() { + } + + @Override + public ComparisonRelation checkedApply(Comparator aComparator, A a, A a2) throws Throwable { + return ComparisonRelation.fromInt(aComparator.compare(a2, a)); + } + + @SuppressWarnings("unchecked") + public static Compare compare() { + return (Compare) INSTANCE; + } + + public static Fn2 compare(Comparator comparator) { + return Compare.compare().apply(comparator); + } + + public static Fn1 compare(Comparator comparator, A a) { + return compare(comparator).apply(a); + } + + public static ComparisonRelation compare(Comparator aComparator, A a, A a2) { + return compare(aComparator, a).apply(a2); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTBy.java index cb9b45168..caddec12e 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTBy.java @@ -6,7 +6,9 @@ import com.jnape.palatable.lambda.functions.specialized.BiPredicate; import com.jnape.palatable.lambda.functions.specialized.Predicate; +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTWith.gtWith; import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; +import static java.util.Comparator.comparing; /** * Given a mapping function from some type A to some {@link Comparable} type B and two values @@ -16,6 +18,7 @@ * @param the value type * @param the mapped comparison type * @see GT + * @see GTWith * @see LTBy */ public final class GTBy> implements Fn3, A, A, Boolean> { @@ -27,7 +30,7 @@ private GTBy() { @Override public Boolean checkedApply(Fn1 compareFn, A y, A x) { - return compareFn.apply(x).compareTo(compareFn.apply(y)) > 0; + return gtWith(comparing(compareFn.toFunction()), y, x); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEBy.java index 30ef09852..7a1b4acad 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEBy.java @@ -6,8 +6,9 @@ import com.jnape.palatable.lambda.functions.specialized.BiPredicate; import com.jnape.palatable.lambda.functions.specialized.Predicate; -import static com.jnape.palatable.lambda.functions.builtin.fn3.CmpEqBy.cmpEqBy; +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTEWith.gteWith; import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; +import static java.util.Comparator.comparing; /** * Given a mapping function from some type A to some {@link Comparable} type B and two values @@ -18,6 +19,7 @@ * @param the value type * @param the mapped comparison type * @see GTE + * @see GTEWith * @see LTEBy */ public final class GTEBy> implements Fn3, A, A, Boolean> { @@ -29,7 +31,7 @@ private GTEBy() { @Override public Boolean checkedApply(Fn1 compareFn, A y, A x) { - return GTBy.gtBy(compareFn).or(cmpEqBy(compareFn)).apply(y, x); + return gteWith(comparing(compareFn.toFunction()), y, x); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWith.java new file mode 100644 index 000000000..29a5dfe82 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWith.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.builtin.fn2.GTE; +import com.jnape.palatable.lambda.functions.specialized.BiPredicate; +import com.jnape.palatable.lambda.functions.specialized.Predicate; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTWith.ltWith; +import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; + +/** + * Given a {@link Comparator} from some type A and two values of type A, + * return true if the second value is greater than or equal to the first value in + * terms of their mapped B results according to {@link Comparator#compare(Object, Object)}; + * otherwise, return false. + * + * @param the value type + * @see GTE + * @see GTEBy + * @see LTEWith + */ +public final class GTEWith implements Fn3, A, A, Boolean> { + + private static final GTEWith INSTANCE = new GTEWith<>(); + + private GTEWith() { + } + + @Override + public BiPredicate apply(Comparator compareFn) { + return Fn3.super.apply(compareFn)::apply; + } + + @Override + public Predicate apply(Comparator compareFn, A x) { + return predicate(Fn3.super.apply(compareFn, x)); + } + + @Override + public Boolean checkedApply(Comparator comparator, A a, A a2) { + return !ltWith(comparator, a, a2); + } + + @SuppressWarnings("unchecked") + public static GTEWith gteWith() { + return (GTEWith) INSTANCE; + } + + public static BiPredicate gteWith(Comparator comparator) { + return GTEWith.gteWith().apply(comparator); + } + + public static Predicate gteWith(Comparator comparator, A y) { + return GTEWith.gteWith(comparator).apply(y); + } + + public static Boolean gteWith(Comparator comparator, A y, A x) { + return gteWith(comparator, y).apply(x); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWith.java new file mode 100644 index 000000000..d65bec00f --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWith.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.builtin.fn2.GT; +import com.jnape.palatable.lambda.functions.specialized.BiPredicate; +import com.jnape.palatable.lambda.functions.specialized.Predicate; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.Compare.compare; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.greaterThan; +import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; + +/** + * Given a {@link Comparator} from some type A and two values of type A, + * return true if the second value is strictly greater than the first value in + * terms of their mapped B results; otherwise, return false. + * + * @param the value type + * @see GT + * @see GTBy + * @see LTWith + */ +public final class GTWith implements Fn3, A, A, Boolean> { + + private static final GTWith INSTANCE = new GTWith<>(); + + private GTWith() { + } + + @Override + public BiPredicate apply(Comparator compareFn) { + return Fn3.super.apply(compareFn)::apply; + } + + @Override + public Predicate apply(Comparator compareFn, A x) { + return predicate(Fn3.super.apply(compareFn, x)); + } + + @SuppressWarnings("unchecked") + public static GTWith gtWith() { + return (GTWith) INSTANCE; + } + + public static BiPredicate gtWith(Comparator comparator) { + return GTWith.gtWith().apply(comparator); + } + + public static Predicate gtWith(Comparator comparator, A y) { + return GTWith.gtWith(comparator).apply(y); + } + + public static Boolean gtWith(Comparator comparator, A y, A x) { + return gtWith(comparator, y).apply(x); + } + + @Override + public Boolean checkedApply(Comparator comparator, A a, A a2) { + return compare(comparator, a, a2).equals(greaterThan()); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTBy.java index c442d9871..05e163844 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTBy.java @@ -6,7 +6,9 @@ import com.jnape.palatable.lambda.functions.specialized.BiPredicate; import com.jnape.palatable.lambda.functions.specialized.Predicate; +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTWith.ltWith; import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; +import static java.util.Comparator.comparing; /** * Given a mapping function from some type A to some {@link Comparable} type B and two values @@ -27,7 +29,7 @@ private LTBy() { @Override public Boolean checkedApply(Fn1 compareFn, A y, A x) { - return compareFn.apply(x).compareTo(compareFn.apply(y)) < 0; + return ltWith(comparing(compareFn.toFunction()), y, x); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEBy.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEBy.java index bc755470d..4c377d583 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEBy.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEBy.java @@ -6,7 +6,8 @@ import com.jnape.palatable.lambda.functions.specialized.BiPredicate; import com.jnape.palatable.lambda.functions.specialized.Predicate; -import static com.jnape.palatable.lambda.functions.builtin.fn3.CmpEqBy.cmpEqBy; +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTEWith.lteWith; +import static java.util.Comparator.comparing; /** * Given a mapping function from some type A to some {@link Comparable} type B and two values @@ -28,7 +29,7 @@ private LTEBy() { @Override public Boolean checkedApply(Fn1 compareFn, A y, A x) { - return LTBy.ltBy(compareFn).or(cmpEqBy(compareFn)).apply(y, x); + return lteWith(comparing(compareFn.toFunction()), y, x); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWith.java new file mode 100644 index 000000000..403f5c15f --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWith.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.builtin.fn2.LTE; +import com.jnape.palatable.lambda.functions.specialized.BiPredicate; +import com.jnape.palatable.lambda.functions.specialized.Predicate; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTWith.gtWith; +import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; + +/** + * Given a {@link Comparator} from some type A and two values of type A, + * return true if the second value is less than or equal to the first value in + * terms of their mapped B results according to {@link Comparator#compare(Object, Object)}; + * otherwise, return false. + * + * @param the value type + * @see LTE + * @see LTEBy + * @see GTEWith + */ +public final class LTEWith implements Fn3, A, A, Boolean> { + + private static final LTEWith INSTANCE = new LTEWith<>(); + + private LTEWith() { + } + + @Override + public BiPredicate apply(Comparator compareFn) { + return Fn3.super.apply(compareFn)::apply; + } + + @Override + public Predicate apply(Comparator compareFn, A x) { + return predicate(Fn3.super.apply(compareFn, x)); + } + + @Override + public Boolean checkedApply(Comparator comparator, A a, A a2) { + return !gtWith(comparator, a, a2); + } + + @SuppressWarnings("unchecked") + public static LTEWith lteWith() { + return (LTEWith) INSTANCE; + } + + public static BiPredicate lteWith(Comparator comparator) { + return LTEWith.lteWith().apply(comparator); + } + + public static Predicate lteWith(Comparator comparator, A y) { + return LTEWith.lteWith(comparator).apply(y); + } + + public static Boolean lteWith(Comparator comparator, A y, A x) { + return lteWith(comparator, y).apply(x); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWith.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWith.java new file mode 100644 index 000000000..14652a4c6 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWith.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import com.jnape.palatable.lambda.functions.Fn3; +import com.jnape.palatable.lambda.functions.builtin.fn2.LT; +import com.jnape.palatable.lambda.functions.specialized.BiPredicate; +import com.jnape.palatable.lambda.functions.specialized.Predicate; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.Compare.compare; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.lessThan; +import static com.jnape.palatable.lambda.functions.specialized.Predicate.predicate; + +/** + * Given a comparator for some type A and two values of type A, + * return true if the second value is strictly less than than the first value in + * terms of their mapped B results; otherwise, return false. + * + * @param the value type + * @see LT + * @see LTBy + * @see GTWith + */ +public final class LTWith implements Fn3, A, A, Boolean> { + + private static final LTWith INSTANCE = new LTWith<>(); + + private LTWith() { + } + + @Override + public BiPredicate apply(Comparator compareFn) { + return Fn3.super.apply(compareFn)::apply; + } + + @Override + public Predicate apply(Comparator compareFn, A x) { + return predicate(Fn3.super.apply(compareFn, x)); + } + + @SuppressWarnings("unchecked") + public static LTWith ltWith() { + return (LTWith) INSTANCE; + } + + public static BiPredicate ltWith(Comparator comparator) { + return LTWith.ltWith().apply(comparator); + } + + public static Predicate ltWith(Comparator comparator, A y) { + return LTWith.ltWith(comparator).apply(y); + } + + public static Boolean ltWith(Comparator comparator, A y, A x) { + return ltWith(comparator, y).apply(x); + } + + @Override + public Boolean checkedApply(Comparator comparator, A a, A a2) { + return compare(comparator, a, a2).equals(lessThan()); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelation.java b/src/main/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelation.java new file mode 100644 index 000000000..0c4bb93c3 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelation.java @@ -0,0 +1,102 @@ +package com.jnape.palatable.lambda.functions.ordering; + +import com.jnape.palatable.lambda.adt.coproduct.CoProduct3; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.builtin.fn3.Compare; + +import java.util.Comparator; + +/** + * Specialized {@link CoProduct3} representing the possible results of a ordered comparison. + * Used by {@link Compare} as the result of a comparison. + * + * @see Compare + */ +public abstract class ComparisonRelation + implements CoProduct3< + ComparisonRelation.LessThan, + ComparisonRelation.Equal, + ComparisonRelation.GreaterThan, + ComparisonRelation> { + + private ComparisonRelation() { + } + + /** + * Return a comparison relation from the result of a {@link Comparator} or {@link Comparable} result + * + * @param signifier The result of {@link Comparator#compare(Object, Object)} or {@link Comparable#compareTo(Object)} + * @return The intended {@link ComparisonRelation} of the signifier + */ + public static ComparisonRelation fromInt(int signifier) { + return signifier > 0 ? greaterThan() : signifier == 0 ? equal() : lessThan(); + } + + public static GreaterThan greaterThan() { + return GreaterThan.INSTANCE; + } + + public static LessThan lessThan() { + return LessThan.INSTANCE; + } + + public static Equal equal() { + return Equal.INSTANCE; + } + + public static final class LessThan extends ComparisonRelation { + private static final LessThan INSTANCE = new LessThan(); + + private LessThan() { + } + + @Override + public String toString() { + return "LessThan"; + } + + @Override + public R match(Fn1 aFn, + Fn1 bFn, + Fn1 cFn) { + return aFn.apply(this); + } + } + + public static final class Equal extends ComparisonRelation { + private static final Equal INSTANCE = new Equal(); + + private Equal() { + } + + public String toString() { + return "Equal"; + } + + @Override + public R match(Fn1 aFn, + Fn1 bFn, + Fn1 cFn) { + return bFn.apply(this); + } + } + + public static final class GreaterThan extends ComparisonRelation { + private static final GreaterThan INSTANCE = new GreaterThan(); + + private GreaterThan() { + } + + @Override + public String toString() { + return "GreaterThan"; + } + + @Override + public R match(Fn1 aFn, + Fn1 bFn, + Fn1 cFn) { + return cFn.apply(this); + } + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functor/builtin/Writer.java b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Writer.java index bb00cb402..c4c6d752d 100644 --- a/src/main/java/com/jnape/palatable/lambda/functor/builtin/Writer.java +++ b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Writer.java @@ -136,6 +136,7 @@ public Writer discardR(Applicative> appB) { /** * Construct a {@link Writer} from an accumulation. * + * @param w the accumulation * @param the accumulation type * @return the {@link Writer} */ @@ -146,6 +147,7 @@ public static Writer tell(W w) { /** * Construct a {@link Writer} from a value. * + * @param a the output value * @param the accumulation type * @param the value type * @return the {@link Writer} @@ -157,6 +159,7 @@ public static Writer listen(A a) { /** * Construct a {@link Writer} from an accumulation and a value. * + * @param aw the output value and accumulation * @param the accumulation type * @param the value type * @return the {@link WriterT} diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableQueue.java b/src/main/java/com/jnape/palatable/lambda/internal/ImmutableQueue.java similarity index 70% rename from src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableQueue.java rename to src/main/java/com/jnape/palatable/lambda/internal/ImmutableQueue.java index 1b683c870..dd605015e 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableQueue.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/ImmutableQueue.java @@ -1,4 +1,4 @@ -package com.jnape.palatable.lambda.internal.iteration; +package com.jnape.palatable.lambda.internal; import com.jnape.palatable.lambda.adt.Maybe; @@ -8,22 +8,29 @@ import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; import static com.jnape.palatable.lambda.functions.builtin.fn3.FoldLeft.foldLeft; -abstract class ImmutableQueue implements Iterable { +/** + * Internal API. Use at your own peril. + */ +public abstract class ImmutableQueue implements Iterable { - abstract ImmutableQueue pushFront(A a); + public abstract ImmutableQueue pushFront(A a); - abstract ImmutableQueue pushBack(A a); + public abstract ImmutableQueue pushBack(A a); - abstract Maybe head(); + public abstract Maybe head(); - abstract ImmutableQueue tail(); + public abstract ImmutableQueue tail(); - abstract ImmutableQueue concat(ImmutableQueue other); + public abstract ImmutableQueue concat(ImmutableQueue other); - final boolean isEmpty() { + public final boolean isEmpty() { return head().fmap(constantly(false)).orElse(true); } + public static ImmutableQueue singleton(A a) { + return new NonEmpty<>(ImmutableStack.empty().push(a), ImmutableStack.empty()); + } + @Override public Iterator iterator() { return new Iterator() { @@ -52,27 +59,27 @@ private static final class Empty extends ImmutableQueue { private static final Empty INSTANCE = new Empty<>(); @Override - ImmutableQueue pushFront(A a) { + public ImmutableQueue pushFront(A a) { return new NonEmpty<>(ImmutableStack.empty().push(a), ImmutableStack.empty()); } @Override - ImmutableQueue pushBack(A a) { + public ImmutableQueue pushBack(A a) { return pushFront(a); } @Override - ImmutableQueue concat(ImmutableQueue other) { + public ImmutableQueue concat(ImmutableQueue other) { return other; } @Override - Maybe head() { + public Maybe head() { return Maybe.nothing(); } @Override - ImmutableQueue tail() { + public ImmutableQueue tail() { return this; } } @@ -87,27 +94,27 @@ private NonEmpty(ImmutableStack outbound, ImmutableStack inbound) { } @Override - ImmutableQueue pushFront(A a) { + public ImmutableQueue pushFront(A a) { return new NonEmpty<>(outbound.push(a), inbound); } @Override - ImmutableQueue pushBack(A a) { + public ImmutableQueue pushBack(A a) { return new NonEmpty<>(outbound, inbound.push(a)); } @Override - ImmutableQueue concat(ImmutableQueue other) { + public ImmutableQueue concat(ImmutableQueue other) { return new NonEmpty<>(outbound, foldLeft(ImmutableStack::push, inbound, other)); } @Override - Maybe head() { + public Maybe head() { return outbound.head(); } @Override - ImmutableQueue tail() { + public ImmutableQueue tail() { ImmutableStack outTail = outbound.tail(); if (!outTail.isEmpty()) return new NonEmpty<>(outTail, inbound); diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableStack.java b/src/main/java/com/jnape/palatable/lambda/internal/ImmutableStack.java similarity index 77% rename from src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableStack.java rename to src/main/java/com/jnape/palatable/lambda/internal/ImmutableStack.java index a337ec21b..cc6e7175b 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ImmutableStack.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/ImmutableStack.java @@ -1,4 +1,4 @@ -package com.jnape.palatable.lambda.internal.iteration; +package com.jnape.palatable.lambda.internal; import com.jnape.palatable.lambda.adt.Maybe; @@ -7,17 +7,20 @@ import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; -abstract class ImmutableStack implements Iterable { +/** + * Internal API. Use at your own peril. + */ +public abstract class ImmutableStack implements Iterable { - final ImmutableStack push(A a) { + public final ImmutableStack push(A a) { return new Node<>(a, this); } - abstract Maybe head(); + public abstract Maybe head(); - abstract ImmutableStack tail(); + public abstract ImmutableStack tail(); - final boolean isEmpty() { + public final boolean isEmpty() { return head().fmap(constantly(false)).orElse(true); } @@ -49,12 +52,12 @@ private static final class Empty extends ImmutableStack { private static final Empty INSTANCE = new Empty<>(); @Override - Maybe head() { + public Maybe head() { return Maybe.nothing(); } @Override - ImmutableStack tail() { + public ImmutableStack tail() { return this; } } @@ -69,12 +72,12 @@ public Node(A head, ImmutableStack tail) { } @Override - Maybe head() { + public Maybe head() { return Maybe.just(head); } @Override - ImmutableStack tail() { + public ImmutableStack tail() { return tail; } } diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ConcatenatingIterable.java b/src/main/java/com/jnape/palatable/lambda/internal/iteration/ConcatenatingIterable.java index 594be70b2..f1ff3a779 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/ConcatenatingIterable.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/iteration/ConcatenatingIterable.java @@ -1,5 +1,7 @@ package com.jnape.palatable.lambda.internal.iteration; +import com.jnape.palatable.lambda.internal.ImmutableQueue; + import java.util.Iterator; import static com.jnape.palatable.lambda.functions.builtin.fn1.Flatten.flatten; diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/TrampoliningIterator.java b/src/main/java/com/jnape/palatable/lambda/internal/iteration/TrampoliningIterator.java index fa0350d08..3b2a7b37f 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/TrampoliningIterator.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/iteration/TrampoliningIterator.java @@ -3,6 +3,7 @@ import com.jnape.palatable.lambda.functions.Fn0; import com.jnape.palatable.lambda.functions.Fn1; import com.jnape.palatable.lambda.functions.recursion.RecursiveResult; +import com.jnape.palatable.lambda.internal.ImmutableQueue; import java.util.Iterator; import java.util.NoSuchElementException; diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherT.java index 806fdb636..a23a81fcf 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherT.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherT.java @@ -10,6 +10,7 @@ import com.jnape.palatable.lambda.functor.builtin.Compose; import com.jnape.palatable.lambda.functor.builtin.Lazy; import com.jnape.palatable.lambda.monad.Monad; +import com.jnape.palatable.lambda.monad.MonadError; import com.jnape.palatable.lambda.monad.MonadRec; import com.jnape.palatable.lambda.monad.transformer.MonadT; @@ -28,7 +29,8 @@ */ public final class EitherT, L, R> implements Bifunctor>, - MonadT, EitherT> { + MonadT, EitherT>, + MonadError> { private final MonadRec, M> melr; @@ -120,6 +122,24 @@ public EitherT discardR(Applicative> appB) { return MonadT.super.discardR(appB).coerce(); } + /** + * {@inheritDoc} + */ + @Override + public EitherT throwError(L l) { + return eitherT(melr.pure(left(l))); + } + + /** + * {@inheritDoc} + */ + @Override + public EitherT catchError(Fn1>> recoveryFn) { + return eitherT(runEitherT().flatMap(e -> e.match( + l -> recoveryFn.apply(l).>coerce().runEitherT(), + r -> melr.pure(r).fmap(Either::right)))); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateT.java new file mode 100644 index 000000000..606e7d894 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateT.java @@ -0,0 +1,454 @@ +package com.jnape.palatable.lambda.monad.transformer.builtin; + +import com.jnape.palatable.lambda.adt.Maybe; +import com.jnape.palatable.lambda.adt.Unit; +import com.jnape.palatable.lambda.adt.choice.Choice2; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.functions.Fn0; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functions.recursion.RecursiveResult; +import com.jnape.palatable.lambda.functions.specialized.Pure; +import com.jnape.palatable.lambda.functor.Applicative; +import com.jnape.palatable.lambda.functor.builtin.Lazy; +import com.jnape.palatable.lambda.internal.ImmutableQueue; +import com.jnape.palatable.lambda.io.IO; +import com.jnape.palatable.lambda.monad.Monad; +import com.jnape.palatable.lambda.monad.MonadRec; +import com.jnape.palatable.lambda.monad.transformer.MonadT; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +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.Unit.UNIT; +import static com.jnape.palatable.lambda.adt.choice.Choice2.a; +import static com.jnape.palatable.lambda.adt.choice.Choice2.b; +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Into.into; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Tupler2.tupler; +import static com.jnape.palatable.lambda.functions.builtin.fn3.FoldLeft.foldLeft; +import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.recurse; +import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.terminate; +import static com.jnape.palatable.lambda.io.IO.io; +import static com.jnape.palatable.lambda.monad.Monad.join; +import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.maybeT; +import static java.util.Arrays.asList; + +/** + * A {@link MonadT monad transformer} over a co-inductive, singly-linked spine of values embedded in effects. This is + * analogous to Haskell's ListT (done right). All append + * operations ({@link IterateT#cons(MonadRec) cons}, {@link IterateT#snoc(MonadRec) snoc}, etc.) are O(1) space/time + * complexity. + *

+ * Due to its singly-linked embedded design, {@link IterateT} is a canonical example of purely-functional streaming + * computation. For example, to lazily print all lines from a file descriptor, an initial implementation using + * {@link IterateT} might take the following form: + *


+ * String filePath = "/tmp/a_tale_of_two_cities.txt";
+ * IterateT<IO<?>, String> streamLines = IterateT.unfold(
+ *         reader -> io(() -> maybe(reader.readLine()).fmap(line -> tuple(line, reader))),
+ *         io(() -> Files.newBufferedReader(Paths.get(filePath))));
+ *
+ * // iterative read and print lines without retaining references
+ * IO<Unit> printLines = streamLines.forEach(line -> io(() -> System.out.println(line)));
+ * printLines.unsafePerformIO(); // prints "It was the best of times, it was the worst of times, [...]"
+ * 
+ * + * @param the effect type + * @param the element type + */ +public class IterateT, A> implements + MonadT, IterateT> { + + private final Pure pureM; + private final ImmutableQueue> conses; + private final ImmutableQueue>>, M>>, IterateT>> middles; + private final ImmutableQueue> snocs; + + private IterateT(Pure pureM, + ImmutableQueue> conses, + ImmutableQueue>>, M>>, IterateT>> middles, + ImmutableQueue> snocs) { + this.pureM = pureM; + this.conses = conses; + this.middles = middles; + this.snocs = snocs; + } + + /** + * Recover the full structure of the embedded {@link Monad}. + * + * @param the witnessed target type + * @return the embedded {@link Monad} + */ + public >>, M>> MMTA runIterateT() { + return pureM., MonadRec, M>>apply(this).trampolineM(IterateT::resume).coerce(); + } + + /** + * Add an element inside an effect to the front of this {@link IterateT}. + * + * @param head the element + * @return the cons'ed {@link IterateT} + */ + public final IterateT cons(MonadRec head) { + return new IterateT<>(pureM, conses.pushFront(head), middles, snocs); + } + + /** + * Add an element inside an effect to the back of this {@link IterateT}. + * + * @param last the element + * @return the snoc'ed {@link IterateT} + */ + public final IterateT snoc(MonadRec last) { + return new IterateT<>(pureM, conses, middles, snocs.pushBack(last)); + } + + /** + * Concat this {@link IterateT} in front of the other {@link IterateT}. + * + * @param other the other {@link IterateT} + * @return the concatenated {@link IterateT} + */ + public IterateT concat(IterateT other) { + return new IterateT<>(pureM, + conses, + middles.pushBack(b(new IterateT<>(pureM, + snocs.concat(other.conses), + other.middles, + other.snocs))), + ImmutableQueue.empty()); + } + + /** + * Monolithically fold the spine of this {@link IterateT} by {@link MonadRec#trampolineM(Fn1) trampolining} the + * underlying effects (for iterative folding, use {@link IterateT#trampolineM(Fn1) trampolineM} directly). + * + * @param fn the folding function + * @param acc the starting accumulation effect + * @param the accumulation type + * @param the witnessed target result type + * @return the folded effect result + */ + public > MB fold(Fn2> fn, + MonadRec acc) { + return foldCut((b, a) -> fn.apply(b, a).fmap(RecursiveResult::recurse), acc); + } + + /** + * Monolithically fold the spine of this {@link IterateT} (with the possibility of early termination) by + * {@link MonadRec#trampolineM(Fn1) trampolining} the underlying effects (for iterative folding, use + * {@link IterateT#trampolineM(Fn1) trampolineM} directly). + * + * @param fn the folding function + * @param acc the starting accumulation effect + * @param the accumulation type + * @param the witnessed target result type + * @return the folded effect result + */ + public > MB foldCut( + Fn2, M>> fn, + MonadRec acc) { + return acc.fmap(tupler(this)) + .trampolineM(into((as, b) -> maybeT(as.runIterateT()) + .flatMap(into((a, aas) -> maybeT(fn.apply(b, a).fmap(Maybe::just)).fmap(tupler(aas)))) + .runMaybeT() + .fmap(maybeR -> maybeR.match( + __ -> terminate(b), + into((rest, rr) -> rr.biMapL(tupler(rest))))))) + .coerce(); + } + + /** + * Convenience method for {@link IterateT#fold(Fn2, MonadRec) folding} the spine of this {@link IterateT} with + * an action to perform on each element without accumulating any results. + * + * @param fn the action to perform on each element + * @param the witnessed target result type + * @return the folded effect result + */ + public > MU forEach(Fn1> fn) { + return fold((__, a) -> fn.apply(a), runIterateT().pure(UNIT)); + } + + /** + * {@inheritDoc} + */ + @Override + public > IterateT lift(MonadRec nb) { + return singleton(nb); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT trampolineM( + Fn1, IterateT>> fn) { + return trampolineM(fn, ImmutableQueue.>>empty().pushBack(flatMap(fn))); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT flatMap(Fn1>> f) { + return suspended(() -> maybeT(runIterateT()) + .flatMap(into((a, as) -> maybeT(f.apply(a) + .>coerce() + .concat(as.flatMap(f)) + .runIterateT()))) + .runMaybeT(), pureM); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT fmap(Fn1 fn) { + return MonadT.super.fmap(fn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT pure(B b) { + return singleton(runIterateT().pure(b)); + } + + /** + * Force the underlying spine of this {@link IterateT} into a {@link Collection} of type C inside the + * context of the monadic effect, using the provided cFn0 to construct the initial instance. + *

+ * Note that this is a fundamentally monolithic operation - meaning that incremental progress is not possible - and + * as such, calling this on an infinite {@link IterateT} will result in either heap exhaustion (e.g. in the case of + * {@link List lists}) or non-termination (e.g. in the case of {@link Set sets}). + * + * @param cFn0 the {@link Collection} construction function + * @param the {@link Collection} type + * @param the witnessed target type + * @return the {@link List} inside of the effect + */ + public , MAS extends MonadRec> MAS toCollection(Fn0 cFn0) { + MonadRec>>, M> mmta = runIterateT(); + return fold((c, a) -> { + c.add(a); + return mmta.pure(c); + }, mmta.pure(cFn0.apply())); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT zip(Applicative, IterateT> appFn) { + return suspended(() -> { + MonadRec>>, M> mmta = runIterateT(); + return join(maybeT(mmta).zip( + maybeT(appFn.>>coerce().runIterateT()) + .fmap(into((f, fs) -> into((a, as) -> maybeT( + as.fmap(f) + .cons(mmta.pure(f.apply(a))) + .concat(as.cons(mmta.pure(a)).zip(fs)) + .runIterateT())))))) + .runMaybeT(); + }, pureM); + } + + /** + * {@inheritDoc} + */ + @Override + public Lazy> lazyZip( + Lazy, IterateT>> lazyAppFn) { + return lazyAppFn.fmap(this::zip); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT discardL(Applicative> appB) { + return MonadT.super.discardL(appB).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT discardR(Applicative> appB) { + return MonadT.super.discardR(appB).coerce(); + } + + private MonadRec, Maybe>>>, M> resume() { + return conses.head().match( + __ -> middles.head().match( + ___ -> snocs.head().match( + ____ -> pureM.apply(terminate(nothing())), + ma -> ma.fmap(a -> terminate(just(tuple(a, new IterateT<>(pureM, + snocs.tail(), + ImmutableQueue.empty(), + ImmutableQueue.empty())))))), + lazyOrStrict -> lazyOrStrict.match( + lazy -> lazy.apply().fmap(maybeRes -> maybeRes.match( + ___ -> recurse(new IterateT<>(pureM, ImmutableQueue.empty(), middles.tail(), snocs)), + into((a, as) -> recurse(new IterateT<>(pureM, + ImmutableQueue.singleton(pureM.apply(a)), + ImmutableQueue.singleton(b(migrateForward(as))), + ImmutableQueue.empty()))) + )), + strict -> pureM.apply(recurse(migrateForward(strict))))), + ma -> ma.fmap(a -> terminate(just(tuple(a, new IterateT<>(pureM, conses.tail(), middles, snocs)))))); + } + + private IterateT migrateForward(IterateT as) { + if (middles.tail().isEmpty()) { + return new IterateT<>(pureM, conses.concat(as.conses), as.middles, as.snocs.concat(snocs)); + } + + IterateT lasts = new IterateT<>(pureM, as.snocs, middles.tail(), snocs); + return new IterateT<>(pureM, as.conses, as.middles.pushBack(b(lasts)), ImmutableQueue.empty()); + } + + private IterateT trampolineM(Fn1, IterateT>> fn, + ImmutableQueue>> queued) { + return suspended(() -> { + MonadRec>>, M> pureQueue = pureM.apply(queued); + return pureQueue.trampolineM( + q -> q.head().match( + __ -> pureM.apply(terminate(nothing())), + next -> next.runIterateT().flatMap(maybeMore -> maybeMore.match( + __ -> pureM.apply(terminate(nothing())), + t -> t.into((aOrB, rest) -> aOrB.match( + a -> pureM.apply(recurse(q.pushFront(fn.apply(a).coerce()))), + b -> trampolineM(fn, q.tail().pushFront(rest)) + .cons(pureM.apply(b)) + .runIterateT() + .fmap(RecursiveResult::terminate))))))); + }, pureM); + } + + /** + * Static factory method for creating an empty {@link IterateT}. + * + * @param pureM the {@link Pure} method for the effect + * @param the effect type + * @param the element type + * @return the empty {@link IterateT} + */ + public static , A> IterateT empty(Pure pureM) { + return new IterateT<>(pureM, ImmutableQueue.empty(), ImmutableQueue.empty(), ImmutableQueue.empty()); + } + + /** + * Static factory method for creating an {@link IterateT} from a single element. + * + * @param ma the element + * @param the effect type + * @param the element type + * @return the singleton {@link IterateT} + */ + public static , A> IterateT singleton(MonadRec ma) { + return new IterateT<>(Pure.of(ma), + ImmutableQueue.>empty().pushFront(ma), + ImmutableQueue.empty(), + ImmutableQueue.empty()); + } + + /** + * Static factory method for wrapping an uncons of an {@link IterateT} in an {@link IterateT}. + * + * @param unwrapped the uncons + * @param the effect type + * @param the element type + * @return the wrapped {@link IterateT} + */ + public static , A> IterateT iterateT( + MonadRec>>, M> unwrapped) { + return new IterateT<>( + Pure.of(unwrapped), + ImmutableQueue.empty(), + ImmutableQueue.>>, M>>, IterateT>>empty() + .pushFront(a(() -> unwrapped)), + ImmutableQueue.empty()); + } + + /** + * Static factory method for creating an {@link IterateT} from a spine represented by one or more elements. + * + * @param ma the head element + * @param mas the tail elements + * @param the effect type + * @param the element type + * @return the {@link IterateT} + */ + @SafeVarargs + public static , A> IterateT of( + MonadRec ma, MonadRec... mas) { + @SuppressWarnings("varargs") + List> as = asList(mas); + return foldLeft(IterateT::snoc, + empty(Pure.of(ma)), + com.jnape.palatable.lambda.functions.builtin.fn2.Cons.cons(ma, as)); + } + + /** + * Lazily unfold an {@link IterateT} from an unfolding function fn and a starting seed value + * mb by successively applying fn to the latest seed value, producing {@link Maybe maybe} + * a value to yield out and the next seed value for the subsequent computation. + * + * @param fn the unfolding function + * @param mb the starting seed value + * @param the effect type + * @param the element type + * @param the seed type + * @return the lazily unfolding {@link IterateT} + */ + public static , A, B> IterateT unfold( + Fn1>, M>> fn, MonadRec mb) { + return suspended(() -> maybeT(mb.flatMap(fn)) + .fmap(ab -> ab.fmap(b -> unfold(fn, mb.pure(b)))) + .runMaybeT(), Pure.of(mb)); + } + + /** + * Create an {@link IterateT} from a suspended computation that yields the spine of the {@link IterateT} inside the + * effect. + * + * @param thunk the suspended computation + * @param pureM the {@link Pure} method for the effect + * @param the effect type + * @param the element type + * @return the {@link IterateT} + */ + public static , A> IterateT suspended( + Fn0>>, M>> thunk, Pure pureM) { + return new IterateT<>(pureM, + ImmutableQueue.empty(), + ImmutableQueue + .>>, M>>, IterateT>>empty() + .pushFront(a(thunk)), + ImmutableQueue.empty()); + } + + /** + * Lazily unfold an {@link IterateT} from an {@link Iterator} inside {@link IO}. + * + * @param as the {@link Iterator} + * @param the element type + * @return the {@link IterateT} + */ + public static IterateT, A> fromIterator(Iterator as) { + return unfold(it -> io(() -> { + if (as.hasNext()) + return just(tuple(as.next(), as)); + return nothing(); + }), io(() -> as)); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeT.java index ca588a7bc..c2e02fd94 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeT.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeT.java @@ -44,6 +44,29 @@ public , M>> MMA runMaybeT() { return mma.coerce(); } + /** + * If the embedded value is present and satisfies predicate + * then return just the embedded value + * + * @param predicate the predicate to apply to the embedded value + * @return maybe the satisfied value embedded under M + */ + public MaybeT filter(Fn1 predicate) { + return maybeT(mma.fmap(ma -> ma.filter(predicate))); + } + + /** + * Returns the first {@link MaybeT} that is an effect around {@link Maybe#just(Object) just} a result. + * + * @param other the other {@link MaybeT} + * @return the first present {@link MaybeT} + */ + public MaybeT or(MaybeT other) { + MonadRec, M> mMaybeA = runMaybeT(); + return maybeT(mMaybeA.flatMap(maybeA -> maybeA.match(constantly(other.runMaybeT()), + a -> mMaybeA.pure(just(a))))); + } + /** * {@inheritDoc} */ @@ -134,7 +157,7 @@ public MaybeT discardR(Applicative> appB) { @Override public boolean equals(Object other) { - return other instanceof MaybeT && Objects.equals(mma, ((MaybeT) other).mma); + return other instanceof MaybeT && Objects.equals(mma, ((MaybeT) other).mma); } @Override diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderT.java index 2af18e16d..443470c6e 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderT.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderT.java @@ -61,6 +61,18 @@ public , N extends MonadRec, B> ReaderT return readerT(r -> fn.apply(runReaderT(r).coerce())); } + /** + * Left-to-right composition between {@link ReaderT} instances running under the same effect and compatible between + * their inputs and outputs. + * + * @param amb the next {@link ReaderT} to run + * @param the final output type + * @return the composed {@link ReaderT} + */ + public ReaderT and(ReaderT amb) { + return readerT(r -> runReaderT(r).flatMap(amb::runReaderT)); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterT.java index aae118a15..7a0205543 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterT.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterT.java @@ -46,13 +46,37 @@ private WriterT(Fn1, ? extends MonadRec, M>> writ * accumulation and the result inside the {@link Monad}. * * @param monoid the accumulation {@link Monoid} - * @param the inferred {@link Monad} result + * @param the inferred {@link MonadRec} result * @return the accumulation with the result */ public , M>> MAW runWriterT(Monoid monoid) { return writerFn.apply(monoid).coerce(); } + /** + * Given a {@link Monoid} for the accumulation, run the computation represented by this {@link WriterT} inside the + * {@link Monad monadic effect}, ignoring the resulting accumulation, yielding the value in isolation. + * + * @param monoid the accumulation {@link Monoid} + * @param the inferred {@link MonadRec} result + * @return the result + */ + public > MA evalWriterT(Monoid monoid) { + return runWriterT(monoid).fmap(Tuple2::_1).coerce(); + } + + /** + * Given a {@link Monoid} for the accumulation, run the computation represented by this {@link WriterT} inside the + * {@link Monad monadic effect}, ignoring the value, yielding the accumulation in isolation. + * + * @param monoid the accumulation {@link Monoid} + * @param the inferred {@link MonadRec} accumulation + * @return the accumulation + */ + public > MW execWriterT(Monoid monoid) { + return runWriterT(monoid).fmap(Tuple2::_2).coerce(); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMaps.java b/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMaps.java new file mode 100644 index 000000000..fc2f28c95 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMaps.java @@ -0,0 +1,81 @@ +package com.jnape.palatable.lambda.monoid.builtin; + +import com.jnape.palatable.lambda.adt.Maybe; +import com.jnape.palatable.lambda.adt.hmap.HMap; +import com.jnape.palatable.lambda.adt.hmap.TypeSafeKey; +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functions.builtin.fn3.FoldLeft; +import com.jnape.palatable.lambda.monoid.Monoid; +import com.jnape.palatable.lambda.semigroup.Semigroup; + +import java.util.Map; + +import static com.jnape.palatable.lambda.adt.Maybe.just; +import static com.jnape.palatable.lambda.adt.Maybe.maybe; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Map.map; +import static com.jnape.palatable.lambda.monoid.builtin.Last.last; +import static com.jnape.palatable.lambda.monoid.builtin.Present.present; +import static com.jnape.palatable.lambda.optics.functions.Set.set; +import static com.jnape.palatable.lambda.optics.lenses.MapLens.valueAt; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; + +/** + * A {@link Monoid} instance formed by merging {@link HMap HMaps} using the chosen + * {@link TypeSafeKey} -> {@link Semigroup} + * {@link MergeHMaps#key(TypeSafeKey, Semigroup) mappings}, defaulting to {@link Last} in case no + * {@link Semigroup} has been chosen for a given {@link TypeSafeKey}. + */ +public final class MergeHMaps implements Monoid { + + private final Map, Fn2> bindings; + private final Φ> defaultBinding; + + private MergeHMaps(Map, Fn2> bindings, + Φ> defaultBinding) { + this.bindings = bindings; + this.defaultBinding = defaultBinding; + } + + public MergeHMaps key(TypeSafeKey key, Semigroup semigroup) { + return new MergeHMaps(set(valueAt(key), just(merge(key, present(semigroup))), bindings), defaultBinding); + } + + @Override + public HMap identity() { + return HMap.emptyHMap(); + } + + @Override + public HMap checkedApply(HMap x, HMap y) throws Throwable { + return reduceLeft(asList(x, y)); + } + + @Override + public HMap foldMap(Fn1 fn, Iterable bs) { + return FoldLeft.foldLeft((acc, m) -> FoldLeft.foldLeft((result, k) -> maybe(bindings.get(k)) + .orElseGet(() -> defaultBinding.eliminate(k)) + .apply(result, m), acc, m.keys()), identity(), map(fn, bs)); + } + + public static MergeHMaps mergeHMaps() { + return new MergeHMaps(emptyMap(), new Φ>() { + @Override + public Fn2 eliminate(TypeSafeKey key) { + return merge(key, last()); + } + }); + } + + private static Fn2 merge(TypeSafeKey key, Semigroup> semigroup) { + return (x, y) -> semigroup.apply(x.get(key), y.get(key)) + .fmap(a -> x.put(key, a)) + .orElse(x); + } + + @SuppressWarnings({"NonAsciiCharacters"}) + private interface Φ { + R eliminate(TypeSafeKey key); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxBy.java b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxBy.java index 78d361ba8..2abee980e 100644 --- a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxBy.java +++ b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxBy.java @@ -18,6 +18,7 @@ * @param the value type * @param the mapped comparison type * @see Max + * @see MaxWith * @see MinBy */ public final class MaxBy> implements SemigroupFactory, A> { diff --git a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWith.java b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWith.java new file mode 100644 index 000000000..ed3f07373 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWith.java @@ -0,0 +1,52 @@ +package com.jnape.palatable.lambda.semigroup.builtin; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.specialized.SemigroupFactory; +import com.jnape.palatable.lambda.semigroup.Semigroup; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTWith.ltWith; + +/** + * Given a comparator for some type A, produce a {@link Semigroup} over A that chooses + * between two values x and y via the following rules: + *

+ * + * @param the value type + * @see Max + * @see MaxBy + * @see MinWith + */ +public final class MaxWith implements SemigroupFactory, A> { + + private static final MaxWith INSTANCE = new MaxWith<>(); + + private MaxWith() { + } + + @SuppressWarnings("unchecked") + public static MaxWith maxWith() { + return (MaxWith) INSTANCE; + } + + public static Semigroup maxWith(Comparator compareFn) { + return MaxWith.maxWith().apply(compareFn); + } + + public static Fn1 maxWith(Comparator compareFn, A x) { + return MaxWith.maxWith(compareFn).apply(x); + } + + public static A maxWith(Comparator compareFn, A x, A y) { + return maxWith(compareFn, x).apply(y); + } + + @Override + public Semigroup checkedApply(Comparator comparator) { + return (x, y) -> ltWith(comparator, y, x) ? y : x; + } +} \ No newline at end of file diff --git a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MinWith.java b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MinWith.java new file mode 100644 index 000000000..05eec2528 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/MinWith.java @@ -0,0 +1,52 @@ +package com.jnape.palatable.lambda.semigroup.builtin; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.specialized.SemigroupFactory; +import com.jnape.palatable.lambda.semigroup.Semigroup; + +import java.util.Comparator; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTWith.gtWith; + +/** + * Given a comparator for some type A, produce a {@link Semigroup} over A that chooses + * between two values x and y via the following rules: + *
    + *
  • If x is strictly greater than y in terms of B, return y
  • + *
  • Otherwise, return x
  • + *
+ * + * @param
the value type + * @see Min + * @see MinBy + * @see MaxBy + */ +public final class MinWith implements SemigroupFactory, A> { + + private static final MinWith INSTANCE = new MinWith<>(); + + private MinWith() { + } + + @SuppressWarnings("unchecked") + public static MinWith minWith() { + return (MinWith) INSTANCE; + } + + public static Semigroup minWith(Comparator compareFn) { + return MinWith.minWith().apply(compareFn); + } + + public static Fn1 minWith(Comparator compareFn, A x) { + return MinWith.minWith(compareFn).apply(x); + } + + public static A minWith(Comparator compareFn, A x, A y) { + return minWith(compareFn, x).apply(y); + } + + @Override + public Semigroup checkedApply(Comparator comparator) { + return (x, y) -> gtWith(comparator, y, x) ? y : x; + } +} diff --git a/src/test/java/com/jnape/palatable/lambda/adt/TryTest.java b/src/test/java/com/jnape/palatable/lambda/adt/TryTest.java index 1d4ed3b0f..dd4ff5dfb 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/TryTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/TryTest.java @@ -41,7 +41,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static testsupport.assertion.MonadErrorAssert.assertLaws; -import static testsupport.matchers.LeftMatcher.isLeftThat; +import static testsupport.matchers.EitherMatcher.isLeftThat; @RunWith(Traits.class) public class TryTest { diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/HListTest.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/HListTest.java index 6be65b6dc..1f75ce450 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/HListTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/HListTest.java @@ -2,15 +2,9 @@ import org.junit.Test; -import static com.jnape.palatable.lambda.adt.hlist.HList.cons; -import static com.jnape.palatable.lambda.adt.hlist.HList.nil; -import static com.jnape.palatable.lambda.adt.hlist.HList.singletonHList; -import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; +import static com.jnape.palatable.lambda.adt.hlist.HList.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.*; public class HListTest { @@ -36,6 +30,18 @@ public void convenienceStaticFactoryMethods() { assertEquals(nil().cons(false).cons(4.0).cons("3").cons('2').cons(1), tuple(1, '2', "3", 4.0, false)); } + @Test + public void autoPromotion() { + assertThat(cons(1, nil()), instanceOf(SingletonHList.class)); + assertThat(cons(1, singletonHList(1)), instanceOf(Tuple2.class)); + assertThat(cons(1, tuple(1, 1)), instanceOf(Tuple3.class)); + assertThat(cons(1, tuple(1, 1, 1)), instanceOf(Tuple4.class)); + assertThat(cons(1, tuple(1, 1, 1, 1)), instanceOf(Tuple5.class)); + assertThat(cons(1, tuple(1, 1, 1, 1, 1)), instanceOf(Tuple6.class)); + assertThat(cons(1, tuple(1, 1, 1, 1, 1, 1)), instanceOf(Tuple7.class)); + assertThat(cons(1, tuple(1, 1, 1, 1, 1, 1, 1)), instanceOf(Tuple8.class)); + } + @Test public void nilReusesInstance() { assertSame(nil(), nil()); diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn1/CoalesceTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn1/CoalesceTest.java index 68db636c1..6502e6104 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn1/CoalesceTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn1/CoalesceTest.java @@ -8,10 +8,10 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.junit.Assert.assertThat; +import static testsupport.matchers.EitherMatcher.isLeftThat; +import static testsupport.matchers.EitherMatcher.isRightThat; import static testsupport.matchers.IterableMatcher.isEmpty; import static testsupport.matchers.IterableMatcher.iterates; -import static testsupport.matchers.LeftMatcher.isLeftThat; -import static testsupport.matchers.RightMatcher.isRightThat; public class CoalesceTest { diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWithTest.java new file mode 100644 index 000000000..8d062f8fd --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CmpEqWithTest.java @@ -0,0 +1,19 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static com.jnape.palatable.lambda.functions.builtin.fn3.CmpEqBy.cmpEqBy; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CmpEqWithTest { + @Test + public void comparisons() { + assertTrue(cmpEqBy(id(), 1, 1)); + assertFalse(cmpEqBy(id(), 1, 2)); + assertFalse(cmpEqBy(id(), 2, 1)); + + assertTrue(cmpEqBy(String::length, "b", "a")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CompareTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CompareTest.java new file mode 100644 index 000000000..8817f94fc --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/CompareTest.java @@ -0,0 +1,19 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.Compare.compare; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.equal; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.greaterThan; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.lessThan; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertEquals; + +public class CompareTest { + @Test + public void comparisons() { + assertEquals(equal(), compare(naturalOrder(), 1, 1)); + assertEquals(lessThan(), compare(naturalOrder(), 2, 1)); + assertEquals(greaterThan(), compare(naturalOrder(), 1, 2)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWithTest.java new file mode 100644 index 000000000..4ccaac4ec --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTEWithTest.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTEWith.gteWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class GTEWithTest { + @Test + public void comparisons() { + assertTrue(gteWith(naturalOrder(), 1, 2)); + assertTrue(gteWith(naturalOrder(), 1, 1)); + assertFalse(gteWith(naturalOrder(), 2, 1)); + + assertTrue(gteWith(comparing(String::length), "b", "ab")); + assertTrue(gteWith(comparing(String::length), "bc", "ab")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWithTest.java new file mode 100644 index 000000000..4520ee272 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/GTWithTest.java @@ -0,0 +1,20 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.GTWith.gtWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; + +public class GTWithTest { + @Test + public void comparisons() { + assertTrue(gtWith(naturalOrder(), 1, 2)); + assertFalse(gtWith(naturalOrder(), 1, 1)); + assertFalse(gtWith(naturalOrder(), 2, 1)); + + assertTrue(gtWith(comparing(String::length), "bb", "aaa")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWithTest.java new file mode 100644 index 000000000..18f749c16 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTEWithTest.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTEWith.lteWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class LTEWithTest { + @Test + public void comparisons() { + assertTrue(lteWith(naturalOrder(), 2, 1)); + assertTrue(lteWith(naturalOrder(), 1, 1)); + assertFalse(lteWith(naturalOrder(), 1, 2)); + + assertTrue(lteWith(comparing(String::length), "ab", "b")); + assertTrue(lteWith(comparing(String::length), "ab", "bc")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWithTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWithTest.java new file mode 100644 index 000000000..dacdfd2ff --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/LTWithTest.java @@ -0,0 +1,20 @@ +package com.jnape.palatable.lambda.functions.builtin.fn3; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.builtin.fn3.LTWith.ltWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class LTWithTest { + @Test + public void comparisons() { + assertTrue(ltWith(naturalOrder(), 2, 1)); + assertFalse(ltWith(naturalOrder(), 1, 1)); + assertFalse(ltWith(naturalOrder(), 1, 2)); + + assertTrue(ltWith(comparing(String::length), "ab", "b")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelationTest.java b/src/test/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelationTest.java new file mode 100644 index 000000000..8b0083661 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/ordering/ComparisonRelationTest.java @@ -0,0 +1,23 @@ +package com.jnape.palatable.lambda.functions.ordering; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.equal; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.greaterThan; +import static com.jnape.palatable.lambda.functions.ordering.ComparisonRelation.lessThan; +import static java.lang.Integer.MAX_VALUE; +import static java.lang.Integer.MIN_VALUE; +import static org.junit.Assert.assertEquals; + +public class ComparisonRelationTest { + @Test + public void fromInt() { + assertEquals(greaterThan(), ComparisonRelation.fromInt(1)); + assertEquals(greaterThan(), ComparisonRelation.fromInt(MAX_VALUE)); + + assertEquals(equal(), ComparisonRelation.fromInt(0)); + + assertEquals(lessThan(), ComparisonRelation.fromInt(-1)); + assertEquals(lessThan(), ComparisonRelation.fromInt(MIN_VALUE)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functor/builtin/StateTest.java b/src/test/java/com/jnape/palatable/lambda/functor/builtin/StateTest.java index db591bcfa..7e86042d2 100644 --- a/src/test/java/com/jnape/palatable/lambda/functor/builtin/StateTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functor/builtin/StateTest.java @@ -1,8 +1,5 @@ package com.jnape.palatable.lambda.functor.builtin; -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.traitor.annotations.TestTraits; import com.jnape.palatable.traitor.runners.Traits; import org.junit.Test; @@ -17,8 +14,12 @@ 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.Id.id; import static com.jnape.palatable.lambda.functions.builtin.fn2.Into.into; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static testsupport.matchers.StateMatcher.whenEvaluated; +import static testsupport.matchers.StateMatcher.whenExecuted; +import static testsupport.matchers.StateMatcher.whenRun; import static testsupport.traits.Equivalence.equivalence; @RunWith(Traits.class) @@ -36,71 +37,65 @@ public Equivalence> testSubject() { @Test public void eval() { - State state = State.put(0); - assertEquals(state.run(1)._1(), state.eval(1)); + assertThat(State.gets(id()), whenEvaluated(1, 1)); } @Test public void exec() { - State state = State.put(0); - assertEquals(state.run(1)._2(), state.exec(1)); + assertThat(State.modify(x -> x + 1), whenExecuted(1, 2)); } @Test public void get() { - assertEquals(tuple(1, 1), State.get().run(1)); + assertThat(State.get(), whenRun(1, 1, 1)); } @Test public void put() { - assertEquals(tuple(UNIT, 1), State.put(1).run(1)); + assertThat(State.put(1), whenRun(1, UNIT, 1)); } @Test public void gets() { - assertEquals(tuple(0, "0"), State.gets(Integer::parseInt).run("0")); + assertThat(State.gets(Integer::parseInt), whenRun("0", 0, "0")); } @Test public void modify() { - assertEquals(tuple(UNIT, 1), State.modify(x -> x + 1).run(0)); + assertThat(State.modify(x -> x + 1), whenRun(0, UNIT, 1)); } @Test public void state() { - assertEquals(tuple(1, UNIT), State.state(1).run(UNIT)); - assertEquals(tuple(1, -1), State.state(x -> tuple(x + 1, x - 1)).run(0)); + assertThat(State.state(1), whenRun(UNIT, 1, UNIT)); + assertThat(State.state(x -> tuple(x + 1, x - 1)), whenRun(0, 1, -1)); } @Test public void stateAccumulation() { - State counter = State.get().flatMap(i -> State.put(i + 1).discardL(State.state(i))); - assertEquals(tuple(0, 1), counter.run(0)); + assertThat(State.get().flatMap(i -> State.put(i + 1).discardL(State.state(i))), + whenRun(0, 0, 1)); } @Test public void zipOrdering() { - Tuple2 result = State.state(s -> tuple(0, s + "1")) - .zip(State.>state(s -> tuple(x -> x + 1, s + "2"))) - .run("_"); - assertEquals(tuple(1, "_12"), result); + assertThat(State.state(s -> tuple(0, s + "1")) + .zip(State.state(s -> tuple(x -> x + 1, s + "2"))), + whenRun("_", 1, "_12")); } @Test public void withState() { - State modified = State.get().withState(x -> x + 1); - assertEquals(tuple(1, 1), modified.run(0)); + assertThat(State.get().withState(x -> x + 1), whenRun(0, 1, 1)); } @Test public void mapState() { - State modified = State.get().mapState(into((a, s) -> tuple(a + 1, s + 2))); - assertEquals(tuple(1, 2), modified.run(0)); + assertThat(State.get().mapState(into((a, s) -> tuple(a + 1, s + 2))), whenRun(0, 1, 2)); } @Test public void staticPure() { - State state = State.pureState().apply(1); - assertEquals(tuple(1, "foo"), state.run("foo")); + assertThat(State.pureState().apply(1), whenRun("foo", 1, "foo")); } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/matchers/StateMatcherTest.java b/src/test/java/com/jnape/palatable/lambda/matchers/StateMatcherTest.java new file mode 100644 index 000000000..42e1eddd3 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/matchers/StateMatcherTest.java @@ -0,0 +1,35 @@ +package com.jnape.palatable.lambda.matchers; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.adt.Either.left; +import static com.jnape.palatable.lambda.adt.Either.right; +import static com.jnape.palatable.lambda.functor.builtin.State.state; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static testsupport.matchers.EitherMatcher.isLeftThat; +import static testsupport.matchers.EitherMatcher.isRightThat; +import static testsupport.matchers.StateMatcher.whenEvaluatedWith; +import static testsupport.matchers.StateMatcher.whenExecutedWith; +import static testsupport.matchers.StateMatcher.whenRunWith; + +public class StateMatcherTest { + + @Test + public void whenEvalWithMatcher() { + assertThat(state(right(1)), + whenEvaluatedWith("0", isRightThat(equalTo(1)))); + } + + @Test + public void whenExecWithMatcher() { + assertThat(state(right(1)), + whenExecutedWith(left("0"), isLeftThat(equalTo("0")))); + } + + @Test + public void whenRunWithMatcher() { + assertThat(state(right(1)), + whenRunWith(left("0"), isRightThat(equalTo(1)), isLeftThat(equalTo("0")))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/matchers/StateTMatcherTest.java b/src/test/java/com/jnape/palatable/lambda/matchers/StateTMatcherTest.java new file mode 100644 index 000000000..562bb8bb4 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/matchers/StateTMatcherTest.java @@ -0,0 +1,81 @@ +package com.jnape.palatable.lambda.matchers; + +import org.hamcrest.core.IsEqual; +import org.junit.Test; + +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.hlist.HList.tuple; +import static com.jnape.palatable.lambda.io.IO.io; +import static com.jnape.palatable.lambda.monad.transformer.builtin.StateT.gets; +import static com.jnape.palatable.lambda.monad.transformer.builtin.StateT.stateT; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static testsupport.matchers.EitherMatcher.isLeftThat; +import static testsupport.matchers.EitherMatcher.isRightThat; +import static testsupport.matchers.IOMatcher.yieldsValue; +import static testsupport.matchers.StateTMatcher.whenEvaluatedWith; +import static testsupport.matchers.StateTMatcher.whenExecutedWith; +import static testsupport.matchers.StateTMatcher.whenRunWith; +import static testsupport.matchers.StateTMatcher.whenRunWithBoth; + +public class StateTMatcherTest { + + @Test + public void whenEvaluatedWithMatcher() { + assertThat(stateT(right(1)), + whenEvaluatedWith("0", isRightThat(equalTo(1)))); + } + + @Test + public void whenEvaluatedWithMatcherOnObject() { + assertThat(stateT(right(1)), + whenEvaluatedWith("0", not(equalTo(new Object())))); + } + + @Test + public void whenExecutedWithMatcher() { + assertThat(stateT(right(1)), + whenExecutedWith(left("0"), isRightThat(isLeftThat(equalTo("0"))))); + } + + @Test + public void whenExecutedWithMatcherOnObject() { + assertThat(stateT(right(1)), + whenExecutedWith(left("0"), not(equalTo(new Object())))); + } + + @Test + @SuppressWarnings("RedundantTypeArguments") + public void whenRunWithUsingTwoMatchers() { + assertThat(stateT(right(1)), + whenRunWithBoth(left("0"), + isRightThat(IsEqual.equalTo(1)), + isRightThat(isLeftThat(equalTo("0"))))); + } + + @Test + public void whenRunWithUsingOneTupleMatcher() { + assertThat(stateT(right(1)), + whenRunWith(left("0"), + isRightThat(equalTo(tuple(1, left("0")))))); + } + + @Test + public void whenRunWithUsingOneTupleMatcherOnObject() { + assertThat(stateT(right(1)), + whenRunWith(left("0"), not(equalTo(new Object())))); + } + + @Test + public void onlyRunsStateOnceWithTupleMatcher() { + AtomicInteger count = new AtomicInteger(0); + + assertThat(gets(s -> io(count::incrementAndGet)), whenRunWith(0, yieldsValue(equalTo(tuple(1, 0))))); + assertEquals(1, count.get()); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherTTest.java index cefc21eda..e495fb2fd 100644 --- a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherTTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/EitherTTest.java @@ -29,6 +29,7 @@ import static com.jnape.palatable.lambda.monad.transformer.builtin.EitherT.liftEitherT; import static com.jnape.palatable.traitor.framework.Subjects.subjects; import static org.junit.Assert.assertEquals; +import static testsupport.assertion.MonadErrorAssert.assertLaws; @RunWith(Traits.class) public class EitherTTest { @@ -73,4 +74,12 @@ public void staticPure() { .apply(1); assertEquals(eitherT(new Identity<>(right(1))), eitherT); } + + @Test + public void monadError() { + assertLaws(subjects(eitherT(new Identity<>(right(1))), + eitherT(new Identity<>(left("")))), + "bar", + str -> eitherT(new Identity<>(right(str.length())))); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateTTest.java new file mode 100644 index 000000000..00768b0c3 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateTTest.java @@ -0,0 +1,245 @@ +package com.jnape.palatable.lambda.monad.transformer.builtin; + +import com.jnape.palatable.lambda.adt.Maybe; +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.functor.builtin.Identity; +import com.jnape.palatable.lambda.functor.builtin.Lazy; +import com.jnape.palatable.lambda.functor.builtin.Writer; +import com.jnape.palatable.lambda.io.IO; +import com.jnape.palatable.traitor.annotations.TestTraits; +import com.jnape.palatable.traitor.framework.Subjects; +import com.jnape.palatable.traitor.runners.Traits; +import org.junit.Test; +import org.junit.runner.RunWith; +import testsupport.traits.ApplicativeLaws; +import testsupport.traits.Equivalence; +import testsupport.traits.FunctorLaws; +import testsupport.traits.MonadLaws; +import testsupport.traits.MonadRecLaws; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +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.Unit.UNIT; +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn2.LTE.lte; +import static com.jnape.palatable.lambda.functions.builtin.fn3.Times.times; +import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.recurse; +import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.terminate; +import static com.jnape.palatable.lambda.functor.builtin.Identity.pureIdentity; +import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy; +import static com.jnape.palatable.lambda.functor.builtin.Writer.listen; +import static com.jnape.palatable.lambda.functor.builtin.Writer.pureWriter; +import static com.jnape.palatable.lambda.functor.builtin.Writer.tell; +import static com.jnape.palatable.lambda.functor.builtin.Writer.writer; +import static com.jnape.palatable.lambda.io.IO.io; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.empty; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.singleton; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.unfold; +import static com.jnape.palatable.lambda.monoid.builtin.AddAll.addAll; +import static com.jnape.palatable.lambda.monoid.builtin.Join.join; +import static com.jnape.palatable.traitor.framework.Subjects.subjects; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static testsupport.Constants.STACK_EXPLODING_NUMBER; +import static testsupport.matchers.IOMatcher.yieldsValue; +import static testsupport.matchers.IterateTMatcher.isEmpty; +import static testsupport.matchers.IterateTMatcher.iterates; +import static testsupport.matchers.IterateTMatcher.iteratesAll; +import static testsupport.traits.Equivalence.equivalence; + +@RunWith(Traits.class) +public class IterateTTest { + + @TestTraits({FunctorLaws.class, ApplicativeLaws.class, MonadLaws.class, MonadRecLaws.class}) + public Subjects, Integer>>> testSubjects() { + Fn1, ?>, Object> toCollection = iterateT -> iterateT + ., Identity>>fold( + (as, a) -> { + as.add(a); + return new Identity<>(as); + }, + new Identity<>(new ArrayList<>())) + .runIdentity(); + return subjects(equivalence(empty(pureIdentity()), toCollection), + equivalence(singleton(new Identity<>(0)), toCollection), + equivalence(IterateT., Integer>empty(pureIdentity()).cons(new Identity<>(1)), + toCollection), + equivalence(IterateT., Integer>empty(pureIdentity()).snoc(new Identity<>(1)), + toCollection), + equivalence(singleton(new Identity<>(0)).concat(singleton(new Identity<>(1))), + toCollection), + equivalence(unfold(x -> new Identity<>(x <= 100 ? just(tuple(x, x + 1)) : nothing()), + new Identity<>(0)), + toCollection) + ); + } + + @Test + public void emptyHasNoElements() { + assertEquals(new Identity<>(nothing()), + IterateT., Integer>empty(pureIdentity()) + ., Integer>>>>>runIterateT()); + } + + @Test + public void singletonHasOneElement() { + assertThat(singleton(new Identity<>(1)), iterates(1)); + } + + @Test + public void unfolding() { + assertThat(unfold(x -> new Identity<>(just(x).filter(lte(3)).fmap(y -> tuple(y, y + 1))), new Identity<>(1)), + iteratesAll(asList(1, 2, 3))); + } + + @Test + public void consAddsElementToFront() { + assertThat(singleton(new Identity<>(1)).cons(new Identity<>(0)), iterates(0, 1)); + } + + @Test + public void snocAddsElementToBack() { + assertThat(singleton(new Identity<>(1)).snoc(new Identity<>(2)), iterates(1, 2)); + } + + @Test + public void concatsTwoIterateTs() { + IterateT, Integer> front = singleton(new Identity<>(0)).snoc(new Identity<>(1)); + IterateT, Integer> back = singleton(new Identity<>(2)).snoc(new Identity<>(3)); + + assertThat(front.concat(back), iterates(0, 1, 2, 3)); + assertThat(IterateT., Integer>empty(pureIdentity()).concat(back), iterates(2, 3)); + assertThat(front.concat(empty(pureIdentity())), iterates(0, 1)); + assertThat(IterateT., Integer>empty(pureIdentity()).concat(empty(pureIdentity())), isEmpty()); + assertThat(singleton(new Identity<>(1)) + .concat(unfold(x -> new Identity<>(nothing()), new Identity<>(0))) + .concat(singleton(new Identity<>(2))), + iterates(1, 2)); + } + + @Test + public void ofIteratesElements() { + assertEquals(tuple(6, asList(1, 2, 3)), + IterateT., ?>, Integer>of(listen(1), listen(2), listen(3)) + ., Integer>>fold( + (x, y) -> writer(tuple(x + y, singletonList(y))), listen(0)) + .runWriter(addAll(ArrayList::new))); + } + + @Test + public void fromIterator() { + IterateT, Integer> it = IterateT.fromIterator(asList(1, 2, 3).iterator()); + assertThat(it., IO>>toCollection(ArrayList::new), + yieldsValue(equalTo(asList(1, 2, 3)))); + assertThat(it., IO>>toCollection(ArrayList::new), + yieldsValue(equalTo(emptyList()))); + } + + @Test + public void fold() { + assertEquals(tuple(6, asList(1, 2, 3)), + IterateT., ?>, Integer>of(listen(1), listen(2), listen(3)) + ., Integer>>fold( + (x, y) -> writer(tuple(x + y, singletonList(y))), listen(0)) + .runWriter(addAll(ArrayList::new))); + } + + @Test + public void foldCut() { + assertEquals(tuple(3, "012"), + IterateT.of(writer(tuple(1, "1")), + writer(tuple(2, "2")), + writer(tuple(3, "3"))) + .>foldCut( + (x, y) -> listen(y == 2 ? terminate(x + y) : recurse(x + y)), + writer(tuple(0, "0"))) + .runWriter(join())); + } + + @Test + public void zipUsesCartesianProduct() { + assertThat(IterateT.of(new Identity<>(1), new Identity<>(2), new Identity<>(3)) + .zip(IterateT.of(new Identity<>(x -> x + 1), new Identity<>(x -> x - 1))), + iterates(2, 3, 4, 0, 1, 2)); + } + + @Test(timeout = 1000) + public void zipsInParallel() { + CountDownLatch latch = new CountDownLatch(2); + singleton(io(() -> { + latch.countDown(); + latch.await(); + return 0; + })).zip(singleton(io(() -> { + latch.countDown(); + latch.await(); + return x -> x + 1; + })))., Integer>>>>>runIterateT() + .unsafePerformAsyncIO() + .join(); + } + + @Test + public void toCollection() { + assertEquals(asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + unfold(x -> new Identity<>(x <= 10 ? just(tuple(x, x + 1)) : nothing()), new Identity<>(1)) + ., Identity>>toCollection(ArrayList::new) + .runIdentity()); + } + + @Test + public void forEach() { + assertEquals(tuple(UNIT, asList(1, 2, 3)), + IterateT., ?>, Integer>empty(pureWriter()) + .cons(listen(3)) + .cons(listen(2)) + .cons(listen(1)) + ., Unit>>forEach(x -> tell(singletonList(x))) + .runWriter(addAll(ArrayList::new))); + } + + @Test + public void foldLargeNumberOfElements() { + IterateT, Integer> largeIterateT = times(STACK_EXPLODING_NUMBER, + it -> it.cons(new Identity<>(1)), + empty(pureIdentity())); + assertEquals(new Identity<>(STACK_EXPLODING_NUMBER), + largeIterateT.fold((x, y) -> new Identity<>(x + y), new Identity<>(0))); + } + + @Test + public void stackSafetyForStrictMonads() { + IterateT, Integer> hugeStrictIterateT = + unfold(x -> new Identity<>(x <= STACK_EXPLODING_NUMBER ? just(tuple(x, x + 1)) : nothing()), + new Identity<>(1)); + Identity fold = hugeStrictIterateT.fold((x, y) -> new Identity<>(x + y), new Identity<>(0)); + assertEquals(new Identity<>(1250025000), fold); + } + + @Test + public void stackSafetyForNonStrictMonads() { + IterateT, Integer> hugeNonStrictIterateT = + unfold(x -> lazy(() -> x <= 50_000 ? just(tuple(x, x + 1)) : nothing()), lazy(0)); + Lazy fold = hugeNonStrictIterateT.fold((x, y) -> lazy(() -> x + y), lazy(0)); + assertEquals((Integer) 1250025000, fold.value()); + } + + @Test + public void concatIsStackSafe() { + IterateT, Integer> bigIterateT = times(10_000, xs -> xs.concat(singleton(new Identity<>(1))), + singleton(new Identity<>(0))); + assertEquals(new Identity<>(10_000), + bigIterateT.fold((x, y) -> new Identity<>(x + y), new Identity<>(0))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeTTest.java index 6af53f359..2c578fbc6 100644 --- a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeTTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/MaybeTTest.java @@ -22,12 +22,12 @@ 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.functions.builtin.fn2.GT.gt; +import static com.jnape.palatable.lambda.functions.builtin.fn2.LT.lt; import static com.jnape.palatable.lambda.functor.builtin.Identity.pureIdentity; import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy; import static com.jnape.palatable.lambda.io.IO.io; -import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.liftMaybeT; -import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.maybeT; -import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.pureMaybeT; +import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.*; import static com.jnape.palatable.traitor.framework.Subjects.subjects; import static org.junit.Assert.assertEquals; @@ -70,4 +70,23 @@ public void composedZip() { .unsafePerformAsyncIO(Executors.newFixedThreadPool(2)) .join(); } + + @Test + public void filter() { + MaybeT, Integer> maybeT = pureMaybeT(pureIdentity()).apply(1); + assertEquals(maybeT(new Identity<>(just(1))), maybeT.filter(gt(0))); + assertEquals(maybeT(new Identity<>(nothing())), maybeT.filter(lt(0))); + } + + @Test + public void orSelectsFirstPresentValueInsideEffect() { + assertEquals(maybeT(new Identity<>(just(1))), + maybeT(new Identity<>(just(1))).or(maybeT(new Identity<>(nothing())))); + + assertEquals(maybeT(new Identity<>(just(1))), + maybeT(new Identity<>(nothing())).or(maybeT(new Identity<>(just(1))))); + + assertEquals(maybeT(new Identity<>(just(1))), + maybeT(new Identity<>(just(1))).or(maybeT(new Identity<>(just(2))))); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderTTest.java index 3ea0710ff..6972c9fb6 100644 --- a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderTTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/ReaderTTest.java @@ -8,12 +8,7 @@ import com.jnape.palatable.traitor.runners.Traits; import org.junit.Test; import org.junit.runner.RunWith; -import testsupport.traits.ApplicativeLaws; -import testsupport.traits.Equivalence; -import testsupport.traits.FunctorLaws; -import testsupport.traits.MonadLaws; -import testsupport.traits.MonadReaderLaws; -import testsupport.traits.MonadRecLaws; +import testsupport.traits.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -58,6 +53,15 @@ public void mapReaderT() { .runReaderT("foo")); } + @Test + public void andComposesLeftToRight() { + ReaderT, Float> intToFloat = readerT(x -> new Identity<>(x.floatValue())); + ReaderT, Double> floatToDouble = readerT(f -> new Identity<>(f.doubleValue())); + + assertEquals(new Identity<>(1.), + intToFloat.and(floatToDouble).runReaderT(1)); + } + @Test public void staticPure() { ReaderT, Integer> readerT = diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateTTest.java index b30fd4596..b51888c9d 100644 --- a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateTTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateTTest.java @@ -1,7 +1,5 @@ package com.jnape.palatable.lambda.monad.transformer.builtin; -import com.jnape.palatable.lambda.adt.Maybe; -import com.jnape.palatable.lambda.adt.Unit; import com.jnape.palatable.lambda.adt.hlist.Tuple2; import com.jnape.palatable.lambda.functor.builtin.Identity; import com.jnape.palatable.traitor.annotations.TestTraits; @@ -26,7 +24,10 @@ import static com.jnape.palatable.lambda.optics.functions.Set.set; import static com.jnape.palatable.lambda.optics.lenses.ListLens.elementAt; import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static testsupport.matchers.StateTMatcher.whenEvaluated; +import static testsupport.matchers.StateTMatcher.whenExecuted; +import static testsupport.matchers.StateTMatcher.whenRun; import static testsupport.traits.Equivalence.equivalence; @RunWith(Traits.class) @@ -47,85 +48,75 @@ public void evalAndExec() { StateT, Integer> stateT = StateT.stateT(str -> new Identity<>(tuple(str.length(), str + "_"))); - assertEquals(new Identity<>("__"), stateT.execT("_")); - assertEquals(new Identity<>(1), stateT.evalT("_")); + assertThat(stateT, whenExecuted("_", new Identity<>("__"))); + assertThat(stateT, whenEvaluated("_", new Identity<>(1))); } @Test public void mapStateT() { - StateT, Integer> stateT = - StateT.stateT(str -> new Identity<>(tuple(str.length(), str + "_"))); - assertEquals(just(tuple(4, "ABC_")), - stateT.mapStateT(id -> id.>>coerce() - .runIdentity() - .into((x, str) -> just(tuple(x + 1, str.toUpperCase())))) - .>>runStateT("abc")); + assertThat(StateT., Integer>stateT(str -> new Identity<>(tuple(str.length(), str + "_"))) + .mapStateT(id -> id.>>coerce() + .runIdentity() + .into((x, str) -> just(tuple(x + 1, str.toUpperCase())))), + whenRun("abc", just(tuple(4, "ABC_")))); } @Test public void zipping() { - Tuple2> result = StateT., Identity>modify( - s -> new Identity<>(set(elementAt(s.size()), just("one"), s))) - .discardL(StateT.modify(s -> new Identity<>(set(elementAt(s.size()), just("two"), s)))) - .>>>runStateT(new ArrayList<>()) - .runIdentity(); - - assertEquals(tuple(UNIT, asList("one", "two")), - result); + assertThat( + StateT., Identity>modify(s -> new Identity<>(set(elementAt(s.size()), just("one"), s))) + .discardL(StateT.modify(s -> new Identity<>(set(elementAt(s.size()), just("two"), s)))), + whenRun(new ArrayList<>(), new Identity<>(tuple(UNIT, asList("one", "two"))))); } @Test public void withStateT() { - StateT, Integer> stateT = - StateT.stateT(str -> new Identity<>(tuple(str.length(), str + "_"))); - assertEquals(new Identity<>(tuple(3, "ABC_")), - stateT.withStateT(str -> new Identity<>(str.toUpperCase())).runStateT("abc")); + assertThat(StateT., Integer>stateT(str -> new Identity<>(tuple(str.length(), str + "_"))) + .withStateT(str -> new Identity<>(str.toUpperCase())), + whenRun("abc", new Identity<>(tuple(3, "ABC_")))); } @Test public void get() { - assertEquals(new Identity<>(tuple("state", "state")), - StateT.>get(pureIdentity()).runStateT("state")); + assertThat(StateT.get(pureIdentity()), + whenRun("state", new Identity<>(tuple("state", "state")))); } @Test public void gets() { - assertEquals(new Identity<>(tuple(5, "state")), - StateT., Integer>gets(s -> new Identity<>(s.length())).runStateT("state")); + assertThat(StateT.gets(s -> new Identity<>(s.length())), + whenRun("state", new Identity<>(tuple(5, "state")))); } @Test public void put() { - assertEquals(new Identity<>(tuple(UNIT, 1)), StateT.put(new Identity<>(1)).runStateT(0)); + assertThat(StateT.put(new Identity<>(1)), + whenRun(0, new Identity<>(tuple(UNIT, 1)))); } @Test public void modify() { - assertEquals(new Identity<>(tuple(UNIT, 1)), - StateT.>modify(x -> new Identity<>(x + 1)).runStateT(0)); + assertThat(StateT.modify(x -> new Identity<>(x + 1)), + whenRun(0, new Identity<>(tuple(UNIT, 1)))); } @Test public void stateT() { - assertEquals(new Identity<>(tuple(0, "_")), - StateT., Integer>stateT(new Identity<>(0)).runStateT("_")); - assertEquals(new Identity<>(tuple(1, "_1")), - StateT., Integer>stateT(s -> new Identity<>(tuple(s.length(), s + "1"))) - .runStateT("_")); + assertThat(StateT.stateT(new Identity<>(0)), + whenRun("_", new Identity<>(tuple(0, "_")))); + assertThat(StateT.stateT(s -> new Identity<>(tuple(s.length(), s + "1"))), + whenRun("_", new Identity<>(tuple(1, "_1")))); } @Test public void staticPure() { - assertEquals(new Identity<>(tuple(1, "foo")), - StateT.>pureStateT(pureIdentity()) - ., Integer>>apply(1) - .>>runStateT("foo")); + assertThat(StateT.>pureStateT(pureIdentity()).apply(1), + whenRun("foo", new Identity<>(tuple(1, "foo")))); } @Test public void staticLift() { - assertEquals(new Identity<>(tuple(1, "foo")), - StateT.liftStateT()., StateT, Integer>>apply(new Identity<>(1)) - .>>runStateT("foo")); + assertThat(StateT.liftStateT().apply(new Identity<>(1)), + whenRun("foo", new Identity<>(tuple(1, "foo")))); } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterTTest.java b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterTTest.java index 932fe7011..c1caa0527 100644 --- a/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterTTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/WriterTTest.java @@ -25,7 +25,11 @@ import static com.jnape.palatable.lambda.monad.transformer.builtin.WriterT.writerT; import static com.jnape.palatable.lambda.monoid.builtin.Join.join; import static com.jnape.palatable.lambda.monoid.builtin.Trivial.trivial; -import static org.junit.Assert.assertEquals; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static testsupport.matchers.WriterTMatcher.whenEvaluatedWith; +import static testsupport.matchers.WriterTMatcher.whenExecutedWith; +import static testsupport.matchers.WriterTMatcher.whenRunWith; import static testsupport.traits.Equivalence.equivalence; @RunWith(Traits.class) @@ -38,38 +42,46 @@ public Equivalence, Integer>> testSubject() { @Test public void accumulationUsesProvidedMonoid() { - Identity> result = writerT(new Identity<>(tuple(1, "foo"))) - .discardR(WriterT.tell(new Identity<>("bar"))) - .flatMap(x -> writerT(new Identity<>(tuple(x + 1, "baz")))) - .runWriterT(join()); + assertThat(writerT(new Identity<>(tuple(1, "foo"))) + .discardR(WriterT.tell(new Identity<>("bar"))) + .flatMap(x -> writerT(new Identity<>(tuple(x + 1, "baz")))), + whenRunWith(join(), equalTo(new Identity<>(tuple(2, "foobarbaz"))))); + } - assertEquals(new Identity<>(tuple(2, "foobarbaz")), result); + @Test + public void eval() { + assertThat(writerT(new Identity<>(tuple(1, "foo"))), + whenEvaluatedWith(join(), equalTo(new Identity<>(1)))); + } + + @Test + public void exec() { + assertThat(writerT(new Identity<>(tuple(1, "foo"))), + whenExecutedWith(join(), equalTo(new Identity<>("foo")))); } @Test public void tell() { - assertEquals(new Identity<>(tuple(UNIT, "")), - WriterT.tell(new Identity<>("")).runWriterT(join())); + assertThat(WriterT.tell(new Identity<>("")), + whenRunWith(join(), equalTo(new Identity<>(tuple(UNIT, ""))))); } @Test public void listen() { - assertEquals(new Identity<>(tuple(1, "")), - WriterT., Integer>listen(new Identity<>(1)).runWriterT(join())); + assertThat(WriterT.listen(new Identity<>(1)), + whenRunWith(join(), equalTo(new Identity<>(tuple(1, ""))))); } @Test public void staticPure() { - WriterT, Integer> apply = WriterT.>pureWriterT(pureIdentity()).apply(1); - assertEquals(new Identity<>(tuple(1, "")), - apply.runWriterT(join())); + assertThat(WriterT.>pureWriterT(pureIdentity()).apply(1), + whenRunWith(join(), equalTo(new Identity<>(tuple(1, ""))))); } @Test public void staticLift() { - WriterT, Integer> apply = WriterT.liftWriterT().apply(new Identity<>(1)); - assertEquals(new Identity<>(tuple(1, "")), - apply.runWriterT(join())); + assertThat(WriterT.liftWriterT().apply(new Identity<>(1)), + whenRunWith(join(), equalTo(new Identity<>(tuple(1, ""))))); } @Test(timeout = 500) diff --git a/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMapsTest.java b/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMapsTest.java new file mode 100644 index 000000000..0bf2d978b --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/monoid/builtin/MergeHMapsTest.java @@ -0,0 +1,62 @@ +package com.jnape.palatable.lambda.monoid.builtin; + +import com.jnape.palatable.lambda.adt.hmap.TypeSafeKey; +import org.junit.Test; + +import static com.jnape.palatable.lambda.adt.hmap.HMap.emptyHMap; +import static com.jnape.palatable.lambda.adt.hmap.HMap.hMap; +import static com.jnape.palatable.lambda.adt.hmap.HMap.singletonHMap; +import static com.jnape.palatable.lambda.adt.hmap.TypeSafeKey.typeSafeKey; +import static com.jnape.palatable.lambda.monoid.builtin.Join.join; +import static com.jnape.palatable.lambda.monoid.builtin.MergeHMaps.mergeHMaps; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class MergeHMapsTest { + + @Test + public void allKeysAccountedFor() { + TypeSafeKey.Simple stringKey = typeSafeKey(); + MergeHMaps mergeHMaps = mergeHMaps().key(stringKey, join()); + + assertEquals(emptyHMap(), mergeHMaps.apply(emptyHMap(), emptyHMap())); + assertEquals(singletonHMap(stringKey, "foo"), + mergeHMaps.apply(singletonHMap(stringKey, "foo"), emptyHMap())); + assertEquals(singletonHMap(stringKey, "foobar"), + mergeHMaps.apply(singletonHMap(stringKey, "foo"), + singletonHMap(stringKey, "bar"))); + } + + @Test + public void unaccountedForKeyUsesLastByDefault() { + TypeSafeKey.Simple stringKey = typeSafeKey(); + + assertEquals(singletonHMap(stringKey, "foo"), + mergeHMaps().apply(singletonHMap(stringKey, "foo"), emptyHMap())); + assertEquals(singletonHMap(stringKey, "bar"), + mergeHMaps().apply(emptyHMap(), singletonHMap(stringKey, "bar"))); + assertEquals(singletonHMap(stringKey, "bar"), + mergeHMaps().apply(singletonHMap(stringKey, "foo"), singletonHMap(stringKey, "bar"))); + } + + @Test + public void sparseKeysAcrossMaps() { + TypeSafeKey.Simple stringKey = typeSafeKey(); + TypeSafeKey.Simple intKey = typeSafeKey(); + TypeSafeKey.Simple boolKey = typeSafeKey(); + + MergeHMaps mergeHMaps = mergeHMaps() + .key(stringKey, join()) + .key(intKey, Integer::sum); + + assertEquals(hMap(stringKey, "foobar", + intKey, 3, + boolKey, false), + mergeHMaps.reduceLeft(asList(singletonHMap(stringKey, "foo"), + singletonHMap(intKey, 1), + singletonHMap(boolKey, true), + hMap(stringKey, "bar", + intKey, 2, + boolKey, false)))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWithTest.java b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWithTest.java new file mode 100644 index 000000000..49211aee0 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MaxWithTest.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.semigroup.builtin; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.semigroup.builtin.MaxWith.maxWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertEquals; + +public class MaxWithTest { + @Test + public void semigroup() { + assertEquals((Integer) 1, maxWith(naturalOrder(), 1, 0)); + assertEquals((Integer) 1, maxWith(naturalOrder(), 1, 1)); + assertEquals((Integer) 2, maxWith(naturalOrder(), 1, 2)); + + assertEquals("ab", maxWith(comparing(String::length), "ab", "a")); + assertEquals("ab", maxWith(comparing(String::length), "ab", "cd")); + assertEquals("bc", maxWith(comparing(String::length), "a", "bc")); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MinWithTest.java b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MinWithTest.java new file mode 100644 index 000000000..e6bfb82bb --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/MinWithTest.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.semigroup.builtin; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.semigroup.builtin.MinWith.minWith; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static org.junit.Assert.assertEquals; + +public class MinWithTest { + @Test + public void semigroup() { + assertEquals((Integer) 1, minWith(naturalOrder(), 1, 2)); + assertEquals((Integer) 1, minWith(naturalOrder(), 1, 1)); + assertEquals((Integer) 0, minWith(naturalOrder(), 1, 0)); + + assertEquals("a", minWith(comparing(String::length), "a", "ab")); + assertEquals("ab", minWith(comparing(String::length), "ab", "cd")); + assertEquals("c", minWith(comparing(String::length), "ab", "c")); + } +} \ No newline at end of file diff --git a/src/test/java/testsupport/matchers/EitherMatcher.java b/src/test/java/testsupport/matchers/EitherMatcher.java new file mode 100644 index 000000000..748d01be2 --- /dev/null +++ b/src/test/java/testsupport/matchers/EitherMatcher.java @@ -0,0 +1,54 @@ +package testsupport.matchers; + +import com.jnape.palatable.lambda.adt.Either; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import static com.jnape.palatable.lambda.adt.Either.left; +import static com.jnape.palatable.lambda.adt.Either.right; +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; +import static com.jnape.palatable.lambda.io.IO.io; + +public final class EitherMatcher extends TypeSafeMatcher> { + private final Either, Matcher> matcher; + + private EitherMatcher(Either, Matcher> matcher) { + this.matcher = matcher; + } + + @Override + protected void describeMismatchSafely(Either item, Description mismatchDescription) { + mismatchDescription.appendText("was "); + item.match(l -> matcher.match(lMatcher -> io(() -> lMatcher.describeMismatch(l, mismatchDescription)), + rMatcher -> io(() -> mismatchDescription.appendValue(item))), + r -> matcher.match(lMatcher -> io(() -> mismatchDescription.appendValue(item)), + lMatcher -> io(() -> lMatcher.describeMismatch(r, mismatchDescription)))) + .unsafePerformIO(); + } + + @Override + protected boolean matchesSafely(Either actual) { + return actual.match(l -> matcher.match(lMatcher -> lMatcher.matches(l), + constantly(false)), + r -> matcher.match(constantly(false), + rMatcher -> rMatcher.matches(r))); + } + + @Override + public void describeTo(Description description) { + matcher.match(l -> io(() -> description.appendText("Left value of ")) + .flatMap(constantly(io(() -> l.describeTo(description)))), + r -> io(() -> description.appendText("Right value of ")) + .flatMap(constantly(io(() -> r.describeTo(description))))) + .unsafePerformIO(); + } + + public static EitherMatcher isLeftThat(Matcher lMatcher) { + return new EitherMatcher<>(left(lMatcher)); + } + + public static EitherMatcher isRightThat(Matcher rMatcher) { + return new EitherMatcher<>(right(rMatcher)); + } +} diff --git a/src/test/java/testsupport/matchers/IterableMatcher.java b/src/test/java/testsupport/matchers/IterableMatcher.java index f5f196aba..ac1faee71 100644 --- a/src/test/java/testsupport/matchers/IterableMatcher.java +++ b/src/test/java/testsupport/matchers/IterableMatcher.java @@ -19,7 +19,7 @@ private IterableMatcher(Iterable expected) { @Override public boolean matches(Object actual) { - return actual instanceof Iterable && iterablesIterateSameElementsInOrder(expected, (Iterable) actual); + return actual instanceof Iterable && iterablesIterateSameElementsInOrder(expected, (Iterable) actual); } @Override @@ -32,18 +32,18 @@ public void describeMismatch(Object item, Description description) { if (item instanceof Iterable) { if (description.toString().endsWith("but: ")) description.appendText("was "); - description.appendText("<").appendText(stringify((Iterable) item)).appendText(">"); + description.appendText("<").appendText(stringify((Iterable) item)).appendText(">"); } else super.describeMismatch(item, description); } private boolean iterablesIterateSameElementsInOrder(Iterable expected, Iterable actual) { - Iterator actualIterator = actual.iterator(); + Iterator actualIterator = actual.iterator(); Iterator expectedIterator = expected.iterator(); while (expectedIterator.hasNext() && actualIterator.hasNext()) { Object nextExpected = expectedIterator.next(); - Object nextActual = actualIterator.next(); + Object nextActual = actualIterator.next(); if (nextExpected instanceof Iterable && nextActual instanceof Iterable) { if (!iterablesIterateSameElementsInOrder((Iterable) nextExpected, (Iterable) nextActual)) @@ -57,11 +57,11 @@ private boolean iterablesIterateSameElementsInOrder(Iterable expected, Iterab private String stringify(Iterable iterable) { StringBuilder stringBuilder = new StringBuilder().append("["); - Iterator iterator = iterable.iterator(); + Iterator iterator = iterable.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); if (next instanceof Iterable) - stringBuilder.append(stringify((Iterable) next)); + stringBuilder.append(stringify((Iterable) next)); else stringBuilder.append(next); if (iterator.hasNext()) @@ -76,6 +76,10 @@ public static IterableMatcher iterates(E... es) { return new IterableMatcher<>(asList(es)); } + public static IterableMatcher iteratesAll(Iterable es) { + return new IterableMatcher<>(es); + } + public static IterableMatcher isEmpty() { return new IterableMatcher<>(new ArrayList<>()); } diff --git a/src/test/java/testsupport/matchers/IterateTMatcher.java b/src/test/java/testsupport/matchers/IterateTMatcher.java new file mode 100644 index 000000000..894fa6edd --- /dev/null +++ b/src/test/java/testsupport/matchers/IterateTMatcher.java @@ -0,0 +1,48 @@ +package testsupport.matchers; + +import com.jnape.palatable.lambda.functor.builtin.Identity; +import com.jnape.palatable.lambda.monad.transformer.builtin.IterateT; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import java.util.LinkedList; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; + +public final class IterateTMatcher extends TypeSafeMatcher, A>> { + private final Iterable as; + + private IterateTMatcher(Iterable as) { + this.as = as; + } + + @Override + protected boolean matchesSafely(IterateT, A> iterateT) { + Identity> fold = iterateT.fold((as, a) -> { + as.add(a); + return new Identity<>(as); + }, new Identity<>(new LinkedList<>())); + LinkedList as = fold.runIdentity(); + return IterableMatcher.iteratesAll(this.as).matches(as); + } + + @Override + public void describeTo(Description description) { + description.appendText("an IterateT iterating " + as.toString() + " inside Identity"); + } + + public static IterateTMatcher iteratesAll(Iterable as) { + return new IterateTMatcher<>(as); + } + + public static IterateTMatcher isEmpty() { + return new IterateTMatcher<>(emptyList()); + } + + @SafeVarargs + @SuppressWarnings("varargs") + public static IterateTMatcher iterates(A... as) { + return iteratesAll(asList(as)); + } +} diff --git a/src/test/java/testsupport/matchers/LeftMatcher.java b/src/test/java/testsupport/matchers/LeftMatcher.java deleted file mode 100644 index 34ea33079..000000000 --- a/src/test/java/testsupport/matchers/LeftMatcher.java +++ /dev/null @@ -1,44 +0,0 @@ -package testsupport.matchers; - -import com.jnape.palatable.lambda.adt.Either; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; - -import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; -import static com.jnape.palatable.lambda.io.IO.io; - -public final class LeftMatcher extends TypeSafeMatcher> { - - private final Matcher lMatcher; - - private LeftMatcher(Matcher lMatcher) { - this.lMatcher = lMatcher; - } - - @Override - protected boolean matchesSafely(Either actual) { - return actual.match(lMatcher::matches, constantly(false)); - } - - @Override - public void describeTo(Description description) { - description.appendText("Left value of "); - lMatcher.describeTo(description); - } - - @Override - protected void describeMismatchSafely(Either item, Description mismatchDescription) { - mismatchDescription.appendText("was "); - item.match(l -> io(() -> { - mismatchDescription.appendText("Left value of "); - lMatcher.describeMismatch(l, mismatchDescription); - }), - r -> io(() -> mismatchDescription.appendValue(item))) - .unsafePerformIO(); - } - - public static LeftMatcher isLeftThat(Matcher lMatcher) { - return new LeftMatcher<>(lMatcher); - } -} diff --git a/src/test/java/testsupport/matchers/RightMatcher.java b/src/test/java/testsupport/matchers/RightMatcher.java deleted file mode 100644 index 1eafc0ab1..000000000 --- a/src/test/java/testsupport/matchers/RightMatcher.java +++ /dev/null @@ -1,44 +0,0 @@ -package testsupport.matchers; - -import com.jnape.palatable.lambda.adt.Either; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; - -import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; -import static com.jnape.palatable.lambda.io.IO.io; - -public final class RightMatcher extends TypeSafeMatcher> { - - private final Matcher rMatcher; - - private RightMatcher(Matcher rMatcher) { - this.rMatcher = rMatcher; - } - - @Override - protected boolean matchesSafely(Either actual) { - return actual.match(constantly(false), rMatcher::matches); - } - - @Override - public void describeTo(Description description) { - description.appendText("Right value of "); - rMatcher.describeTo(description); - } - - @Override - protected void describeMismatchSafely(Either item, Description mismatchDescription) { - mismatchDescription.appendText("was "); - item.match(l -> io(() -> mismatchDescription.appendValue(item)), - r -> io(() -> { - mismatchDescription.appendText("Right value of "); - rMatcher.describeMismatch(r, mismatchDescription); - })) - .unsafePerformIO(); - } - - public static RightMatcher isRightThat(Matcher rMatcher) { - return new RightMatcher<>(rMatcher); - } -} diff --git a/src/test/java/testsupport/matchers/StateMatcher.java b/src/test/java/testsupport/matchers/StateMatcher.java new file mode 100644 index 000000000..339deada8 --- /dev/null +++ b/src/test/java/testsupport/matchers/StateMatcher.java @@ -0,0 +1,90 @@ +package testsupport.matchers; + +import com.jnape.palatable.lambda.adt.These; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.functor.builtin.State; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import static com.jnape.palatable.lambda.adt.These.a; +import static com.jnape.palatable.lambda.adt.These.b; +import static com.jnape.palatable.lambda.adt.These.both; +import static com.jnape.palatable.lambda.io.IO.io; +import static org.hamcrest.Matchers.equalTo; + +public final class StateMatcher extends TypeSafeMatcher> { + private final S initialState; + private final These, Matcher> matchers; + + private StateMatcher(S initialState, These, Matcher> matchers) { + this.initialState = initialState; + this.matchers = matchers; + } + + @Override + protected boolean matchesSafely(State item) { + Tuple2 ran = item.run(initialState); + return matchers.match(a -> a.matches(ran._1()), + b -> b.matches(ran._2()), + ab -> ab._1().matches(ran._1()) && ab._2().matches(ran._2())); + } + + @Override + public void describeTo(Description description) { + matchers.match(a -> io(() -> a.describeTo(description.appendText("Value matching "))), + b -> io(() -> b.describeTo(description.appendText("State matching "))), + ab -> io(() -> { + description.appendText("Value matching: "); + ab._1().describeTo(description); + description.appendText(" and state matching: "); + ab._2().describeTo(description); + })) + .unsafePerformIO(); + } + + @Override + protected void describeMismatchSafely(State item, Description mismatchDescription) { + Tuple2 ran = item.run(initialState); + matchers.match(a -> io(() -> { + mismatchDescription.appendText("value matching "); + a.describeMismatch(ran._1(), mismatchDescription); + }), + b -> io(() -> { + mismatchDescription.appendText("state matching "); + b.describeMismatch(ran._2(), mismatchDescription); + }), + ab -> io(() -> { + mismatchDescription.appendText("value matching: "); + ab._1().describeMismatch(ran._1(), mismatchDescription); + mismatchDescription.appendText(" and state matching: "); + ab._2().describeMismatch(ran._2(), mismatchDescription); + })) + .unsafePerformIO(); + } + + public static StateMatcher whenRunWith(S initialState, Matcher valueMatcher, + Matcher stateMatcher) { + return new StateMatcher<>(initialState, both(valueMatcher, stateMatcher)); + } + + public static StateMatcher whenRun(S initialState, A value, S state) { + return whenRunWith(initialState, equalTo(value), equalTo(state)); + } + + public static StateMatcher whenExecutedWith(S initialState, Matcher stateMatcher) { + return new StateMatcher<>(initialState, b(stateMatcher)); + } + + public static StateMatcher whenExecuted(S initialState, S state) { + return whenExecutedWith(initialState, equalTo(state)); + } + + public static StateMatcher whenEvaluatedWith(S initialState, Matcher valueMatcher) { + return new StateMatcher<>(initialState, a(valueMatcher)); + } + + public static StateMatcher whenEvaluated(S initialState, A value) { + return whenEvaluatedWith(initialState, equalTo(value)); + } +} diff --git a/src/test/java/testsupport/matchers/StateTMatcher.java b/src/test/java/testsupport/matchers/StateTMatcher.java new file mode 100644 index 000000000..8bfa97684 --- /dev/null +++ b/src/test/java/testsupport/matchers/StateTMatcher.java @@ -0,0 +1,144 @@ +package testsupport.matchers; + +import com.jnape.palatable.lambda.adt.Either; +import com.jnape.palatable.lambda.adt.These; +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.monad.MonadRec; +import com.jnape.palatable.lambda.monad.transformer.builtin.StateT; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +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.These.a; +import static com.jnape.palatable.lambda.adt.These.b; +import static com.jnape.palatable.lambda.adt.These.both; +import static com.jnape.palatable.lambda.io.IO.io; +import static org.hamcrest.Matchers.equalTo; + +public final class StateTMatcher, A> extends TypeSafeMatcher> { + private final S initialState; + + private final Either< + Matcher, M>>, + These>, Matcher>>> matcher; + + private StateTMatcher(S initialState, + Either, M>>, + These>, Matcher>>> matcher) { + this.initialState = initialState; + this.matcher = matcher; + } + + @Override + protected boolean matchesSafely(StateT item) { + MonadRec, M> ran = item.runStateT(initialState); + return matcher.match(bothMatcher -> bothMatcher.matches(ran), + theseMatchers -> theseMatchers.match( + a -> a.matches(ran.fmap(Tuple2::_1)), + b -> b.matches(ran.fmap(Tuple2::_2)), + ab -> ab._1().matches(ran.fmap(Tuple2::_1)) + && ab._2().matches(ran.fmap(Tuple2::_2)))); + } + + @Override + public void describeTo(Description description) { + matcher.match(both -> io(() -> both.describeTo(description.appendText("Value and state matching "))), + these -> these.match( + a -> io(() -> a.describeTo(description.appendText("Value matching "))), + b -> io(() -> b.describeTo(description.appendText("State matching "))), + ab -> io(() -> { + description.appendText("Value run matching: "); + ab._1().describeTo(description); + description.appendText(", then state run matching: "); + ab._2().describeTo(description); + }))) + .unsafePerformIO(); + } + + @Override + protected void describeMismatchSafely(StateT item, Description mismatchDescription) { + MonadRec, M> ran = item.runStateT(initialState); + + matcher.match(bothMatcher -> io(() -> { + mismatchDescription.appendText("value and state matching "); + bothMatcher.describeMismatch(ran, mismatchDescription); + }), + theseMatchers -> theseMatchers.match( + a -> io(() -> { + mismatchDescription.appendText("value matching "); + a.describeMismatch(ran.fmap(Tuple2::_1), mismatchDescription); + }), + b -> io(() -> { + mismatchDescription.appendText("state matching "); + b.describeMismatch(ran.fmap(Tuple2::_2), mismatchDescription); + }), + ab -> io(() -> { + mismatchDescription.appendText("value run matching: "); + ab._1().describeMismatch(ran.fmap(Tuple2::_1), mismatchDescription); + mismatchDescription.appendText(", then state run matching: "); + ab._2().describeMismatch(ran.fmap(Tuple2::_2), mismatchDescription); + }))) + .unsafePerformIO(); + } + + public static , A, MAS extends MonadRec, M>> StateTMatcher + whenRunWith(S initialState, Matcher bothMatcher) { + return new StateTMatcher(initialState, left(extendMatcher(bothMatcher))); + } + + public static , A> StateTMatcher whenRun( + S initialState, MonadRec, M> both) { + return whenRunWith(initialState, equalTo(both)); + } + + public static , A, MA extends MonadRec, MS extends MonadRec> + StateTMatcher whenRunWithBoth(S initialState, + Matcher valueMatcher, + Matcher stateMatcher) { + return new StateTMatcher(initialState, right(both(extendMatcher(valueMatcher), + extendMatcher(stateMatcher)))); + } + + public static , A> StateTMatcher whenRunBoth(S initialState, + MonadRec value, + MonadRec state) { + return whenRunWithBoth(initialState, equalTo(value), equalTo(state)); + } + + public static , A, MS extends MonadRec> StateTMatcher whenExecutedWith( + S initialState, Matcher stateMatcher) { + return new StateTMatcher(initialState, right(b(extendMatcher(stateMatcher)))); + } + + public static , A> StateTMatcher whenExecuted(S initialState, + MonadRec state) { + return whenExecutedWith(initialState, equalTo(state)); + } + + public static , A, MA extends MonadRec> StateTMatcher whenEvaluatedWith( + S initialState, Matcher valueMatcher) { + return new StateTMatcher(initialState, right(a(extendMatcher(valueMatcher)))); + } + + public static , A> StateTMatcher whenEvaluated(S initialState, + MonadRec value) { + return whenEvaluatedWith(initialState, equalTo(value)); + } + + private static , MX extends MonadRec> Matcher> extendMatcher( + Matcher matcher) { + return new TypeSafeMatcher>() { + @Override + protected boolean matchesSafely(MonadRec item) { + return matcher.matches(item); + } + + @Override + public void describeTo(Description description) { + matcher.describeTo(description); + } + }; + } +} diff --git a/src/test/java/testsupport/matchers/WriterTMatcher.java b/src/test/java/testsupport/matchers/WriterTMatcher.java new file mode 100644 index 000000000..36e2786bb --- /dev/null +++ b/src/test/java/testsupport/matchers/WriterTMatcher.java @@ -0,0 +1,81 @@ +package testsupport.matchers; + +import com.jnape.palatable.lambda.adt.hlist.Tuple2; +import com.jnape.palatable.lambda.monad.MonadRec; +import com.jnape.palatable.lambda.monad.transformer.builtin.WriterT; +import com.jnape.palatable.lambda.monoid.Monoid; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +public final class WriterTMatcher, A> extends + TypeSafeMatcher>> { + + private final Matcher, M>> expected; + private final Monoid wMonoid; + + private WriterTMatcher(Matcher, M>> expected, Monoid wMonoid) { + this.wMonoid = wMonoid; + this.expected = expected; + } + + @Override + protected boolean matchesSafely(MonadRec> item) { + return expected.matches(item.>coerce().runWriterT(wMonoid)); + } + + @Override + public void describeTo(Description description) { + expected.describeTo(description); + } + + @Override + protected void describeMismatchSafely(MonadRec> item, Description mismatchDescription) { + expected.describeMismatch(item.>coerce().runWriterT(wMonoid), mismatchDescription); + } + + public static , A, MAW extends MonadRec, M>> + WriterTMatcher whenRunWith(Monoid wMonoid, Matcher matcher) { + return new WriterTMatcher<>(matcher, wMonoid); + } + + public static , A, MW extends MonadRec> + WriterTMatcher whenExecutedWith(Monoid wMonoid, Matcher matcher) { + return whenRunWith(wMonoid, new TypeSafeMatcher, M>>() { + @Override + protected boolean matchesSafely(MonadRec, M> item) { + return matcher.matches(item.fmap(Tuple2::_2)); + } + + @Override + public void describeTo(Description description) { + matcher.describeTo(description); + } + + @Override + protected void describeMismatchSafely(MonadRec, M> item, Description mismatchDescription) { + matcher.describeMismatch(item.fmap(Tuple2::_2), mismatchDescription); + } + }); + } + + public static , A, MA extends MonadRec> + WriterTMatcher whenEvaluatedWith(Monoid wMonoid, Matcher matcher) { + return whenRunWith(wMonoid, new TypeSafeMatcher, M>>() { + @Override + protected boolean matchesSafely(MonadRec, M> item) { + return matcher.matches(item.fmap(Tuple2::_1)); + } + + @Override + public void describeTo(Description description) { + matcher.describeTo(description); + } + + @Override + protected void describeMismatchSafely(MonadRec, M> item, Description mismatchDescription) { + matcher.describeMismatch(item.fmap(Tuple2::_1), mismatchDescription); + } + }); + } +} \ No newline at end of file