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/.github/workflows/maven.yml b/.github/workflows/maven.yml index 2f7615ccb..296a02977 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,6 +1,8 @@ name: Java CI -on: [push] +on: + push: + pull_request: jobs: build-java-1_8: @@ -28,3 +30,16 @@ jobs: java-version: 11 - name: Build with Maven run: mvn clean verify + + build-java-14: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 14 + uses: actions/setup-java@v1 + with: + java-version: 14 + - name: Build with Maven + run: mvn clean verify diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39074daea..000000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -dist: trusty -language: java -jdk: - - oraclejdk8 - - openjdk11 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6900a75..3aecf9b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,68 @@ # Change Log + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [Unreleased] -There are currently no unreleased changes +### Added +- `ReaderT#ask`, a static factory method for returning an identity `ReaderT` + +### Fixed +- nested `DropWhile`s no longer incorrectly deforest using disjunction + +## [5.4.0] - 2021-09-17 + +### Changed +- `Absent` folds short-circuit on the first `nothing()` +- `EitherMatcher#isLeftThat/isRightThat` support contravariant bounds on their delegates + +### Added +- `IterateT#runStep`, a method used to run a single step of an IterateT without the contractual guarantee of emitting a + value or reaching the end +- `These#fromMaybes :: Maybe a -> Maybe b -> Maybe (These a b)` +- `EitherMatcher#isLeftOf/isRightOf` for asserting equality + +### Fixed +- `WriterT` now keeps an immediate reference to the embedded monad's `pure` + +## [5.3.0] - 2020-12-07 + +### Changed +- `IterateT#unfold` now only computes a single `Pure` for the given input +- `ReaderT#fmap` and `StateT#fmap` avoid unnecessary calls to `pure` +- `MaybeT` implements `MonadError` + +### Added +- `$`, function application represented as a higher-order `Fn2` +- `Fn1#withSelf`, a static method for constructing a self-referencing `Fn1` +- `HNil/SingletonHList/TupleX#snoc`, a method to add a new last element (append to a tuple) +- `Tuple2-8#init`, for populating a `TupleN` with all but the last element + +### Fixed +- `IterateT#trampolineM` now yields and stages all recursive result values, rather + than prematurely terminating on the first termination result +- `IterateT#flatMap` is now stack-safe regardless of how many consecutive empty `IterateT`s + are returned and regardless of whether the monad is strict or lazy or internally trampolined + +## [5.2.0] - 2020-02-12 + +### 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 @@ -543,7 +600,10 @@ There are currently no unreleased changes - `Monadic/Dyadic/TriadicFunction`, `Predicate`, `Tuple2`, `Tuple3` - `Functor`, `BiFunctor`, `ProFunctor` -[Unreleased]: https://github.com/palatable/lambda/compare/lambda-5.1.0...HEAD +[Unreleased]: https://github.com/palatable/lambda/compare/lambda-5.4.0...HEAD +[5.4.0]: https://github.com/palatable/lambda/compare/lambda-5.3.0...lambda-5.4.0 +[5.3.0]: https://github.com/palatable/lambda/compare/lambda-5.2.0...lambda-5.3.0 +[5.2.0]: https://github.com/palatable/lambda/compare/lambda-5.1.0...lambda-5.2.0 [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 diff --git a/README.md b/README.md index edaf4dde9..7d84c6295 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ λ ====== -[![Build Status](https://travis-ci.com/palatable/lambda.svg?branch=master)](https://travis-ci.com/palatable/lambda) [![Actions Status](https://github.com/palatable/lambda/workflows/Java%20CI/badge.svg)](https://github.com/palatable/lambda/actions) [![Lambda](https://img.shields.io/maven-central/v/com.jnape.palatable/lambda.svg)](http://search.maven.org/#search%7Cga%7C1%7Ccom.jnape.palatable.lambda) -[![Join the chat at https://gitter.im/palatable/lambda](https://badges.gitter.im/palatable/lambda.svg)](https://gitter.im/palatable/lambda?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[Join the chat on Discord](https://discord.gg/wR7k8RAKM5) [![Floobits Status](https://floobits.com/jnape/lambda.svg)](https://floobits.com/jnape/lambda/redirect) Functional patterns for Java @@ -30,7 +29,7 @@ Functional patterns for Java - [Either](#either) - [Lenses](#lenses) - [Notes](#notes) - - [Community](#community) + - [Ecosystem](#ecosystem) - [License](#license) Background @@ -61,14 +60,14 @@ Add the following dependency to your: com.jnape.palatable lambda - 5.1.0 + 5.4.0 ``` `build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)): ```gradle -compile group: 'com.jnape.palatable', name: 'lambda', version: '5.1.0' +compile group: 'com.jnape.palatable', name: 'lambda', version: '5.4.0' ``` Examples @@ -740,12 +739,23 @@ 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 +Ecosystem ----- -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! + +### Official extension libraries: + +These are officially supported libraries that extend lambda's core functionality and are developed under the same governance and processes as lambda. + +- [Shōki](https://github.com/palatable/shoki) - Purely functional, persistent data structures for the JVM + +### Third-party community libraries: + +These are open-sourced community projects that rely on _lambda_ for significant functionality, but are not necessarily affiliated with lambda and have their own separate 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) +- [WuWei](https://github.com/nomicflux/WuWei) - Michael Anderson [@nomicflux](https://github.com/nomicflux) - `ST` monad for safe mutability +- [Kraftwerk](https://github.com/kschuetz/kraftwerk) - Kevin Schuetz [@kschuetz](https://github.com/kschuetz) - random data generators and combinators License ------- diff --git a/pom.xml b/pom.xml index c94448fa2..bae03b1e3 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ lambda - 5.1.1-SNAPSHOT + 5.5.1-SNAPSHOT jar Lambda @@ -74,6 +74,7 @@ org.mockito mockito-core 2.28.2 + test com.jnape.palatable @@ -102,6 +103,13 @@ org.apache.maven.plugins maven-jar-plugin ${maven-jar-plugin.version} + + + + com.jnape.palatable.lambda + + + diff --git a/src/main/java/com/jnape/palatable/lambda/adt/These.java b/src/main/java/com/jnape/palatable/lambda/adt/These.java index 6721d33e8..4fc4cc27b 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/These.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/These.java @@ -189,6 +189,25 @@ public static These both(A a, B b) { return new Both<>(tuple(a, b)); } + /** + * Convenience method for converting a pair of {@link Maybe}s into a {@link Maybe} of {@link These}. If both + * {@link Maybe}s are {@link Maybe#just} then the result is a {@link Maybe#just} {@link These#both}. If only one + * {@link Maybe} is {@link Maybe#just} then it will be {@link Maybe#just} {@link These#a} or + * {@link Maybe#just} {@link These#b}. If both {@link Maybe}s are {@link Maybe#nothing} then the result will be + * {@link Maybe#nothing}. + * + * @param maybeA the first optional value + * @param maybeB the second optional value + * @param the first possible type + * @param the second possible type + * @return the wrapped values as a {@link Maybe}<{@link These}<A,B>> + */ + public static Maybe> fromMaybes(Maybe maybeA, Maybe maybeB) { + return maybeA.fmap(a -> maybeB.fmap(b -> both(a, b)).orElse(a(a))) + .fmap(Maybe::just) + .orElse(maybeB.fmap(These::b)); + } + /** * The canonical {@link Pure} instance for {@link These}. * 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..705e43bad 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)); } /** @@ -87,7 +89,6 @@ public static SingletonHList singletonHList(Head head) { * @return the 2-element HList * @see Tuple2 */ - @SuppressWarnings("JavaDoc") public static <_1, _2> Tuple2<_1, _2> tuple(_1 _1, _2 _2) { return singletonHList(_2).cons(_1); } @@ -104,7 +105,6 @@ public static <_1, _2> Tuple2<_1, _2> tuple(_1 _1, _2 _2) { * @return the 3-element HList * @see Tuple3 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3> Tuple3<_1, _2, _3> tuple(_1 _1, _2 _2, _3 _3) { return tuple(_2, _3).cons(_1); } @@ -123,7 +123,6 @@ public static <_1, _2, _3> Tuple3<_1, _2, _3> tuple(_1 _1, _2 _2, _3 _3) { * @return the 4-element HList * @see Tuple4 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3, _4> Tuple4<_1, _2, _3, _4> tuple(_1 _1, _2 _2, _3 _3, _4 _4) { return tuple(_2, _3, _4).cons(_1); } @@ -144,7 +143,6 @@ public static <_1, _2, _3, _4> Tuple4<_1, _2, _3, _4> tuple(_1 _1, _2 _2, _3 _3, * @return the 5-element HList * @see Tuple5 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3, _4, _5> Tuple5<_1, _2, _3, _4, _5> tuple(_1 _1, _2 _2, _3 _3, _4 _4, _5 _5) { return tuple(_2, _3, _4, _5).cons(_1); } @@ -167,7 +165,6 @@ public static <_1, _2, _3, _4, _5> Tuple5<_1, _2, _3, _4, _5> tuple(_1 _1, _2 _2 * @return the 6-element HList * @see Tuple6 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3, _4, _5, _6> Tuple6<_1, _2, _3, _4, _5, _6> tuple(_1 _1, _2 _2, _3 _3, _4 _4, _5 _5, _6 _6) { return tuple(_2, _3, _4, _5, _6).cons(_1); @@ -193,7 +190,6 @@ public static <_1, _2, _3, _4, _5, _6> Tuple6<_1, _2, _3, _4, _5, _6> tuple(_1 _ * @return the 7-element HList * @see Tuple7 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3, _4, _5, _6, _7> Tuple7<_1, _2, _3, _4, _5, _6, _7> tuple(_1 _1, _2 _2, _3 _3, _4 _4, _5 _5, _6 _6, _7 _7) { return tuple(_2, _3, _4, _5, _6, _7).cons(_1); @@ -221,7 +217,6 @@ public static <_1, _2, _3, _4, _5, _6, _7> Tuple7<_1, _2, _3, _4, _5, _6, _7> tu * @return the 8-element HList * @see Tuple8 */ - @SuppressWarnings("JavaDoc") public static <_1, _2, _3, _4, _5, _6, _7, _8> Tuple8<_1, _2, _3, _4, _5, _6, _7, _8> tuple(_1 _1, _2 _2, _3 _3, _4 _4, _5 _5, _6 _6, _7 _7, _8 _8) { diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/SingletonHList.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/SingletonHList.java index 927f27ae3..8e8516a47 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/SingletonHList.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/SingletonHList.java @@ -39,6 +39,18 @@ public <_0> Tuple2<_0, _1> cons(_0 _0) { return new Tuple2<>(_0, this); } + + /** + * Snoc an element onto the back of this {@link SingletonHList}. + * + * @param _2 the new last element + * @param <_2> the new last element type + * @return the new {@link Tuple2} + */ + public <_2> Tuple2<_1, _2> snoc(_2 _2) { + return tuple(head(), _2); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple2.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple2.java index 726335f6d..aafa40b5e 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple2.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple2.java @@ -48,7 +48,7 @@ public class Tuple2<_1, _2> extends HCons<_1, SingletonHList<_2>> implements Tuple2(_1 _1, SingletonHList<_2> tail) { super(_1, tail); this._1 = _1; - _2 = tail.head(); + _2 = tail.head(); } /** @@ -75,6 +75,17 @@ public <_0> Tuple3<_0, _1, _2> cons(_0 _0) { return new Tuple3<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple2}. + * + * @param _3 the new last element + * @param <_3> the new last element type + * @return the new {@link Tuple3} + */ + public <_3> Tuple3<_1, _2, _3> snoc(_3 _3) { + return tuple(_1, _2, _3); + } + /** * {@inheritDoc} */ @@ -226,6 +237,15 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_2).fmap(_2Prime -> fmap(constantly(_2Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link SingletonHList}<_1> of the first element. + * + * @return The {@link SingletonHList}<_1> + */ + public SingletonHList<_1> init() { + return invert().tail(); + } + /** * Static factory method for creating Tuple2s from {@link java.util.Map.Entry}s. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple3.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple3.java index e32eac589..b392ffe38 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple3.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple3.java @@ -44,8 +44,8 @@ public class Tuple3<_1, _2, _3> extends HCons<_1, Tuple2<_2, _3>> implements Tuple3(_1 _1, Tuple2<_2, _3> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); + _2 = tail._1(); + _3 = tail._2(); } /** @@ -56,6 +56,17 @@ public <_0> Tuple4<_0, _1, _2, _3> cons(_0 _0) { return new Tuple4<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple3}. + * + * @param _4 the new last element + * @param <_4> the new last element type + * @return the new {@link Tuple4} + */ + public <_4> Tuple4<_1, _2, _3, _4> snoc(_4 _4) { + return tuple(_1, _2, _3, _4); + } + /** * {@inheritDoc} */ @@ -208,6 +219,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_3).fmap(_3Prime -> fmap(constantly(_3Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple2}<_1, _2> of all the elements of this + * {@link Tuple3}<_1, _2, _3> except the last. + * + * @return The {@link Tuple2}<_1, _2> representing all but the last element + */ + public Tuple2<_1, _2> init() { + return rotateR3().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple4.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple4.java index 3e74da859..d8dc42868 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple4.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple4.java @@ -46,9 +46,9 @@ public class Tuple4<_1, _2, _3, _4> extends HCons<_1, Tuple3<_2, _3, _4>> implem Tuple4(_1 _1, Tuple3<_2, _3, _4> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); - _4 = tail._3(); + _2 = tail._1(); + _3 = tail._2(); + _4 = tail._3(); } /** @@ -59,6 +59,17 @@ public <_0> Tuple5<_0, _1, _2, _3, _4> cons(_0 _0) { return new Tuple5<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple4}. + * + * @param _5 the new last element + * @param <_5> the new last element type + * @return the new {@link Tuple5} + */ + public <_5> Tuple5<_1, _2, _3, _4, _5> snoc(_5 _5) { + return tuple(_1, _2, _3, _4, _5); + } + /** * {@inheritDoc} */ @@ -235,6 +246,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_4).fmap(_4Prime -> fmap(constantly(_4Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple3}<_1, _2, _3> of all the elements of this + * {@link Tuple4}<_1, _2, _3, _4> except the last. + * + * @return The {@link Tuple3}<_1, _2, _3> representing all but the last element + */ + public Tuple3<_1, _2, _3> init() { + return rotateR4().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple5.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple5.java index c87ee564b..5a6f20cbb 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple5.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple5.java @@ -48,10 +48,10 @@ public class Tuple5<_1, _2, _3, _4, _5> extends HCons<_1, Tuple4<_2, _3, _4, _5> Tuple5(_1 _1, Tuple4<_2, _3, _4, _5> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); - _4 = tail._3(); - _5 = tail._4(); + _2 = tail._1(); + _3 = tail._2(); + _4 = tail._3(); + _5 = tail._4(); } /** @@ -62,6 +62,17 @@ public <_0> Tuple6<_0, _1, _2, _3, _4, _5> cons(_0 _0) { return new Tuple6<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple5}. + * + * @param _6 the new last element + * @param <_6> the new last element type + * @return the new {@link Tuple6} + */ + public <_6> Tuple6<_1, _2, _3, _4, _5, _6> snoc(_6 _6) { + return tuple(_1, _2, _3, _4, _5, _6); + } + /** * {@inheritDoc} */ @@ -262,6 +273,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_5).fmap(_3Prime -> fmap(constantly(_3Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple4}<_1, _2, _3, _4> of all the elements of this + * {@link Tuple5}<_1, _2, _3, _4, _5> except the last. + * + * @return The {@link Tuple4}<_1, _2, _3, _4> representing all but the last element + */ + public Tuple4<_1, _2, _3, _4> init() { + return rotateR5().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple6.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple6.java index 6d07b503b..1034e3b6b 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple6.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple6.java @@ -51,11 +51,11 @@ public class Tuple6<_1, _2, _3, _4, _5, _6> extends HCons<_1, Tuple5<_2, _3, _4, Tuple6(_1 _1, Tuple5<_2, _3, _4, _5, _6> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); - _4 = tail._3(); - _5 = tail._4(); - _6 = tail._5(); + _2 = tail._1(); + _3 = tail._2(); + _4 = tail._3(); + _5 = tail._4(); + _6 = tail._5(); } /** @@ -66,6 +66,17 @@ public <_0> Tuple7<_0, _1, _2, _3, _4, _5, _6> cons(_0 _0) { return new Tuple7<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple6}. + * + * @param _7 the new last element + * @param <_7> the new last element type + * @return the new {@link Tuple7} + */ + public <_7> Tuple7<_1, _2, _3, _4, _5, _6, _7> snoc(_7 _7) { + return tuple(_1, _2, _3, _4, _5, _6, _7); + } + /** * {@inheritDoc} */ @@ -293,6 +304,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_6).fmap(_6Prime -> fmap(constantly(_6Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple5}<_1, _2, _3, _4, _5> of all the elements of this + * {@link Tuple6}<_1, _2, _3, _4, _5, _6> except the last. + * + * @return The {@link Tuple5}<_1, _2, _3, _4, _5> representing all but the last element + */ + public Tuple5<_1, _2, _3, _4, _5> init() { + return rotateR6().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple7.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple7.java index a3e162f73..cdfe552d6 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple7.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple7.java @@ -54,12 +54,12 @@ public class Tuple7<_1, _2, _3, _4, _5, _6, _7> extends HCons<_1, Tuple6<_2, _3, Tuple7(_1 _1, Tuple6<_2, _3, _4, _5, _6, _7> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); - _4 = tail._3(); - _5 = tail._4(); - _6 = tail._5(); - _7 = tail._6(); + _2 = tail._1(); + _3 = tail._2(); + _4 = tail._3(); + _5 = tail._4(); + _6 = tail._5(); + _7 = tail._6(); } /** @@ -70,6 +70,17 @@ public <_0> Tuple8<_0, _1, _2, _3, _4, _5, _6, _7> cons(_0 _0) { return new Tuple8<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple7}. + * + * @param _8 the new last element + * @param <_8> the new last element type + * @return the new {@link Tuple8} + */ + public <_8> Tuple8<_1, _2, _3, _4, _5, _6, _7, _8> snoc(_8 _8) { + return tuple(_1, _2, _3, _4, _5, _6, _7, _8); + } + /** * {@inheritDoc} */ @@ -323,6 +334,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_7).fmap(_7Prime -> fmap(constantly(_7Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple6}<_1, _2, _3, _4, _5, _6> of all the elements of this + * {@link Tuple7}<_1, _2, _3, _4, _5, _6, _7> except the last. + * + * @return The {@link Tuple6}<_1, _2, _3, _4, _5, _6> representing all but the last element + */ + public Tuple6<_1, _2, _3, _4, _5, _6> init() { + return rotateR7().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple8.java b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple8.java index 8a8001e24..079a30611 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple8.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hlist/Tuple8.java @@ -57,13 +57,13 @@ public class Tuple8<_1, _2, _3, _4, _5, _6, _7, _8> extends HCons<_1, Tuple7<_2, Tuple8(_1 _1, Tuple7<_2, _3, _4, _5, _6, _7, _8> tail) { super(_1, tail); this._1 = _1; - _2 = tail._1(); - _3 = tail._2(); - _4 = tail._3(); - _5 = tail._4(); - _6 = tail._5(); - _7 = tail._6(); - _8 = tail._7(); + _2 = tail._1(); + _3 = tail._2(); + _4 = tail._3(); + _5 = tail._4(); + _6 = tail._5(); + _7 = tail._6(); + _8 = tail._7(); } /** @@ -74,6 +74,17 @@ public <_0> HCons<_0, Tuple8<_1, _2, _3, _4, _5, _6, _7, _8>> cons(_0 _0) { return new HCons<>(_0, this); } + /** + * Snoc an element onto the back of this {@link Tuple8}. + * + * @param _9 the new last element + * @param <_9> the new last element type + * @return the new {@link HCons consed} {@link Tuple8} + */ + public <_9> HCons<_1, Tuple8<_2, _3, _4, _5, _6, _7, _8, _9>> snoc(_9 _9) { + return singletonHList(_9).cons(_8).cons(_7).cons(_6).cons(_5).cons(_4).cons(_3).cons(_2).cons(_1); + } + /** * {@inheritDoc} */ @@ -353,6 +364,16 @@ AppTrav extends Applicative> AppTrav traverse( return fn.apply(_8).fmap(_8Prime -> fmap(constantly(_8Prime))).fmap(Applicative::coerce).coerce(); } + /** + * Returns a {@link Tuple7}<_1, _2, _3, _4, _5, _6, _7> of all the elements of this + * {@link Tuple8}<_1, _2, _3, _4, _5, _6, _7, _8> except the last. + * + * @return The {@link Tuple7}<_1, _2, _3, _4, _5, _6, _7> representing all but the last element + */ + public Tuple7<_1, _2, _3, _4, _5, _6, _7> init() { + return rotateR8().tail(); + } + /** * Given a value of type A, produced an instance of this tuple with each slot set to that value. * 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..cb4904d35 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 @@ -302,6 +302,10 @@ default Fn2 andThen(Fn2 after return (a, c) -> after.apply(apply(a), c); } + default Fn1 self() { + return this; + } + /** * Static factory method for avoid explicit casting when using method references as {@link Fn1}s. * @@ -335,4 +339,17 @@ static Fn1 fromFunction(Function function) static Pure> pureFn1() { return Constantly::constantly; } + + /** + * Construct an {@link Fn1} that has a reference to itself in scope at the time it is executed (presumably for + * recursive invocations). + * + * @param fn the body of the function, with access to itself + * @param the input type + * @param the output type + * @return the {@link Fn1} + */ + static Fn1 withSelf(Fn2, ? super A, ? extends B> fn) { + return a -> fn.apply(withSelf(fn), a); + } } diff --git a/src/main/java/com/jnape/palatable/lambda/functions/Fn7.java b/src/main/java/com/jnape/palatable/lambda/functions/Fn7.java index 2d27e6be2..15f7a3be9 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/Fn7.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/Fn7.java @@ -8,7 +8,7 @@ import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; /** - * A function taking six arguments. Defined in terms of {@link Fn6}, so similarly auto-curried. + * A function taking seven arguments. Defined in terms of {@link Fn6}, so similarly auto-curried. * * @param The first argument type * @param The second argument type @@ -258,7 +258,7 @@ static Fn7 fn7(Fn4 the first input argument type * @param the second input argument type * @param the third input argument type @@ -276,7 +276,7 @@ static Fn7 fn7(Fn5 the first input argument type * @param the second input argument type * @param the third input argument type diff --git a/src/main/java/com/jnape/palatable/lambda/functions/Fn8.java b/src/main/java/com/jnape/palatable/lambda/functions/Fn8.java index a38e74eb1..60f3a4a3c 100644 --- a/src/main/java/com/jnape/palatable/lambda/functions/Fn8.java +++ b/src/main/java/com/jnape/palatable/lambda/functions/Fn8.java @@ -5,7 +5,7 @@ import com.jnape.palatable.lambda.internal.Runtime; /** - * A function taking six arguments. Defined in terms of {@link Fn7}, so similarly auto-curried. + * A function taking eight arguments. Defined in terms of {@link Fn7}, so similarly auto-curried. * * @param The first argument type * @param The second argument type @@ -274,7 +274,7 @@ static Fn8 fn8( /** * Static factory method for wrapping a curried {@link Fn5} in an {@link Fn8}. * - * @param curriedFn5 the curried fn4 to adapt + * @param curriedFn5 the curried fn5 to adapt * @param the first input argument type * @param the second input argument type * @param the third input argument type @@ -294,7 +294,7 @@ static Fn8 fn8( /** * Static factory method for wrapping a curried {@link Fn6} in an {@link Fn8}. * - * @param curriedFn6 the curried fn4 to adapt + * @param curriedFn6 the curried fn6 to adapt * @param the first input argument type * @param the second input argument type * @param the third input argument type @@ -314,7 +314,7 @@ static Fn8 fn8( /** * Static factory method for wrapping a curried {@link Fn7} in an {@link Fn8}. * - * @param curriedFn7 the curried fn4 to adapt + * @param curriedFn7 the curried fn7 to adapt * @param the first input argument type * @param the second input argument type * @param the third input argument type 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/fn2/$.java b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/$.java new file mode 100644 index 000000000..63c60c051 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functions/builtin/fn2/$.java @@ -0,0 +1,43 @@ +package com.jnape.palatable.lambda.functions.builtin.fn2; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; + +/** + * Function application, represented as a higher-order {@link Fn2} that receives an {@link Fn1} and its argument, and + * applies it. Useful for treating application as a combinator, e.g.: + *
+ * {@code
+ * List> fns     = asList(x -> x + 1, x -> x, x -> x - 1);
+ * List               args    = asList(0, 1, 2);
+ * Iterable           results = zipWith($(), fns, args); // [1, 1, 1]
+ * }
+ * 
+ * + * @param
the applied {@link Fn1 Fn1's} input type + * @param the applied {@link Fn1 Fn1's} output type + */ +public final class $ implements Fn2, A, B> { + private static final $ INSTANCE = new $<>(); + + private $() { + } + + @Override + public B checkedApply(Fn1 fn, A a) { + return fn.apply(a); + } + + @SuppressWarnings("unchecked") + public static $ $() { + return ($) INSTANCE; + } + + public static Fn1 $(Fn1 fn) { + return $.$().apply(fn); + } + + public static B $(Fn1 fn, A a) { + return $.$(fn).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/PredicatedDroppingIterable.java b/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterable.java index d87cf0e09..ea7b68bc7 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterable.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterable.java @@ -1,24 +1,20 @@ package com.jnape.palatable.lambda.internal.iteration; import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.internal.ImmutableQueue; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; - -import static com.jnape.palatable.lambda.functions.builtin.fn2.Any.any; -import static java.util.Collections.singletonList; public final class PredicatedDroppingIterable implements Iterable { - private final List> predicates; - private final Iterable as; + private final ImmutableQueue> predicates; + private final Iterable as; public PredicatedDroppingIterable(Fn1 predicate, Iterable as) { - List> predicates = new ArrayList<>(singletonList(predicate)); + ImmutableQueue> predicates = ImmutableQueue.singleton(predicate); while (as instanceof PredicatedDroppingIterable) { PredicatedDroppingIterable nested = (PredicatedDroppingIterable) as; as = nested.as; - predicates.addAll(0, nested.predicates); + predicates = nested.predicates.concat(predicates); } this.predicates = predicates; this.as = as; @@ -26,7 +22,6 @@ public PredicatedDroppingIterable(Fn1 predicate, I @Override public Iterator iterator() { - Fn1 metaPredicate = a -> any(p -> p.apply(a), predicates); - return new PredicatedDroppingIterator<>(metaPredicate, as.iterator()); + return new PredicatedDroppingIterator<>(predicates, as.iterator()); } } diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterator.java b/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterator.java index 7add62a45..a97b513ac 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterator.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIterator.java @@ -1,19 +1,18 @@ package com.jnape.palatable.lambda.internal.iteration; import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.internal.ImmutableQueue; import java.util.Iterator; import java.util.NoSuchElementException; public final class PredicatedDroppingIterator extends ImmutableIterator { - private final Fn1 predicate; - private final RewindableIterator rewindableIterator; - private boolean finishedDropping; + private final Iterator> predicates; + private final RewindableIterator rewindableIterator; - public PredicatedDroppingIterator(Fn1 predicate, Iterator asIterator) { - this.predicate = predicate; + public PredicatedDroppingIterator(ImmutableQueue> predicates, Iterator asIterator) { + this.predicates = predicates.iterator(); rewindableIterator = new RewindableIterator<>(asIterator); - finishedDropping = false; } @Override @@ -31,11 +30,17 @@ public A next() { } private void dropElementsIfNecessary() { - while (rewindableIterator.hasNext() && !finishedDropping) { - if (!predicate.apply(rewindableIterator.next())) { - rewindableIterator.rewind(); - finishedDropping = true; + while (predicates.hasNext() && rewindableIterator.hasNext()) { + Fn1 predicate = predicates.next(); + boolean predicateDone = false; + + while (rewindableIterator.hasNext() && !predicateDone) { + if (!predicate.apply(rewindableIterator.next())) { + rewindableIterator.rewind(); + predicateDone = true; + } } + } } } diff --git a/src/main/java/com/jnape/palatable/lambda/internal/iteration/RewindableIterator.java b/src/main/java/com/jnape/palatable/lambda/internal/iteration/RewindableIterator.java index 5bdde4312..092e7e98b 100644 --- a/src/main/java/com/jnape/palatable/lambda/internal/iteration/RewindableIterator.java +++ b/src/main/java/com/jnape/palatable/lambda/internal/iteration/RewindableIterator.java @@ -54,9 +54,7 @@ public A retrieve() { if (cache == null) throw new NoSuchElementException("Cache is empty."); - A cache = this.cache; - this.cache = null; - return cache; + return this.cache; } public boolean isEmpty() { 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..cb719060b --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateT.java @@ -0,0 +1,450 @@ +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.Lift; +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.Fn0.fn0; +import static com.jnape.palatable.lambda.functions.Fn1.withSelf; +import static com.jnape.palatable.lambda.functions.builtin.fn2.$.$; +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>>, M>>, MonadRec>> spine; + + private IterateT(Pure pureM, + ImmutableQueue>>, M>>, MonadRec>> spine) { + this.pureM = pureM; + this.spine = spine; + } + + /** + * 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 -> iterateT.runStep() + .fmap(maybeMore -> maybeMore.match( + fn0(() -> terminate(nothing())), + t -> t.into((Maybe maybeA, IterateT as) -> maybeA.match( + fn0(() -> recurse(as)), + a -> terminate(just(tuple(a, as)))))))) + .coerce(); + } + + /** + * Run a single step of this {@link IterateT}, where a step is the smallest amount of work that could possibly be + * productive in advancing through the {@link IterateT}. Useful for implementing interleaving algorithms that + * require {@link IterateT IterateTs} to yield, emit, or terminate as soon as possible, regardless of whether the + * next element is readily available. + * + * @param the witnessed target type of the step + * @return the step + */ + public , IterateT>>, M>> MStep runStep() { + return spine.head().match( + fn0(() -> pureM., IterateT>>, MStep>apply(nothing())), + thunkOrReal -> thunkOrReal.match( + thunk -> thunk.apply()., IterateT>>>fmap(m -> m.match( + fn0(() -> just(tuple(nothing(), new IterateT<>(pureM, spine.tail())))), + t -> just(t.biMap(Maybe::just, + as -> new IterateT<>(pureM, as.spine.concat(spine.tail())))))) + .coerce(), + ma -> ma.fmap(a -> just(tuple(just(a), new IterateT<>(pureM, spine.tail())))).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, spine.pushFront(b(head))); + } + + /** + * 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, spine.pushBack(b(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, spine.concat(other.spine)); + } + + /** + * 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 + @SuppressWarnings("RedundantTypeArguments") + public IterateT trampolineM( + Fn1, IterateT>> fn) { + return $(withSelf( + (self, queued) -> suspended( + () -> pureM.>, MonadRec>, M>>apply(queued) + .trampolineM(q -> q.runIterateT().>, Maybe>>>>fmap(m -> m.match( + __ -> terminate(nothing()), + into((rr, tail) -> rr.biMap( + a -> fn.apply(a).>>coerce().concat(tail), + b -> just(tuple(b, self.apply(tail)))))))), + pureM)), + flatMap(fn)); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT flatMap(Fn1>> f) { + return suspended(() -> maybeT(runIterateT()) + .trampolineM(into((a, as) -> maybeT( + f.apply(a).>coerce().runIterateT() + .flatMap(maybePair -> maybePair.match( + fn0(() -> as.runIterateT() + .fmap(maybeResult -> maybeResult.fmap(RecursiveResult::recurse))), + t -> pureM.apply(just(terminate(t.fmap(mb -> mb.concat(as.flatMap(f)))))) + ))))) + .runMaybeT(), + pureM); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT fmap(Fn1 fn) { + return MonadT.super.fmap(fn).coerce(); + } + + /** + * {@inheritDoc} + */ + @Override + public IterateT pure(B b) { + return singleton(pureM.>apply(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(); + } + + /** + * 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()); + } + + /** + * 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 IterateT.empty(Pure.of(ma)).cons(ma); + } + + /** + * 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 suspended(() -> unwrapped, Pure.of(unwrapped)); + } + + /** + * 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, singleton(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) { + Pure pureM = Pure.of(mb); + return $(withSelf((self, mmb) -> suspended(() -> maybeT(mmb.flatMap(fn)) + .fmap(ab -> ab.>fmap(b -> self.apply(pureM.apply(b)))) + .runMaybeT(), pureM)), 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.singleton(a(thunk))); + } + + /** + * 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)); + } + + /** + * The canonical {@link Pure} instance for {@link IterateT}. + * + * @param pureM the argument {@link Monad} {@link Pure} + * @param the argument {@link Monad} witness + * @return the {@link Pure} instance + */ + public static > Pure> pureIterateT(Pure pureM) { + return new Pure>() { + @Override + public IterateT checkedApply(A a) { + return liftIterateT().apply(pureM.>apply(a)); + } + }; + } + + /** + * {@link Lift} for {@link IterateT}. + * + * @return the {@link Monad} lifted into {@link IterateT} + */ + public static Lift> liftIterateT() { + return IterateT::singleton; + } +} 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..95dcdfd21 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 @@ -1,6 +1,7 @@ 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.functions.Fn1; import com.jnape.palatable.lambda.functions.recursion.RecursiveResult; import com.jnape.palatable.lambda.functions.specialized.Lift; @@ -9,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; @@ -16,6 +18,8 @@ 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.functions.Fn0.fn0; import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; import static com.jnape.palatable.lambda.functions.recursion.RecursiveResult.terminate; @@ -26,7 +30,7 @@ * @param the carrier type */ public final class MaybeT, A> implements - MonadT, MaybeT> { + MonadT, MaybeT>, MonadError> { private final MonadRec, M> mma; @@ -44,6 +48,47 @@ 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} + */ + @Override + public MaybeT throwError(Unit unit) { + return maybeT(mma.pure(nothing())); + } + + /** + * {@inheritDoc} + */ + @Override + public MaybeT catchError(Fn1>> recoveryFn) { + return maybeT(mma.flatMap(maybeA -> maybeA.match( + fn0(() -> recoveryFn.apply(UNIT).>coerce().runMaybeT()), + a -> mma.pure(just(a))))); + } + /** * {@inheritDoc} */ @@ -134,7 +179,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..e4c8ec5f0 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} */ @@ -108,7 +120,7 @@ public ReaderT pure(B b) { */ @Override public ReaderT fmap(Fn1 fn) { - return MonadT.super.fmap(fn).coerce(); + return readerT(r -> runReaderT(r).fmap(fn)); } /** @@ -192,6 +204,19 @@ public ReaderT> carry() { return (ReaderT>) Cartesian.super.carry(); } + /** + * Given a {@link Pure} ask will give you access to the input within the monadic embedding + * + * @param pureM the {@link Pure} instance for the given {@link Monad} + * @param the input and output type of the returned ReaderT + * @param the returned {@link Monad} + * @return the {@link ReaderT} + */ + public static > ReaderT ask(Pure pureM) { + //noinspection Convert2MethodRef + return readerT(a -> pureM.apply(a)); + } + /** * Lift a {@link Fn1 function} (R -> {@link Monad}<A, M>) into a {@link ReaderT} instance. * diff --git a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateT.java b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateT.java index 071ce0dc5..219ad3e03 100644 --- a/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateT.java +++ b/src/main/java/com/jnape/palatable/lambda/monad/transformer/builtin/StateT.java @@ -144,7 +144,7 @@ public StateT pure(B b) { */ @Override public StateT fmap(Fn1 fn) { - return MonadT.super.fmap(fn).coerce(); + return stateT(s -> runStateT(s).fmap(t -> t.biMapL(fn))); } /** @@ -292,7 +292,7 @@ public static , A> StateT stateT( public static > Pure> pureStateT(Pure pureM) { return new Pure>() { @Override - public StateT checkedApply(A a) throws Throwable { + public StateT checkedApply(A a) { return stateT(pureM.>apply(a)); } }; 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..c8625bbf8 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 @@ -34,9 +34,12 @@ public final class WriterT, A> implements MonadWriter>, MonadT, WriterT> { + private final Pure pureM; private final Fn1, ? extends MonadRec, M>> writerFn; - private WriterT(Fn1, ? extends MonadRec, M>> writerFn) { + private WriterT(Pure pureM, + Fn1, ? extends MonadRec, M>> writerFn) { + this.pureM = pureM; this.writerFn = writerFn; } @@ -46,19 +49,43 @@ 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} */ @Override public WriterT> listens(Fn1 fn) { - return new WriterT<>(writerFn.fmap(m -> m.fmap(into((a, w) -> both(both(constantly(a), fn), id(), w))))); + return new WriterT<>(pureM, writerFn.fmap(m -> m.fmap(into((a, w) -> both(both(constantly(a), fn), id(), w))))); } /** @@ -66,7 +93,7 @@ public WriterT> listens(Fn1 fn) { */ @Override public WriterT censor(Fn1 fn) { - return new WriterT<>(writerFn.fmap(mt -> mt.fmap(t -> t.fmap(fn)))); + return new WriterT<>(pureM, writerFn.fmap(mt -> mt.fmap(t -> t.fmap(fn)))); } /** @@ -83,7 +110,7 @@ public > WriterT lift(MonadRec mb) { @Override public WriterT trampolineM( Fn1, WriterT>> fn) { - return new WriterT<>(monoid -> runWriterT(monoid).trampolineM(into((a, w) -> fn.apply(a) + return new WriterT<>(pureM, monoid -> runWriterT(monoid).trampolineM(into((a, w) -> fn.apply(a) .>>coerce() .runWriterT(monoid).fmap(t -> t.fmap(monoid.apply(w))) .fmap(into((aOrB, w_) -> aOrB.biMap(a_ -> tuple(a_, w_), b -> tuple(b, w_))))))); @@ -102,7 +129,7 @@ public WriterT fmap(Fn1 fn) { */ @Override public WriterT pure(B b) { - return new WriterT<>(m -> runWriterT(m).pure(tuple(b, m.identity()))); + return new WriterT<>(pureM, m -> pureM.apply(tuple(b, m.identity()))); } /** @@ -110,7 +137,7 @@ public WriterT pure(B b) { */ @Override public WriterT flatMap(Fn1>> f) { - return new WriterT<>(monoid -> writerFn.apply(monoid) + return new WriterT<>(pureM, monoid -> writerFn.apply(monoid) .flatMap(into((a, w) -> f.apply(a).>coerce().runWriterT(monoid) .fmap(t -> t.fmap(monoid.apply(w)))))); } @@ -120,7 +147,7 @@ public WriterT flatMap(Fn1 WriterT zip(Applicative, WriterT> appFn) { - return new WriterT<>(monoid -> runWriterT(monoid) + return new WriterT<>(pureM, monoid -> runWriterT(monoid) .zip(appFn.>>coerce().runWriterT(monoid) .fmap(into((f, y) -> into((a, x) -> tuple(f.apply(a), monoid.apply(x, y))))))); } @@ -172,7 +199,7 @@ public static > WriterT tell(MonadRec, A> WriterT listen(MonadRec ma) { - return new WriterT<>(monoid -> ma.fmap(a -> tuple(a, monoid.identity()))); + return new WriterT<>(Pure.of(ma), monoid -> ma.fmap(a -> tuple(a, monoid.identity()))); } /** @@ -185,7 +212,7 @@ public static , A> WriterT listen(MonadRec< * @return the {@link WriterT} */ public static , A> WriterT writerT(MonadRec, M> maw) { - return new WriterT<>(constantly(maw)); + return new WriterT<>(Pure.of(maw), constantly(maw)); } /** diff --git a/src/main/java/com/jnape/palatable/lambda/monoid/Monoid.java b/src/main/java/com/jnape/palatable/lambda/monoid/Monoid.java index 8ecf83775..59f4ce1c7 100644 --- a/src/main/java/com/jnape/palatable/lambda/monoid/Monoid.java +++ b/src/main/java/com/jnape/palatable/lambda/monoid/Monoid.java @@ -82,7 +82,7 @@ default A foldLeft(A a, Iterable as) { */ @Override default Lazy foldRight(A a, Iterable as) { - return lazy(() -> flip().foldMap(id(), reverse(cons(a, as)))); + return lazy(() -> flip().foldMap(id(), cons(a, reverse(as)))); } /** 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/Semigroup.java b/src/main/java/com/jnape/palatable/lambda/semigroup/Semigroup.java index a061328ce..d4cc1e6b4 100644 --- a/src/main/java/com/jnape/palatable/lambda/semigroup/Semigroup.java +++ b/src/main/java/com/jnape/palatable/lambda/semigroup/Semigroup.java @@ -39,7 +39,7 @@ default A foldLeft(A a, Iterable as) { * @see FoldRight */ default Lazy foldRight(A a, Iterable as) { - return FoldRight.foldRight((y, lazyX) -> lazyX.fmap(x -> apply(x, y)), lazy(a), as); + return FoldRight.foldRight((y, lazyX) -> lazyX.fmap(x -> apply(y, x)), lazy(a), as); } /** diff --git a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/Absent.java b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/Absent.java index a692eac77..222a5e7dc 100644 --- a/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/Absent.java +++ b/src/main/java/com/jnape/palatable/lambda/semigroup/builtin/Absent.java @@ -2,11 +2,21 @@ import com.jnape.palatable.lambda.adt.Maybe; import com.jnape.palatable.lambda.functions.Fn1; -import com.jnape.palatable.lambda.functions.builtin.fn3.LiftA2; +import com.jnape.palatable.lambda.functions.builtin.fn3.FoldRight; import com.jnape.palatable.lambda.functions.specialized.SemigroupFactory; +import com.jnape.palatable.lambda.functor.builtin.Lazy; import com.jnape.palatable.lambda.monoid.builtin.Present; import com.jnape.palatable.lambda.semigroup.Semigroup; +import static com.jnape.palatable.lambda.adt.Maybe.nothing; +import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; +import static com.jnape.palatable.lambda.functions.builtin.fn2.Into.into; +import static com.jnape.palatable.lambda.functions.builtin.fn3.LiftA2.liftA2; +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.functions.recursion.Trampoline.trampoline; +import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy; + /** * A {@link Semigroup} instance formed by {@link Maybe}<A> and a semigroup over A. The * application to two {@link Maybe} values is absence-biased, such that for a given {@link Maybe} x @@ -32,7 +42,7 @@ private Absent() { @Override public Semigroup> checkedApply(Semigroup aSemigroup) { - return LiftA2., Maybe>liftA2(aSemigroup)::apply; + return shortCircuitSemigroup(aSemigroup); } @SuppressWarnings("unchecked") @@ -40,8 +50,8 @@ public static Absent absent() { return (Absent) INSTANCE; } - public static Semigroup> absent(Semigroup semigroup) { - return Absent.absent().apply(semigroup); + public static Semigroup> absent(Semigroup aSemigroup) { + return shortCircuitSemigroup(aSemigroup); } public static Fn1, Maybe> absent(Semigroup aSemigroup, Maybe x) { @@ -51,4 +61,34 @@ public static Fn1, Maybe> absent(Semigroup aSemigroup, Maybe< public static Maybe absent(Semigroup semigroup, Maybe x, Maybe y) { return absent(semigroup, x).apply(y); } + + private static Semigroup> shortCircuitSemigroup(Semigroup aSemigroup) { + return new Semigroup>() { + @Override + public Maybe checkedApply(Maybe maybeX, Maybe maybeY) { + return liftA2(aSemigroup, maybeX, maybeY); + } + + @Override + public Maybe foldLeft(Maybe acc, Iterable> maybes) { + return trampoline( + into((res, it) -> res.equals(nothing()) || !it.hasNext() + ? terminate(res) + : recurse(tuple(liftA2(aSemigroup, res, it.next()), it))), + tuple(acc, maybes.iterator())); + } + + @Override + public Lazy> foldRight(Maybe accumulation, Iterable> as) { + boolean shouldShortCircuit = accumulation == nothing(); + if (shouldShortCircuit) + return lazy(accumulation); + return FoldRight.foldRight( + (maybeX, acc) -> maybeX.lazyZip(acc.fmap(maybeY -> maybeY.fmap(aSemigroup.flip()))), + lazy(accumulation), + as + ); + } + }; + } } 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/TheseTest.java b/src/test/java/com/jnape/palatable/lambda/adt/TheseTest.java index 36a195011..e9f764d49 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/TheseTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/TheseTest.java @@ -12,9 +12,12 @@ import testsupport.traits.MonadRecLaws; import testsupport.traits.TraversableLaws; +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.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.adt.These.fromMaybes; import static com.jnape.palatable.lambda.functor.builtin.Lazy.lazy; import static com.jnape.palatable.traitor.framework.Subjects.subjects; import static org.junit.Assert.assertEquals; @@ -48,4 +51,12 @@ public void staticPure() { These these = These.pureThese().apply(1); assertEquals(b(1), these); } + + @Test + public void fromMaybesPermutations() { + assertEquals(nothing(), fromMaybes(nothing(), nothing())); + assertEquals(just(These.a(1)), fromMaybes(just(1), nothing())); + assertEquals(just(These.b(1)), fromMaybes(nothing(), just(1))); + assertEquals(just(These.both(1, "hello")), fromMaybes(just(1), just("hello"))); + } } \ No newline at end of file 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..3e3513a3a 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 @@ -6,11 +6,11 @@ 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.hamcrest.CoreMatchers.instanceOf; 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 org.junit.Assert.assertThat; public class HListTest { @@ -36,22 +36,33 @@ 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()); } @Test - @SuppressWarnings({"EqualsWithItself", "EqualsBetweenInconvertibleTypes"}) public void equality() { - assertTrue(nil().equals(nil())); - assertTrue(cons(1, nil()).equals(cons(1, nil()))); + assertEquals(nil(), nil()); + assertEquals(cons(1, nil()), cons(1, nil())); - assertFalse(cons(1, nil()).equals(nil())); - assertFalse(nil().equals(cons(1, nil()))); + assertNotEquals(cons(1, nil()), nil()); + assertNotEquals(nil(), cons(1, nil())); - assertFalse(cons(1, cons(2, nil())).equals(cons(1, nil()))); - assertFalse(cons(1, nil()).equals(cons(1, cons(2, nil())))); + assertNotEquals(cons(1, cons(2, nil())), cons(1, nil())); + assertNotEquals(cons(1, nil()), cons(1, cons(2, nil()))); } @Test diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/SingletonHListTest.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/SingletonHListTest.java index 57a9f85b7..7a577f2ce 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/SingletonHListTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/SingletonHListTest.java @@ -13,6 +13,7 @@ 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 com.jnape.palatable.lambda.adt.hlist.SingletonHList.pureSingletonHList; import static org.junit.Assert.assertEquals; @@ -46,6 +47,11 @@ public void cons() { assertEquals(new Tuple2<>("0", singletonHList), singletonHList.cons("0")); } + @Test + public void snoc() { + assertEquals(tuple((byte) 127, 'x'), singletonHList((byte) 127).snoc('x')); + } + @Test public void intoAppliesHeadToFn() { assertEquals("FOO", singletonHList("foo").into(String::toUpperCase)); diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple2Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple2Test.java index 32483a168..6637386ce 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple2Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple2Test.java @@ -63,11 +63,21 @@ public void tail() { assertEquals(new SingletonHList<>(2), tuple2.tail()); } + @Test + public void init() { + assertEquals(new SingletonHList<>(1), tuple2.init()); + } + @Test public void cons() { assertEquals(new Tuple3<>(0, tuple2), tuple2.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple(Long.MAX_VALUE, 123, "hi"), tuple(Long.MAX_VALUE, 123).snoc("hi")); + } + @Test public void accessors() { assertEquals((Integer) 1, tuple2._1()); diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple3Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple3Test.java index 9ac57fcfb..a4cad8198 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple3Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple3Test.java @@ -18,6 +18,7 @@ import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; import static com.jnape.palatable.lambda.adt.hlist.Tuple3.pureTuple; import static com.jnape.palatable.lambda.functions.builtin.fn1.Repeat.repeat; +import static java.time.Duration.ofSeconds; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @@ -62,6 +63,12 @@ public void cons() { assertEquals(new Tuple4<>(0, tuple3), tuple3.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple("qux", Long.MIN_VALUE, 7, ofSeconds(13)), + tuple("qux", Long.MIN_VALUE, 7).snoc(ofSeconds(13))); + } + @Test public void accessors() { assertEquals((Integer) 1, tuple3._1()); @@ -119,4 +126,10 @@ public void staticPure() { Tuple3 tuple = pureTuple(1, "2").apply('3'); assertEquals(tuple(1, "2", '3'), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2), + tuple(1, 2, 3).init()); + } } diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple4Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple4Test.java index b2a1d6319..13d5da838 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple4Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple4Test.java @@ -62,6 +62,11 @@ public void cons() { assertEquals(new Tuple5<>(0, tuple4), tuple4.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple("qux", 7, "foo", 13L, 17), tuple("qux", 7, "foo", 13L).snoc(17)); + } + @Test public void accessors() { assertEquals((Integer) 1, tuple4._1()); @@ -122,4 +127,10 @@ public void staticPure() { Tuple4 tuple = pureTuple(1, "2", '3').apply(true); assertEquals(tuple(1, "2", '3', true), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2, 3), + tuple(1, 2, 3, 4).init()); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple5Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple5Test.java index 83b300b97..7b81a2190 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple5Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple5Test.java @@ -63,6 +63,11 @@ public void cons() { assertEquals(new HCons<>(0, tuple5), tuple5.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple("a", 5, "b", 7, "c", 11), tuple("a", 5, "b", 7, "c").snoc(11)); + } + @Test public void accessors() { assertEquals((Integer) 1, tuple5._1()); @@ -128,4 +133,10 @@ public void staticPure() { Tuple5 tuple = pureTuple(1, "2", '3', true).apply(5f); assertEquals(tuple(1, "2", '3', true, 5f), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2, 3, 4), + tuple(1, 2, 3, 4, 5).init()); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple6Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple6Test.java index 902b4b254..880eee1e4 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple6Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple6Test.java @@ -64,6 +64,11 @@ public void cons() { assertEquals(new HCons<>(0, tuple6), tuple6.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple(5L, "a", 7, "b", 11, "c", 13), tuple(5L, "a", 7, "b", 11, "c").snoc(13)); + } + @Test public void accessors() { assertEquals((Float) 2.0f, tuple6._1()); @@ -132,4 +137,10 @@ public void staticPure() { Tuple6 tuple = pureTuple(1, "2", '3', true, 5f).apply((byte) 6); assertEquals(tuple(1, "2", '3', true, 5f, (byte) 6), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2, 3, 4, 5), + tuple(1, 2, 3, 4, 5, 6).init()); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple7Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple7Test.java index 0e8a68aec..d5b13fd24 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple7Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple7Test.java @@ -64,6 +64,11 @@ public void cons() { assertEquals(new HCons<>(0, tuple7), tuple7.cons(0)); } + @Test + public void snoc() { + assertEquals(tuple("b", 7L, "c", 11, "d", 13, "e", 'f'), tuple("b", 7L, "c", 11, "d", 13, "e").snoc('f')); + } + @Test public void accessors() { assertEquals((Byte) (byte) 127, tuple7._1()); @@ -136,4 +141,10 @@ public void staticPure() { pureTuple((byte) 1, (short) 2, 3, 4L, 5F, 6D).apply(true); assertEquals(tuple((byte) 1, (short) 2, 3, 4L, 5F, 6D, true), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2, 3, 4, 5, 6), + tuple(1, 2, 3, 4, 5, 6, 7).init()); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple8Test.java b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple8Test.java index 4d2a8cb76..ced5531b5 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple8Test.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hlist/Tuple8Test.java @@ -14,6 +14,8 @@ import testsupport.traits.MonadRecLaws; import testsupport.traits.TraversableLaws; +import java.time.LocalDate; + import static com.jnape.palatable.lambda.adt.Maybe.just; import static com.jnape.palatable.lambda.adt.Maybe.nothing; import static com.jnape.palatable.lambda.adt.hlist.HList.tuple; @@ -67,6 +69,15 @@ public void cons() { assertEquals(new HCons<>(0, tuple8), tuple8.cons(0)); } + @Test + public void snoc() { + LocalDate last = LocalDate.of(2020, 4, 14); + HCons> actual = + tuple("b", 7L, "c", 11, "d", 13, "e", 15L).snoc(last); + assertEquals("b", actual.head()); + assertEquals(actual.tail(), tuple(7L, "c", 11, "d", 13, "e", 15L, last)); + } + @Test public void accessors() { assertEquals((Short) (short) 65535, tuple8._1()); @@ -147,4 +158,10 @@ public void staticPure() { pureTuple((byte) 1, (short) 2, 3, 4L, 5F, 6D, true).apply('8'); assertEquals(tuple((byte) 1, (short) 2, 3, 4L, 5F, 6D, true, '8'), tuple); } + + @Test + public void init() { + assertEquals(tuple(1, 2, 3, 4, 5, 6, 7), + tuple(1, 2, 3, 4, 5, 6, 7, 8).init()); + } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java b/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java index 39400c6b0..047aea1db 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java @@ -8,8 +8,8 @@ import testsupport.traits.Equivalence; import testsupport.traits.FunctorLaws; import testsupport.traits.MonadLaws; -import testsupport.traits.MonadRecLaws; import testsupport.traits.MonadReaderLaws; +import testsupport.traits.MonadRecLaws; import testsupport.traits.MonadWriterLaws; import java.util.function.Function; @@ -105,4 +105,9 @@ public void staticPure() { Fn1 fn1 = Fn1.pureFn1().apply(1); assertEquals((Integer) 1, fn1.apply("anything")); } + + @Test + public void withSelf() { + assertEquals((Integer) 15, Fn1.withSelf((f, x) -> x > 1 ? x + f.apply(x - 1) : x).apply(5)); + } } 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/fn2/$Test.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/$Test.java new file mode 100644 index 000000000..166e99f29 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/$Test.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.functions.builtin.fn2; + +import org.junit.Test; + +import static com.jnape.palatable.lambda.functions.Fn2.fn2; +import static com.jnape.palatable.lambda.functions.builtin.fn2.$.$; +import static org.junit.Assert.assertEquals; + +public class $Test { + + @Test + public void application() { + assertEquals((Integer) 1, $(x -> x + 1, 0)); + assertEquals((Integer) 1, $.$(x -> x + 1).apply(0)); + } + + @Test + public void curryingInference() { + assertEquals((Integer) 1, $($(fn2(Integer::sum), 0), 1)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/DropWhileTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/DropWhileTest.java index 83903f451..ff4551a11 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/DropWhileTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn2/DropWhileTest.java @@ -56,7 +56,13 @@ public void deforestingExecutesPredicatesInOrder() { innerInvocations.add(x); return x > 2; }, asList(1, 2, 3))).forEach(__ -> {}); - assertThat(innerInvocations, iterates(1, 2, 3)); - assertThat(outerInvocations, iterates(1, 2)); + assertThat(innerInvocations, iterates(1)); + assertThat(outerInvocations, iterates(1, 2, 3)); + } + + @Test + public void eachLayerIsAppliedOnce() { + assertThat(dropWhile(i -> i % 2 == 0, dropWhile(i -> i % 2 == 1, asList(1, 2, 3))), + iterates(3)); } } 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/FoldRightTest.java b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/FoldRightTest.java index 912ee9b4a..62f6d43c9 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/FoldRightTest.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/builtin/fn3/FoldRightTest.java @@ -29,7 +29,7 @@ public Fn1, Iterable> createTestSubject() { @Test public void foldRightAccumulatesRightToLeft() { - assertThat(foldRight((a, lazyB) -> lazyB.fmap(b -> explainFold().apply(a, b)), + assertThat(foldRight((a, lazyAcc) -> lazyAcc.fmap(acc -> explainFold().apply(a, acc)), lazy("5"), asList("1", "2", "3", "4")) .value(), 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/internal/iteration/PredicatedDroppingIteratorTest.java b/src/test/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIteratorTest.java index 7fb0613c3..44fcde9f1 100644 --- a/src/test/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIteratorTest.java +++ b/src/test/java/com/jnape/palatable/lambda/internal/iteration/PredicatedDroppingIteratorTest.java @@ -1,15 +1,18 @@ package com.jnape.palatable.lambda.internal.iteration; import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.internal.ImmutableQueue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; +import static java.util.Collections.singletonList; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static testsupport.Mocking.mockIteratorToHaveValues; @@ -25,7 +28,7 @@ public class PredicatedDroppingIteratorTest { @Before public void setUp() { - predicatedDroppingIterator = new PredicatedDroppingIterator<>(EVEN, iterator); + predicatedDroppingIterator = new PredicatedDroppingIterator<>(ImmutableQueue.singleton(EVEN), iterator); } @Test diff --git a/src/test/java/com/jnape/palatable/lambda/internal/iteration/RewindableIteratorTest.java b/src/test/java/com/jnape/palatable/lambda/internal/iteration/RewindableIteratorTest.java index 33bda1ad4..f07fe55a5 100644 --- a/src/test/java/com/jnape/palatable/lambda/internal/iteration/RewindableIteratorTest.java +++ b/src/test/java/com/jnape/palatable/lambda/internal/iteration/RewindableIteratorTest.java @@ -10,6 +10,7 @@ import java.util.NoSuchElementException; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static testsupport.Mocking.mockIteratorToHaveValues; @@ -51,13 +52,14 @@ public void cannotRewindIfNoValuesIterated() { rewindableIterator.rewind(); } - @Test(expected = NoSuchElementException.class) - public void cannotRewindTheSameElementTwice() { + @Test + public void canRewindTheSameElementTwice() { mockIteratorToHaveValues(iterator, 1, 2, 3); rewindableIterator.next(); rewindableIterator.rewind(); rewindableIterator.next(); rewindableIterator.rewind(); + assertEquals(1, rewindableIterator.next()); } @Test 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..7c2997ca6 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/monad/transformer/builtin/IterateTTest.java @@ -0,0 +1,352 @@ +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.lambda.monoid.Monoid; +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 java.util.concurrent.atomic.AtomicInteger; + +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.fn1.Constantly.constantly; +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.iterateT; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.liftIterateT; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.of; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.pureIterateT; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.singleton; +import static com.jnape.palatable.lambda.monad.transformer.builtin.IterateT.suspended; +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))); + } + + @Test + public void staticPure() { + assertEquals(new Identity<>(singletonList(1)), + pureIterateT(pureIdentity()) + ., Integer>>apply(1) + ., Identity>>toCollection(ArrayList::new)); + } + + @Test + public void staticLift() { + assertEquals(new Identity<>(singletonList(1)), + liftIterateT() + ., IterateT, Integer>>apply(new Identity<>(1)) + ., Identity>>toCollection(ArrayList::new)); + } + + @Test + public void trampolineMRecursesBreadth() { + IterateT, Integer> firstFour = of(new Identity<>(1), new Identity<>(2), new Identity<>(3), new Identity<>(4)); + IterateT, Integer> trampolined = firstFour + .trampolineM(x -> (x % 3 == 0 && (x < 30)) + ? of(new Identity<>(terminate(x + 10)), new Identity<>(recurse(x + 11)), new Identity<>(recurse(x + 12)), new Identity<>(recurse(x + 13))) + : singleton(new Identity<>(terminate(x)))); + assertThat(trampolined, iterates(1, 2, 13, 14, 25, 26, 37, 38, 39, 40, 28, 16, 4)); + } + + @Test + public void flatMapToEmptyStackSafety() { + assertEquals(new Identity<>(UNIT), + unfold(x -> new Identity<>(x <= STACK_EXPLODING_NUMBER ? just(tuple(x, x + 1)) : nothing()), + new Identity<>(1)) + .flatMap(constantly(iterateT(new Identity<>(nothing())))) + .forEach(constantly(new Identity<>(UNIT)))); + + assertEquals((Integer) 1_250_025_000, + unfold(x -> listen(x <= STACK_EXPLODING_NUMBER ? just(tuple(x, x + 1)) : nothing()), + Writer.listen(1)) + .flatMap(x -> iterateT(writer(tuple(nothing(), x)))) + .>forEach(constantly(listen(UNIT))) + .runWriter(Monoid.monoid(Integer::sum, 0)) + ._2()); + } + + @Test + public void flatMapCostsNoMoreEffortThanRequiredToYieldFirstValue() { + AtomicInteger flatMapCost = new AtomicInteger(0); + AtomicInteger unfoldCost = new AtomicInteger(0); + assertEquals(just(1), + unfold(x -> { + unfoldCost.incrementAndGet(); + return new Identity<>(x <= 10 ? just(tuple(x, x + 1)) : nothing()); + }, + new Identity<>(1)) + .flatMap(x -> { + flatMapCost.incrementAndGet(); + return singleton(new Identity<>(x)); + }) + ., Integer>>>>>runIterateT() + .runIdentity() + .fmap(Tuple2::_1)); + assertEquals(1, flatMapCost.get()); + assertEquals(1, unfoldCost.get()); + } + + @Test + public void runStep() { + assertEquals(new Identity<>(nothing()), + IterateT., Integer>empty(pureIdentity()) + ., IterateT, Integer>>>>>runStep()); + + Tuple2, IterateT, Integer>> singletonStep = + singleton(new Identity<>(1)) + ., IterateT, Integer>>>>>runStep() + .runIdentity() + .orElseThrow(AssertionError::new); + assertEquals(just(1), singletonStep._1()); + assertThat(singletonStep._2(), isEmpty()); + + Tuple2, IterateT, Integer>> emptySuspendedStep = + IterateT., Integer>suspended(() -> new Identity<>(nothing()), + pureIdentity()) + ., IterateT, Integer>>>>>runStep() + .runIdentity() + .orElseThrow(AssertionError::new); + + assertEquals(nothing(), emptySuspendedStep._1()); + assertThat(emptySuspendedStep._2(), isEmpty()); + + Tuple2, IterateT, Integer>> nonEmptySuspendedStep = + suspended(() -> new Identity<>(just(tuple(1, empty(pureIdentity())))), + pureIdentity()) + ., IterateT, Integer>>>>>runStep() + .runIdentity() + .orElseThrow(AssertionError::new); + + assertEquals(just(1), nonEmptySuspendedStep._1()); + assertThat(nonEmptySuspendedStep._2(), isEmpty()); + } +} \ 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..1a81e1c57 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,6 +22,9 @@ import static com.jnape.palatable.lambda.adt.Either.right; import static com.jnape.palatable.lambda.adt.Maybe.just; import static com.jnape.palatable.lambda.adt.Maybe.nothing; +import static com.jnape.palatable.lambda.adt.Unit.UNIT; +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; @@ -30,6 +33,7 @@ import static com.jnape.palatable.lambda.monad.transformer.builtin.MaybeT.pureMaybeT; 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 MaybeTTest { @@ -41,6 +45,13 @@ public class MaybeTTest { maybeT(left("foo"))); } + @Test + public void monadError() { + assertLaws(subjects(maybeT(new Identity<>(nothing())), maybeT(new Identity<>(just(1)))), + UNIT, + e -> maybeT(new Identity<>(just(2)))); + } + @Test public void lazyZip() { assertEquals(maybeT(right(just(2))), @@ -70,4 +81,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..983e7113a 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 @@ -2,21 +2,18 @@ import com.jnape.palatable.lambda.adt.Maybe; import com.jnape.palatable.lambda.adt.Unit; +import com.jnape.palatable.lambda.functions.Fn1; import com.jnape.palatable.lambda.functor.builtin.Identity; import com.jnape.palatable.lambda.io.IO; import com.jnape.palatable.traitor.annotations.TestTraits; 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; +import java.util.concurrent.atomic.AtomicInteger; import static com.jnape.palatable.lambda.adt.Maybe.just; import static com.jnape.palatable.lambda.adt.Unit.UNIT; @@ -58,6 +55,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 = @@ -78,4 +84,24 @@ public void composedZip() { .unsafePerformAsyncIO(Executors.newFixedThreadPool(2)) .join(); } + + @Test + public void fmapInteractions() { + AtomicInteger invocations = new AtomicInteger(0); + ReaderT, Integer> readerT = readerT(i -> { + invocations.incrementAndGet(); + return new Identity<>(i); + }); + + Fn1 plusOne = x -> x + 1; + readerT.fmap(plusOne).fmap(plusOne).fmap(plusOne).runReaderT(0); + assertEquals(1, invocations.get()); + } + + @Test + public void askRetrievesInput() { + assertEquals(new Identity<>(1), + ReaderT.>ask(pureIdentity()) + .>runReaderT(1)); + } } \ No newline at end of file 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..785165952 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; @@ -18,15 +16,21 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import static com.jnape.palatable.lambda.adt.Maybe.just; 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.functor.builtin.Identity.pureIdentity; 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.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +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 +51,89 @@ 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")))); + } + + @Test + public void fmapInteractions() { + AtomicInteger invocations = new AtomicInteger(0); + StateT., Integer>gets(x -> { + invocations.incrementAndGet(); + return new Identity<>(x); + }) + .fmap(id()) + .fmap(id()) + .fmap(id()) + .>>runStateT(0); + assertEquals(1, invocations.get()); } } \ 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/MonoidTest.java b/src/test/java/com/jnape/palatable/lambda/monoid/MonoidTest.java index 87cfced3e..135d87195 100644 --- a/src/test/java/com/jnape/palatable/lambda/monoid/MonoidTest.java +++ b/src/test/java/com/jnape/palatable/lambda/monoid/MonoidTest.java @@ -1,6 +1,7 @@ package com.jnape.palatable.lambda.monoid; import com.jnape.palatable.lambda.adt.Maybe; +import com.jnape.palatable.lambda.functor.builtin.Lazy; import org.junit.Test; import java.util.List; @@ -10,6 +11,7 @@ import static com.jnape.palatable.lambda.monoid.Monoid.monoid; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static testsupport.functions.ExplainFold.explainFold; public class MonoidTest { @@ -25,6 +27,13 @@ public void reduceRight() { assertEquals((Integer) 6, sum.reduceRight(asList(1, 2, 3))); } + @Test + public void foldRight() { + Lazy lazyString = monoid(explainFold()::apply, "0") + .foldRight("4", asList("1", "2", "3")); + assertEquals("(1 + (2 + (3 + (4 + 0))))", lazyString.value()); + } + @Test public void foldMap() { Monoid sum = monoid(Integer::sum, 0); 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/SemigroupTest.java b/src/test/java/com/jnape/palatable/lambda/semigroup/SemigroupTest.java index 3291b6b1b..712c09e02 100644 --- a/src/test/java/com/jnape/palatable/lambda/semigroup/SemigroupTest.java +++ b/src/test/java/com/jnape/palatable/lambda/semigroup/SemigroupTest.java @@ -4,18 +4,19 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static testsupport.functions.ExplainFold.explainFold; public class SemigroupTest { @Test public void foldLeft() { - Semigroup sum = (x, y) -> x + y; - assertEquals((Integer) 6, sum.foldLeft(0, asList(1, 2, 3))); + Semigroup foldFn = explainFold()::apply; + assertEquals("(((0 + 1) + 2) + 3)", foldFn.foldLeft("0", asList("1", "2", "3"))); } @Test public void foldRight() { - Semigroup sum = (x, y) -> x + y; - assertEquals((Integer) 6, sum.foldRight(0, asList(1, 2, 3)).value()); + Semigroup foldFn = explainFold()::apply; + assertEquals("(1 + (2 + (3 + 0)))", foldFn.foldRight("0", asList("1", "2", "3")).value()); } } \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/AbsentTest.java b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/AbsentTest.java index 5ef25395e..9d3dc4524 100644 --- a/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/AbsentTest.java +++ b/src/test/java/com/jnape/palatable/lambda/semigroup/builtin/AbsentTest.java @@ -1,10 +1,17 @@ package com.jnape.palatable.lambda.semigroup.builtin; +import com.jnape.palatable.lambda.adt.Maybe; +import com.jnape.palatable.lambda.adt.Unit; +import com.jnape.palatable.lambda.functions.builtin.fn1.Constantly; import com.jnape.palatable.lambda.semigroup.Semigroup; import org.junit.Test; +import java.util.Arrays; + 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.functions.builtin.fn1.Repeat.repeat; import static com.jnape.palatable.lambda.semigroup.builtin.Absent.absent; import static org.junit.Assert.assertEquals; @@ -12,12 +19,84 @@ public class AbsentTest { @Test public void semigroup() { + Semigroup addition = Integer::sum; + + assertEquals(just(3), absent(addition, just(1), just(2))); + assertEquals(nothing(), absent(addition, nothing(), just(1))); + assertEquals(nothing(), absent(addition, just(1), nothing())); + assertEquals(nothing(), absent(addition, nothing(), nothing())); + } + + @Test + public void foldRight() { + Absent absent = absent(); + Semigroup addition = Integer::sum; + + assertEquals(just(3), absent.apply(addition).foldRight(just(0), Arrays.asList(just(1), just(2))).value()); + assertEquals(nothing(), absent.apply(addition).foldRight(just(0), Arrays.asList(nothing(), just(1))).value()); + assertEquals(nothing(), absent.apply(addition).foldRight(just(0), Arrays.asList(just(1), nothing())).value()); + assertEquals(nothing(), absent.apply(addition).foldRight(just(0), Arrays.asList(nothing(), nothing())).value()); + } + + @Test + public void foldLeft() { Absent absent = absent(); Semigroup addition = Integer::sum; - assertEquals(just(3), absent.apply(addition, just(1), just(2))); - assertEquals(nothing(), absent.apply(addition, nothing(), just(1))); - assertEquals(nothing(), absent.apply(addition, just(1), nothing())); - assertEquals(nothing(), absent.apply(addition, nothing(), nothing())); + assertEquals(just(3), absent.apply(addition).foldLeft(just(0), Arrays.asList(just(1), just(2)))); + assertEquals(nothing(), absent.apply(addition).foldLeft(just(0), Arrays.asList(nothing(), just(1)))); + assertEquals(nothing(), absent.apply(addition).foldLeft(just(0), Arrays.asList(just(1), nothing()))); + assertEquals(nothing(), absent.apply(addition).foldLeft(just(0), Arrays.asList(nothing(), nothing()))); + } + + @Test(timeout = 200) + public void foldRightShortCircuit() { + Maybe result = Absent.absent(Constantly::constantly) + .foldRight(just(UNIT), repeat(nothing())).value(); + assertEquals(nothing(), result); + + result = Absent.absent(Constantly::constantly) + .foldRight(nothing(), repeat(just(UNIT))).value(); + assertEquals(nothing(), result); + } + + @Test(timeout = 200) + public void foldLeftShortCircuit() { + Maybe result = Absent.absent(Constantly::constantly) + .foldLeft(just(UNIT), repeat(nothing())); + assertEquals(nothing(), result); + + result = Absent.absent(Constantly::constantly) + .foldLeft(nothing(), repeat(just(UNIT))); + assertEquals(nothing(), result); + } + + @Test + public void foldLeftWorksForJusts() { + Maybe result = Absent.absent(Constantly::constantly) + .foldLeft(just(UNIT), Arrays.asList(just(UNIT), just(UNIT))); + assertEquals(just(UNIT), result); + } + + @Test(timeout = 200) + public void checkedApplyFoldRightShortCircuit() { + Maybe result = Absent.absent().checkedApply(Constantly::constantly) + .foldRight(just(UNIT), repeat(nothing())).value(); + assertEquals(nothing(), result); + + result = Absent.absent().checkedApply(Constantly::constantly) + .foldRight(nothing(), repeat(just(UNIT))).value(); + assertEquals(nothing(), result); + } + + @Test(timeout = 200) + public void checkedApplyFoldLeftShortCircuit() { + Maybe result = Absent.absent().checkedApply(Constantly::constantly) + .foldLeft(just(UNIT), repeat(nothing())); + assertEquals(nothing(), result); + + result = Absent.absent().checkedApply(Constantly::constantly) + .foldLeft(nothing(), repeat(just(UNIT))); + assertEquals(nothing(), result); } } \ 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/functions/ExplainFold.java b/src/test/java/testsupport/functions/ExplainFold.java index d59a2378b..ba5c29527 100644 --- a/src/test/java/testsupport/functions/ExplainFold.java +++ b/src/test/java/testsupport/functions/ExplainFold.java @@ -7,6 +7,6 @@ public class ExplainFold { public static Fn2 explainFold() { - return (acc, x) -> format("(%s + %s)", acc, x); + return (x, y) -> format("(%s + %s)", x, y); } } diff --git a/src/test/java/testsupport/matchers/EitherMatcher.java b/src/test/java/testsupport/matchers/EitherMatcher.java new file mode 100644 index 000000000..b18f68af6 --- /dev/null +++ b/src/test/java/testsupport/matchers/EitherMatcher.java @@ -0,0 +1,72 @@ +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; +import static org.hamcrest.CoreMatchers.anything; +import static org.hamcrest.CoreMatchers.equalTo; + +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 isLeft() { + return isLeftThat(anything()); + } + + public static EitherMatcher isLeftOf(L l) { + return isLeftThat(equalTo(l)); + } + + public static EitherMatcher isRightThat(Matcher rMatcher) { + return new EitherMatcher<>(right(rMatcher)); + } + + public static EitherMatcher isRight() { + return isRightThat(anything()); + } + + public static EitherMatcher isRightOf(R r) { + return isRightThat(equalTo(r)); + } +} 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