diff --git a/README.md b/README.md index aa144b856..57c889944 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,14 @@ Add the following dependency to your: com.jnape.palatable lambda - 1.2 + 1.3 ``` `build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)): ```gradle - compile group: 'com.jnape.palatable', name: 'lambda', version: '1.2' + compile group: 'com.jnape.palatable', name: 'lambda', version: '1.3' ``` @@ -114,7 +114,7 @@ Let's compose two functions: Fn1 noOp = add.then(subtract); // same as - Fn1 alsoNoOp = subtract.fmap(add); + Fn1 alsoNoOp = subtract.compose(subtract); ``` And partially apply some: diff --git a/pom.xml b/pom.xml index b192e8e58..491ad2e47 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ lambda - 1.4 + 1.5 jar Lambda @@ -56,6 +56,7 @@ 3.1 1.0 3.3 + 1.3 @@ -63,6 +64,12 @@ junit junit + + org.hamcrest + hamcrest-all + ${hamcrest-all.version} + test + org.mockito mockito-all diff --git a/src/main/java/com/jnape/palatable/lambda/adt/Either.java b/src/main/java/com/jnape/palatable/lambda/adt/Either.java index 4fdd106fd..a898e9bf1 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/Either.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/Either.java @@ -12,6 +12,8 @@ import java.util.function.Supplier; import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static com.jnape.palatable.lambda.functions.builtin.fn3.FoldLeft.foldLeft; +import static java.util.Arrays.asList; /** * The binary tagged union. General semantics tend to connote "success" values via the right value and "failure" values @@ -120,22 +122,24 @@ public final Either flatMap(FunctionEither<L, R>s into a single + * Given two binary operators over L and R, merge multiple Either<L, R>s into a single * Either<L, R>. Note that merge biases towards left values; that is, if any left * value exists, the result will be a left value, such that only unanimous right values result in an ultimate right * value. * * @param leftFn the binary operator for L * @param rightFn the binary operator for R - * @param other the other Either + * @param others the other Eithers to merge into this one * @return the merged Either */ + @SafeVarargs public final Either merge(BiFunction leftFn, BiFunction rightFn, - Either other) { - return this.match( - l1 -> other.match(l2 -> left(leftFn.apply(l1, l2)), r -> left(l1)), - r1 -> other.match(Either::left, r2 -> right(rightFn.apply(r1, r2)))); + Either... others) { + return foldLeft((x, y) -> x.match(l1 -> y.>match(l2 -> left(leftFn.apply(l1, l2)), r -> left(l1)), + r1 -> y.>match(Either::left, r2 -> right(rightFn.apply(r1, r2)))), + this, + asList(others)); } /** @@ -216,6 +220,10 @@ public static Either fromOptional(Optional optional, Supplier .orElse(left(leftFn.get())); } + public Optional toOptional() { + return match(__ -> Optional.empty(), Optional::ofNullable); + } + /** * Attempt to execute the {@link CheckedSupplier}, returning its result in a right value. If the supplier throws an * exception, apply leftFn to it, wrap it in a left value and return it. 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 aaebee9c7..3c3e792d4 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 @@ -134,6 +134,12 @@ public static <_1, _2, _3, _4, _5> Tuple5<_1, _2, _3, _4, _5> tuple(_1 _1, _2 _2 return tuple(_2, _3, _4, _5).cons(_1); } + /** + * The consing of a head element to a tail HList. + * + * @param the head element type + * @param the HList tail type + */ public static class HCons> extends HList { private final Head head; private final Tail tail; @@ -143,10 +149,20 @@ public static class HCons> extends HListHList. + * + * @return the head element + */ public Head head() { return head; } + /** + * The remaining tail of the HList; returns an HNil if this is the last element. + * + * @return the tail + */ public Tail tail() { return tail; } @@ -180,6 +196,9 @@ public final String toString() { } } + /** + * The empty HList. + */ public static final class HNil extends HList { private static final HNil INSTANCE = new HNil(); diff --git a/src/main/java/com/jnape/palatable/lambda/adt/hmap/HMap.java b/src/main/java/com/jnape/palatable/lambda/adt/hmap/HMap.java index ba15852e3..f9073da12 100644 --- a/src/main/java/com/jnape/palatable/lambda/adt/hmap/HMap.java +++ b/src/main/java/com/jnape/palatable/lambda/adt/hmap/HMap.java @@ -27,7 +27,7 @@ public class HMap implements Iterable> { private final Map table; - HMap(Map table) { + private HMap(Map table) { this.table = table; } @@ -70,7 +70,7 @@ public HMap put(TypeSafeKey key, V value) { /** * Store all the key/value mappings in hMap in this HMap. * - * @param hMap the other hMap + * @param hMap the other HMap * @return the updated HMap */ public HMap putAll(HMap hMap) { @@ -87,6 +87,26 @@ public boolean containsKey(TypeSafeKey key) { return table.containsKey(key); } + /** + * Remove a mapping from this HMap. + * + * @param key the key + * @return the updated HMap + */ + public HMap remove(TypeSafeKey key) { + return alter(t -> t.remove(key)); + } + + /** + * Remove all the key/value mappings in hMap from this HMap. + * + * @param hMap the other HMap + * @return the updated HMap + */ + public HMap removeAll(HMap hMap) { + return alter(t -> t.keySet().removeAll(hMap.table.keySet())); + } + /** * Retrieve all the mapped keys. * diff --git a/src/main/java/com/jnape/palatable/lambda/functor/builtin/Const.java b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Const.java new file mode 100644 index 000000000..e19261415 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Const.java @@ -0,0 +1,88 @@ +package com.jnape.palatable.lambda.functor.builtin; + +import com.jnape.palatable.lambda.functor.Bifunctor; +import com.jnape.palatable.lambda.functor.Functor; + +import java.util.function.Function; + +/** + * A (surprisingly useful) functor over some phantom type B, retaining a value of type A that + * can be retrieved later. This is useful in situations where it is desirable to retain constant information throughout + * arbitrary functor transformations, such that at the end of the chain, regardless of how B has been + * altered, A is still pristine and retrievable. + * + * @param the left parameter type, and the type of the stored value + * @param the right (phantom) parameter type + */ +public final class Const implements Functor, Bifunctor { + + private final A a; + + public Const(A a) { + this.a = a; + } + + /** + * Retrieve the stored value. + * + * @return the value + */ + public A runConst() { + return a; + } + + /** + * Map over the right parameter. Note that because B is never actually known quantity outside of a type + * signature, this is effectively a no-op that serves only to alter Const's type signature. + * + * @param fn the mapping function + * @param the new right parameter type + * @return a Const over A (the same value) and C (the new phantom parameter) + */ + @Override + @SuppressWarnings("unchecked") + public Const fmap(Function fn) { + return (Const) this; + } + + /** + * Covariantly map over the left parameter type (the value). + * + * @param fn the mapping function + * @param the new left parameter type (the value) + * @return a Const over Z (the new value) and B (the same phantom parameter) + */ + @Override + @SuppressWarnings("unchecked") + public Const biMapL(Function fn) { + return (Const) Bifunctor.super.biMapL(fn); + } + + /** + * Covariantly map over the right parameter (phantom) type. + * + * @param fn the mapping function + * @param the new right parameter (phantom) type + * @return a Const over A (the same value) and C (the new phantom parameter) + */ + @Override + @SuppressWarnings("unchecked") + public Const biMapR(Function fn) { + return (Const) Bifunctor.super.biMapR(fn); + } + + /** + * Bifunctor's biMap, specialized for Const. + * + * @param lFn the left parameter mapping function + * @param rFn the right parameter mapping function + * @param the new left parameter type + * @param the new right parameter type + * @return a Const over C (the new value) and D (the new phantom parameter) + */ + @Override + public Const biMap(Function lFn, + Function rFn) { + return new Const<>(lFn.apply(a)); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/functor/builtin/Identity.java b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Identity.java new file mode 100644 index 000000000..04160b05f --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/functor/builtin/Identity.java @@ -0,0 +1,40 @@ +package com.jnape.palatable.lambda.functor.builtin; + +import com.jnape.palatable.lambda.functor.Functor; + +import java.util.function.Function; + +/** + * A functor over some value of type A that can be mapped over and retrieved later. + * + * @param the value type + */ +public final class Identity implements Functor { + + private final A a; + + public Identity(A a) { + this.a = a; + } + + /** + * Retrieve the value. + * + * @return the value + */ + public A runIdentity() { + return a; + } + + /** + * Covariantly map over the value. + * + * @param fn the mapping function + * @param the new value type + * @return an Identity over B (the new value) + */ + @Override + public Identity fmap(Function fn) { + return new Identity<>(fn.apply(a)); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/Lens.java b/src/main/java/com/jnape/palatable/lambda/lens/Lens.java new file mode 100644 index 000000000..6717baefc --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/Lens.java @@ -0,0 +1,273 @@ +package com.jnape.palatable.lambda.lens; + +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functor.Functor; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static com.jnape.palatable.lambda.functions.builtin.fn1.Id.id; +import static com.jnape.palatable.lambda.lens.functions.Over.over; +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; + +/** + * An approximation of van Laarhoven lenses. + *

+ * A "lens" can be considered in its simplest form as the conjugation of a "getter" and a "setter"; that is, a + * unification type representing the way to retrieve a "smaller" value A from a "larger" value + * S, as well as a way to update a "smaller" value B of a "larger" value S, + * producing another "larger" value T. + *

+ * Consider the following example: + *

+ * {@code
+ * public final class Person {
+ *     private final int age;
+ *
+ *     public Person(int age) {
+ *         this.age = age;
+ *     }
+ *
+ *     public int getAge() {
+ *         return age;
+ *     }
+ *
+ *     public Person setAge(int age) {
+ *         return new Person(age);
+ *     }
+ * }
+ * }
+ * 
+ * A lens that focused on the age field of an instance of Person might look like this: + *
+ * {@code
+ * Lens ageLens = Lens.lens(Person::getAge, Person::setAge);
+ *
+ * Person adult = new Person(18);
+ * Integer age = view(ageLens, adult); // 18
+ *
+ * Person olderAdult = set(ageLens, 19, adult);
+ * Integer olderAge = view(ageLens, olderAdult); // 19
+ * }
+ * 
+ * The pattern of a getter and setter that mutually agree on both A and B as well as on both + * S and T is so common that this can be given a simplified type signature: + *
+ * {@code
+ * Lens.Simple ageLens = Lens.simpleLens(Person::getAge, Person::setAge);
+ *
+ * Person adult = new Person(18);
+ * Integer age = view(ageLens, adult); // 18
+ *
+ * Person olderAdult = set(ageLens, 19, adult);
+ * Integer olderAge = view(ageLens, olderAdult); // 19
+ * }
+ * 
+ * However, consider if age could be updated on a Person by being provided a date of birth, in + * the form of a LocalDate: + *
+ * {@code
+ * public final class Person {
+ *     private final int age;
+ *
+ *     public Person(int age) {
+ *         this.age = age;
+ *     }
+ *
+ *     public int getAge() {
+ *         return age;
+ *     }
+ *
+ *     public Person setAge(int age) {
+ *         return new Person(age);
+ *     }
+ *
+ *     public Person setAge(LocalDate dob) {
+ *         return setAge((int) YEARS.between(dob, LocalDate.now()));
+ *     }
+ * }
+ * }
+ * 
+ * This is why Lens has both an A and a B: A is the value for "getting", and + * B is the potentially different value for "setting". This distinction makes lenses powerful enough to + * express the more complicated setAge case naturally: + *
+ * {@code
+ * Lens ageDobLens = Lens.lens(Person::getAge, Person::setAge);
+ *
+ * Person adult = new Person(18);
+ * Integer age = view(ageDobLens, adult); // 18
+ *
+ * Person olderAdult = set(ageDobLens, LocalDate.of(1997, 1, 1), adult);
+ * Integer olderAge = view(ageDobLens, olderAdult); // 19 at the time of this writing...anyone else feel old?
+ * }
+ * 
+ * Additionally, we might imagine a lens that produces a different "larger" value on updating than what was given. + * Consider a lens that reads the first string from a list, but produces a Set of strings on update: + *
+ * {@code
+ * Lens, Set, String, String> lens = Lens.lens(
+ *         l -> l.get(0),
+ *         (l, s) -> {
+ *             List copy = new ArrayList<>(l);
+ *             copy.set(0, s);
+ *             return new HashSet<>(copy);
+ *         });
+ *
+ * String firstElement = view(lens, asList("foo", "bar")); // "foo
+ * System.out.println(firstElement);
+ *
+ * set(lens, "oof", asList("foo", "bar")); // ["bar", "oof"]
+ * set(lens, "bar", asList("foo", "bar")); // ["bar"]
+ * }
+ * 
+ * For more information, learn + * about + * lenses. + * + * @param The type of the "larger" value for reading + * @param The type of the "larger" value for putting + * @param The type of the "smaller" value that is read + * @param The type of the "smaller" update value + */ +@FunctionalInterface +public interface Lens extends Functor { + + , FB extends Functor> FT apply(Function fn, S s); + + /** + * Fix this lens against some functor, producing a non-polymorphic runnable lens as an {@link Fn2}. + *

+ * Although the Java type system does not allow enforceability, the functor instance FT should be the same as FB, + * only differentiating in their parameters. + * + * @param The type of the lifted T + * @param The type of the lifted B + * @return the lens, "fixed" to the functor + */ + default , FB extends Functor> Fixed fix() { + return this::apply; + } + + @Override + default Lens fmap(Function fn) { + return this.compose(Lens.lens(id(), (s, t) -> fn.apply(t))); + } + + /** + * Left-to-right composition of lenses. Requires compatibility between S and T. + * + * @param f the other lens + * @param the new "smaller" value to read (previously A) + * @param the new "smaller" update value (previously B) + * @return the composed lens + */ + default Lens andThen(Lens f) { + return f.compose(this); + } + + /** + * Right-to-left composition of lenses. Requires compatibility between A and B. + * + * @param g the other lens + * @param the new "larger" value for reading (previously S) + * @param the new "larger" value for putting (previously T) + * @return the composed lens + */ + default Lens compose(Lens g) { + return lens(view(g).fmap(view(this)), (q, b) -> over(g, set(this, b), q)); + } + + /** + * Static factory method for creating a lens from a getter function and a setter function. + * + * @param getter the getter function + * @param setter the setter function + * @param The type of the "larger" value for reading + * @param The type of the "larger" value for putting + * @param The type of the "smaller" value that is read + * @param The type of the "smaller" update value + * @return the lens + */ + static Lens lens(Function getter, + BiFunction setter) { + return new Lens() { + @Override + @SuppressWarnings("unchecked") + public , FB extends Functor> FT apply(Function fn, + S s) { + return (FT) fn.apply(getter.apply(s)).fmap(b -> setter.apply(s, b)); + } + }; + } + + /** + * Static factory method for creating a simple lens from a getter function and a setter function. + * + * @param getter the getter function + * @param setter the setter function + * @param The type of both "larger" values + * @param The type of both "smaller" values + * @return the lens + */ + @SuppressWarnings("unchecked") + static Lens.Simple simpleLens(Function getter, + BiFunction setter) { + return lens(getter, setter)::apply; + } + + /** + * A convenience type with a simplified type signature for common lenses with both unified "larger" values and + * unified "smaller" values. + * + * @param The type of both "larger" values + * @param The type of both "smaller" values + */ + @FunctionalInterface + interface Simple extends Lens { + + @Override + default , FA extends Functor> Fixed fix() { + return this::apply; + } + + @SuppressWarnings("unchecked") + default Lens.Simple compose(Lens.Simple g) { + return Lens.super.compose(g)::apply; + } + + default Lens.Simple andThen(Lens.Simple f) { + return f.compose(this); + } + + /** + * A convenience type with a simplified type signature for fixed simple lenses. + * + * @param The type of both "larger" values + * @param The type of both "smaller" values + * @param The type of the lifted s + * @param The type of the lifted A + */ + @FunctionalInterface + interface Fixed, FA extends Functor> + extends Lens.Fixed { + } + } + + /** + * A lens that has been fixed to a functor. Because the lens is no longer polymorphic, it can additionally be safely + * represented as an Fn2. + * + * @param The type of the "larger" value for reading + * @param The type of the "larger" value for putting + * @param The type of the "smaller" value that is read + * @param The type of the "smaller" update value + * @param The type of the lifted T + * @param The type of the lifted B + */ + @FunctionalInterface + interface Fixed, FB extends Functor> + extends Fn2, S, FT> { + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/functions/Over.java b/src/main/java/com/jnape/palatable/lambda/lens/functions/Over.java new file mode 100644 index 000000000..782f3d5c7 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/functions/Over.java @@ -0,0 +1,55 @@ +package com.jnape.palatable.lambda.lens.functions; + +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.functor.builtin.Identity; +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.function.Function; + +/** + * Given a lens, a function from A to B, and a "larger" value S, produce a + * T by retrieving the A from the S, applying the function, and updating the + * S with the B resulting from the function. + *

+ * This function is similar to {@link Set}, except that it allows the setting value B to be derived from + * S via function application, rather than provided. + * + * @param the type of the larger value + * @param the type of the larger updated value + * @param the type of the smaller retrieving value + * @param the type of the smaller setting value + * @see Set + * @see View + */ +public final class Over implements Fn3, Function, S, T> { + + private Over() { + } + + @Override + public T apply(Lens lens, Function fn, S s) { + return lens., Identity>fix() + .apply(fn.andThen((Function>) Identity::new), s) + .runIdentity(); + } + + public static Over over() { + return new Over<>(); + } + + public static Fn2, S, T> over( + Lens lens) { + return Over.over().apply(lens); + } + + public static Fn1 over(Lens lens, + Function fn) { + return over(lens).apply(fn); + } + + public static T over(Lens lens, Function fn, S s) { + return over(lens, fn).apply(s); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/functions/Set.java b/src/main/java/com/jnape/palatable/lambda/lens/functions/Set.java new file mode 100644 index 000000000..aed093e8f --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/functions/Set.java @@ -0,0 +1,51 @@ +package com.jnape.palatable.lambda.lens.functions; + +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.functor.builtin.Identity; +import com.jnape.palatable.lambda.lens.Lens; + +import static com.jnape.palatable.lambda.functions.builtin.fn1.Constantly.constantly; +import static com.jnape.palatable.lambda.lens.functions.Over.over; + +/** + * Given a lens, a "smaller" value B, and a "larger" value S, produce a T by + * lifting the lens into {@link Identity}. + *

+ * More idiomatically, this function can be used to treat a lens as a "setter" of Bs on Ss, + * potentially producing a different "larger" value, T. + * + * @param the type of the larger value + * @param the type of the larger updated value + * @param the type of the smaller retrieving value (unused, but necessary for composition) + * @param the type of the smaller setting value + * @see Over + * @see View + */ +public final class Set implements Fn3, B, S, T> { + + private Set() { + } + + @Override + public T apply(Lens lens, B b, S s) { + return over(lens, constantly(b), s); + } + + public static Set set() { + return new Set<>(); + } + + public static Fn2 set(Lens lens) { + return Set.set().apply(lens); + } + + public static Fn1 set(Lens lens, B b) { + return set(lens).apply(b); + } + + public static T set(Lens lens, B b, S s) { + return set(lens, b).apply(s); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/functions/View.java b/src/main/java/com/jnape/palatable/lambda/lens/functions/View.java new file mode 100644 index 000000000..966debeff --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/functions/View.java @@ -0,0 +1,42 @@ +package com.jnape.palatable.lambda.lens.functions; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functions.Fn2; +import com.jnape.palatable.lambda.functor.builtin.Const; +import com.jnape.palatable.lambda.lens.Lens; + +/** + * Given a lens and a "larger" value S, retrieve a "smaller" value A by lifting the lens into + * {@link Const}. + *

+ * More idiomatically, this function can be used to treat a lens as a "getter" of As from Ss. + * + * @param the type of the larger value + * @param the type of the larger updated value (unused, but necessary for composition) + * @param the type of the smaller retrieving value + * @param the type of the smaller setting value (unused, but necessary for composition) + * @see Set + * @see Over + */ +public final class View implements Fn2, S, A> { + + private View() { + } + + @Override + public A apply(Lens lens, S s) { + return lens., Const>fix().apply(Const::new, s).runConst(); + } + + public static View view() { + return new View<>(); + } + + public static Fn1 view(Lens lens) { + return View.view().apply(lens); + } + + public static A view(Lens lens, S s) { + return view(lens).apply(s); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/CollectionLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/CollectionLens.java new file mode 100644 index 000000000..5bcf16315 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/CollectionLens.java @@ -0,0 +1,63 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import static com.jnape.palatable.lambda.lens.Lens.simpleLens; + +/** + * Lenses that operate on {@link Collection}s. + */ +public final class CollectionLens { + + private CollectionLens() { + } + + /** + * Convenience static factory method for creating a lens that focuses on a copy of a Collection, given + * a function that creates the copy. Useful for composition to avoid mutating a Collection reference. + * + * @param copyFn the copying function + * @param the collection element type + * @param the type of the collection + * @return a lens that focuses on a copy of CX + */ + public static > Lens.Simple asCopy(Function copyFn) { + return simpleLens(copyFn, (__, copy) -> copy); + } + + /** + * Convenience static factory method for creating a lens that focuses on an arbitrary {@link Collection} as a + * {@link Set}. + * + * @param the collection element type + * @param the type of the collection + * @return a lens that focuses on a Collection as a Set + */ + public static > Lens.Simple> asSet() { + return simpleLens(HashSet::new, (xsL, xsS) -> { + xsL.retainAll(xsS); + return xsL; + }); + } + + /** + * Convenience static factory method for creating a lens that focuses on a Collection as a Stream. + * + * @param the collection element type + * @param the type of the collection + * @return a lens that focuses on a Collection as a stream. + */ + public static > Lens.Simple> asStream() { + return simpleLens(Collection::stream, (xsL, xsS) -> { + xsL.clear(); + xsS.forEach(xsL::add); + return xsL; + }); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/EitherLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/EitherLens.java new file mode 100644 index 000000000..8652259ec --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/EitherLens.java @@ -0,0 +1,44 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.adt.Either; +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.Optional; + +import static com.jnape.palatable.lambda.lens.Lens.simpleLens; + +/** + * Lenses that operate on {@link Either}s. + */ +public final class EitherLens { + + private EitherLens() { + } + + /** + * Convenience static factory method for creating a lens over right values, wrapping them in an {@link Optional}. + * When setting, an empty Optional value means to leave the either unaltered, where as a present Optional value + * replaces the either with a right over the wrapped Optional value. + * + * @param the left parameter type + * @param the right parameter type + * @return a lens that focuses on right values + */ + public static Lens.Simple, Optional> right() { + return simpleLens(Either::toOptional, (lOrR, optR) -> optR.>map(Either::right).orElse(lOrR)); + } + + /** + * Convenience static factory method for creating a lens over left values, wrapping them in an {@link Optional}. + * When setting, an empty Optional value means to leave the either unaltered, where as a present Optional value + * replaces the either with a left over the wrapped Optional value. + * + * @param the left parameter type + * @param the right parameter type + * @return a lens that focuses on left values + */ + public static Lens.Simple, Optional> left() { + return simpleLens(e -> e.match(Optional::ofNullable, __ -> Optional.empty()), + (lOrR, optL) -> optL.>map(Either::left).orElse(lOrR)); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/ListLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/ListLens.java new file mode 100644 index 000000000..49982aad7 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/ListLens.java @@ -0,0 +1,47 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.Lens.simpleLens; + +/** + * Lenses that operate on {@link List}s. + */ +public final class ListLens { + + private ListLens() { + } + + /** + * Convenience static factory method for creating a lens over a copy of a list. Useful for composition to avoid + * mutating a list reference. + * + * @param the list element type + * @return a lens that focuses on copies of lists + */ + public static Lens.Simple, List> asCopy() { + return simpleLens(ArrayList::new, (xs, ys) -> ys); + } + + /** + * Convenience static factory method for creating a lens that focuses on an element in a list at a particular index. + * Wraps result in an Optional to handle null values or indexes that fall outside of list boundaries. + * + * @param index the index to focus on + * @param the list element type + * @return an Optional wrapping the element at the index + */ + public static Lens, List, Optional, X> at(int index) { + return lens(xs -> Optional.ofNullable(xs.size() > index ? xs.get(index) : null), + (xs, x) -> { + if (xs.size() > index) + xs.set(index, x); + return xs; + }); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java new file mode 100644 index 000000000..0f3e91370 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java @@ -0,0 +1,109 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.Lens.simpleLens; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.util.stream.Collectors.toSet; + +/** + * Lenses that operate on {@link Map}s. + */ +public final class MapLens { + + private MapLens() { + } + + /** + * Convenience static factory method for creating a lens that focuses on a copy of a Map. Useful for composition to + * avoid mutating a map reference. + * + * @param the key type + * @param the value type + * @return a lens that focuses on copies of maps + */ + public static Lens.Simple, Map> asCopy() { + return simpleLens(HashMap::new, (__, copy) -> copy); + } + + /** + * Convenience static factory method for creating a lens that focuses on a value at a key in a map, as an {@link + * Optional}. + * + * @param k the key to focus on + * @param the key type + * @param the value type + * @return a lens that focuses on the value at key, as an {@link Optional} + */ + public static Lens, Map, Optional, V> atKey(K k) { + return lens(m -> Optional.ofNullable(m.get(k)), (m, v) -> { + m.put(k, v); + return m; + }); + } + + /** + * Convenience static factory method for creating a lens that focuses on the keys of a map. + * + * @param the key type + * @param the value type + * @return a lens that focuses on the keys of a map + */ + public static Lens.Simple, Set> keys() { + return simpleLens(Map::keySet, (m, ks) -> { + Set keys = m.keySet(); + keys.retainAll(ks); + ks.removeAll(keys); + ks.forEach(k -> m.put(k, null)); + return m; + }); + } + + /** + * Convenience static factory method for creating a lens that focuses on the values of a map. In the case of + * updating the map, only the entries with a value listed in the update collection of values are kept. + * + * @param the key type + * @param the value type + * @return a lens that focuses on the values of a map + */ + public static Lens, Map, Collection, Collection> values() { + return lens(Map::values, (m, vs) -> { + Set valueSet = new HashSet<>(vs); + Set matchingKeys = m.entrySet().stream() + .filter(kv -> valueSet.contains(kv.getValue())) + .map(Map.Entry::getKey) + .collect(toSet()); + m.keySet().retainAll(matchingKeys); + return m; + }); + } + + /** + * Convenience static factory method for creating a lens that focuses on the inverse of a map (keys and values + * swapped). In the case of multiple equal values becoming keys, the last one wins. + * + * @param the key type + * @param the value type + * @return a lens that focuses on the inverse of a map + */ + public static Lens.Simple, Map> inverted() { + return simpleLens(m -> { + Map inverted = new HashMap<>(); + m.entrySet().forEach(entry -> inverted.put(entry.getValue(), entry.getKey())); + return inverted; + }, (m, im) -> { + m.clear(); + m.putAll(view(inverted(), im)); + return m; + }); + } +} diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/OptionalLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/OptionalLens.java new file mode 100644 index 000000000..8737a8cf2 --- /dev/null +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/OptionalLens.java @@ -0,0 +1,26 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; + +import java.util.Optional; + +import static com.jnape.palatable.lambda.lens.Lens.simpleLens; + +/** + * Lenses that operate on {@link Optional}s. + */ +public final class OptionalLens { + + private OptionalLens() { + } + + /** + * Convenience static factory method for creating a lens that focuses on a value as an {@link Optional}. + * + * @param the value type + * @return a lens that focuses on the value as an Optional + */ + public static Lens.Simple> asOptional() { + return simpleLens(Optional::ofNullable, (v, optV) -> optV.orElse(v)); + } +} diff --git a/src/test/java/com/jnape/palatable/lambda/adt/EitherTest.java b/src/test/java/com/jnape/palatable/lambda/adt/EitherTest.java index 634706372..4e2911dc7 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/EitherTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/EitherTest.java @@ -116,13 +116,20 @@ public void matchDuallyLiftsAndFlattens() { assertThat(right.match(l -> l + "bar", r -> r + 1), is(2)); } + @Test + public void toOptionalMapsEitherToOptional() { + assertEquals(Optional.of(1), Either.right(1).toOptional()); + assertEquals(Optional.empty(), Either.right(null).toOptional()); + assertEquals(Optional.empty(), Either.left("fail").toOptional()); + } + @Test public void fromOptionalMapsOptionalToEither() { - Optional present = Optional.of("foo"); - Optional absent = Optional.empty(); + Optional present = Optional.of(1); + Optional absent = Optional.empty(); - assertThat(fromOptional(present, () -> -1), is(right("foo"))); - assertThat(fromOptional(absent, () -> -1), is(left(-1))); + assertThat(fromOptional(present, () -> "fail"), is(right(1))); + assertThat(fromOptional(absent, () -> "fail"), is(left("fail"))); } @Test diff --git a/src/test/java/com/jnape/palatable/lambda/adt/hmap/HMapTest.java b/src/test/java/com/jnape/palatable/lambda/adt/hmap/HMapTest.java index c89861eea..2edcb1d17 100644 --- a/src/test/java/com/jnape/palatable/lambda/adt/hmap/HMapTest.java +++ b/src/test/java/com/jnape/palatable/lambda/adt/hmap/HMapTest.java @@ -10,8 +10,6 @@ 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 java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -26,29 +24,30 @@ public class HMapTest { public void getForPresentKey() { TypeSafeKey stringKey = typeSafeKey(); assertEquals(Optional.of("string value"), - new HMap(singletonMap(stringKey, "string value")).get(stringKey)); + singletonHMap(stringKey, "string value").get(stringKey)); } @Test public void getForAbsentKey() { assertEquals(Optional.empty(), - new HMap(singletonMap(typeSafeKey(), "string value")).get(typeSafeKey())); + singletonHMap(typeSafeKey(), "string value") + .get(typeSafeKey())); } @Test public void getForPresentKeyWithNullValue() { TypeSafeKey stringKey = typeSafeKey(); assertEquals(Optional.empty(), - new HMap(singletonMap(stringKey, null)).get(stringKey)); + singletonHMap(stringKey, null).get(stringKey)); } @Test public void put() { TypeSafeKey stringKey = typeSafeKey(); - assertEquals(new HMap(singletonMap(stringKey, "string value")), + assertEquals(singletonHMap(stringKey, "string value"), emptyHMap().put(stringKey, "string value")); - assertEquals(new HMap(singletonMap(stringKey, "new value")), + assertEquals(singletonHMap(stringKey, "new value"), emptyHMap() .put(stringKey, "string value") .put(stringKey, "new value")); @@ -75,6 +74,35 @@ public void putAll() { right.putAll(left)); } + @Test + public void remove() { + TypeSafeKey stringKey1 = typeSafeKey(); + TypeSafeKey stringKey2 = typeSafeKey(); + assertEquals(emptyHMap(), + emptyHMap() + .put(stringKey1, "string value") + .remove(stringKey1)); + + assertEquals(singletonHMap(stringKey2, "another string value"), + emptyHMap() + .put(stringKey1, "string value") + .put(stringKey2, "another string value") + .remove(stringKey1)); + } + + @Test + public void removeAll() { + TypeSafeKey stringKey1 = typeSafeKey(); + TypeSafeKey stringKey2 = typeSafeKey(); + + HMap hMap1 = hMap(stringKey1, "foo", + stringKey2, "bar"); + HMap hMap2 = singletonHMap(stringKey1, "foo"); + + assertEquals(singletonHMap(stringKey2, "bar"), + hMap1.removeAll(hMap2)); + } + @Test public void containsKey() { TypeSafeKey stringKey1 = typeSafeKey(); @@ -127,8 +155,8 @@ public void convenienceStaticFactoryMethods() { TypeSafeKey stringKey = typeSafeKey(); TypeSafeKey intKey = typeSafeKey(); TypeSafeKey floatKey = typeSafeKey(); - assertEquals(new HMap(emptyMap()), HMap.emptyHMap()); - assertEquals(new HMap(singletonMap(stringKey, "string value")), HMap.singletonHMap(stringKey, "string value")); + assertEquals(emptyHMap().put(stringKey, "string value"), + singletonHMap(stringKey, "string value")); assertEquals(emptyHMap().put(stringKey, "string value").put(intKey, 1), hMap(stringKey, "string value", intKey, 1)); @@ -157,13 +185,12 @@ public void equality() { public void hashCodeUsesDecentDistribution() { assertEquals(emptyHMap().hashCode(), emptyHMap().hashCode()); TypeSafeKey stringKey = typeSafeKey(); - assertEquals(new HMap(singletonMap(stringKey, "string value")).hashCode(), - new HMap(singletonMap(stringKey, "string value")).hashCode()); - - assertNotEquals(emptyHMap(), new HMap(singletonMap(stringKey, "string value"))); - assertNotEquals(new HMap(singletonMap(stringKey, "string value")), - new HMap(singletonMap(stringKey, "another string value"))); + assertEquals(singletonHMap(stringKey, "string value").hashCode(), + singletonHMap(stringKey, "string value").hashCode()); + assertNotEquals(emptyHMap(), singletonHMap(stringKey, "string value")); + assertNotEquals(singletonHMap(stringKey, "string value"), + singletonHMap(stringKey, "another string value")); } @Test 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 06ef3d6cf..2bdfca588 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/Fn1Test.java @@ -1,18 +1,28 @@ package com.jnape.palatable.lambda.functions; +import org.hamcrest.MatcherAssert; import org.junit.Test; -import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; public class Fn1Test { @Test - public void fmapComposesFunctions() { + public void functorProperties() { Fn1 add2 = integer -> integer + 2; Fn1 toString = Object::toString; - assertThat(add2.fmap(toString).apply(2), is(toString.apply(add2.apply(2)))); + MatcherAssert.assertThat(add2.fmap(toString).apply(2), is(toString.apply(add2.apply(2)))); + } + + @Test + public void profunctorProperties() { + Fn1 add2 = integer -> integer + 2; + + assertEquals((Integer) 3, add2.diMapL(Integer::parseInt).apply("1")); + assertEquals("3", add2.diMapR(Object::toString).apply(1)); + assertEquals("3", add2.diMap(Integer::parseInt, Object::toString).apply("1")); } @Test @@ -20,6 +30,6 @@ public void thenIsJustAnAliasForFmap() { Fn1 add2 = integer -> integer + 2; Fn1 toString = Object::toString; - assertThat(add2.then(toString).apply(2), is(toString.apply(add2.apply(2)))); + MatcherAssert.assertThat(add2.then(toString).apply(2), is(toString.apply(add2.apply(2)))); } } diff --git a/src/test/java/com/jnape/palatable/lambda/functions/Fn2Test.java b/src/test/java/com/jnape/palatable/lambda/functions/Fn2Test.java index 49d7e9809..e12b9e3e3 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/Fn2Test.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/Fn2Test.java @@ -1,6 +1,5 @@ package com.jnape.palatable.lambda.functions; -import com.jnape.palatable.lambda.functions.builtin.fn1.Id; import org.junit.Test; import java.util.function.BiFunction; @@ -30,24 +29,6 @@ public void uncurries() { assertThat(CHECK_LENGTH.uncurry().apply(tuple("abc", 3)), is(true)); } - @Test - public void functorProperties() { - assertThat(CHECK_LENGTH.fmap(f -> Id.id()).apply("foo").apply("bar"), is("bar")); - } - - @Test - public void profunctorProperties() { - assertThat(CHECK_LENGTH.diMapL(Object::toString).apply(123).apply(3), is(true)); - assertThat(CHECK_LENGTH.diMapR(fn -> fn.andThen(Object::toString)).apply("123").apply(3), is("true")); - assertThat( - CHECK_LENGTH.>diMap( - Object::toString, - fn -> fn.andThen(Object::toString) - ).apply("123").apply(3), - is("true") - ); - } - @Test public void toBiFunction() { BiFunction biFunction = CHECK_LENGTH.toBiFunction(); diff --git a/src/test/java/com/jnape/palatable/lambda/functions/Fn3Test.java b/src/test/java/com/jnape/palatable/lambda/functions/Fn3Test.java index c35532933..7214064f2 100644 --- a/src/test/java/com/jnape/palatable/lambda/functions/Fn3Test.java +++ b/src/test/java/com/jnape/palatable/lambda/functions/Fn3Test.java @@ -28,20 +28,4 @@ public void flipsFirstAndSecondArgument() { public void uncurries() { assertThat(CHECK_MULTIPLICATION.uncurry().apply(tuple(2, 3), 6), is(true)); } - - @Test - public void functorProperties() { - assertThat(CHECK_MULTIPLICATION.fmap(f -> f.fmap(g -> g.andThen(Object::toString))).apply(2).apply(3).apply(6), is("true")); - } - - @Test - public void profunctorProperties() { - assertThat(CHECK_MULTIPLICATION.diMapL(Integer::parseInt).apply("2").apply(3).apply(6), is(true)); - assertThat(CHECK_MULTIPLICATION.diMapR(f -> f.fmap(g -> g.andThen(Object::toString))).apply(2).apply(3).apply(6), is("true")); - assertThat(CHECK_MULTIPLICATION.diMap((Fn1) Integer::parseInt, - f -> f.fmap(g -> g.andThen(Object::toString))) - .apply("2") - .apply(3) - .apply(6), is("true")); - } } diff --git a/src/test/java/com/jnape/palatable/lambda/functor/builtin/ConstTest.java b/src/test/java/com/jnape/palatable/lambda/functor/builtin/ConstTest.java new file mode 100644 index 000000000..5d8a06abc --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functor/builtin/ConstTest.java @@ -0,0 +1,18 @@ +package com.jnape.palatable.lambda.functor.builtin; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ConstTest { + + @Test + public void functorProperties() { + assertEquals("foo", new Const("foo").fmap(x -> x + 1).runConst()); + } + + @Test + public void bifunctorProperties() { + assertEquals("FOO", new Const("foo").biMap(String::toUpperCase, x -> x + 1).runConst()); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/functor/builtin/IdentityTest.java b/src/test/java/com/jnape/palatable/lambda/functor/builtin/IdentityTest.java new file mode 100644 index 000000000..792980e99 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/functor/builtin/IdentityTest.java @@ -0,0 +1,13 @@ +package com.jnape.palatable.lambda.functor.builtin; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IdentityTest { + + @Test + public void functorProperties() { + assertEquals("FOO", new Identity<>("foo").fmap(String::toUpperCase).runIdentity()); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java new file mode 100644 index 000000000..c9deff678 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java @@ -0,0 +1,67 @@ +package com.jnape.palatable.lambda.lens; + +import com.jnape.palatable.lambda.functions.Fn1; +import com.jnape.palatable.lambda.functor.builtin.Const; +import com.jnape.palatable.lambda.functor.builtin.Identity; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; + +public class LensTest { + + private static final Lens>, Map>, List, Set> EARLIER_LENS = lens(m -> m.get("foo"), (m, s) -> singletonMap("foo", s)); + private static final Lens, Set, String, Integer> LENS = lens(xs -> xs.get(0), (xs, i) -> singleton(i)); + + @Test + public void setsUnderIdentity() { + Set ints = LENS.>, Identity>apply(s -> new Identity<>(s.length()), asList("foo", "bar", "baz")).runIdentity(); + assertEquals(singleton(3), ints); + } + + @Test + public void viewsUnderConst() { + Integer i = LENS.>, Const>apply(s -> new Const<>(s.length()), asList("foo", "bar", "baz")).runConst(); + assertEquals((Integer) 3, i); + } + + @Test + public void fix() { + Fn1> fn = s -> new Const<>(s.length()); + List s = singletonList("foo"); + + Integer fixedLensResult = LENS.>, Const>fix().apply(fn, s).runConst(); + Integer unfixedLensResult = LENS.>, Const>apply(fn, s).runConst(); + + assertEquals(unfixedLensResult, fixedLensResult); + } + + @Test + public void functorProperties() { + assertEquals(false, set(LENS.fmap(Set::isEmpty), 1, singletonList("foo"))); + } + + @Test + public void composition() { + Map> map = singletonMap("foo", asList("one", "two", "three")); + assertEquals("one", view(LENS.compose(EARLIER_LENS), map)); + assertEquals(singletonMap("foo", singleton(1)), set(LENS.compose(EARLIER_LENS), 1, map)); + } + + @Test + public void andThenComposesInReverse() { + Map> map = singletonMap("foo", asList("one", "two", "three")); + assertEquals("one", view(EARLIER_LENS.andThen(LENS), map)); + assertEquals(singletonMap("foo", singleton(1)), set(EARLIER_LENS.andThen(LENS), 1, map)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/functions/OverTest.java b/src/test/java/com/jnape/palatable/lambda/lens/functions/OverTest.java new file mode 100644 index 000000000..e5a95cf4a --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/functions/OverTest.java @@ -0,0 +1,22 @@ +package com.jnape.palatable.lambda.lens.functions; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Test; + +import java.util.List; +import java.util.Set; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.functions.Over.over; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.Assert.assertEquals; + +public class OverTest { + + @Test + public void mapsDataWithLensAndFunction() { + Lens, Set, String, Integer> lens = lens(xs -> xs.get(0), (xs, i) -> singleton(i)); + assertEquals(singleton(1), over(lens, String::length, asList("a", "aa", "aaa"))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/functions/SetTest.java b/src/test/java/com/jnape/palatable/lambda/lens/functions/SetTest.java new file mode 100644 index 000000000..47fa8a959 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/functions/SetTest.java @@ -0,0 +1,21 @@ +package com.jnape.palatable.lambda.lens.functions; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Test; + +import java.util.List; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.Assert.assertEquals; + +public class SetTest { + + @Test + public void updatesWithLensAndNewValue() { + Lens, java.util.Set, String, Integer> lens = lens(xs -> xs.get(0), (xs, i) -> singleton(i)); + assertEquals(singleton(5), set(lens, 5, asList("a", "aa", "aaa"))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/functions/ViewTest.java b/src/test/java/com/jnape/palatable/lambda/lens/functions/ViewTest.java new file mode 100644 index 000000000..201ec990a --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/functions/ViewTest.java @@ -0,0 +1,22 @@ +package com.jnape.palatable.lambda.lens.functions; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Test; + +import java.util.List; +import java.util.Set; + +import static com.jnape.palatable.lambda.lens.Lens.lens; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.Assert.assertEquals; + +public class ViewTest { + + @Test + public void viewsSubPartWithLens() { + Lens, Set, String, Integer> lens = lens(xs -> xs.get(0), (xs, i) -> singleton(i)); + assertEquals("foo", view(lens, asList("foo", "bar", "baz"))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/CollectionLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/CollectionLensTest.java new file mode 100644 index 000000000..2df8dd73a --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/CollectionLensTest.java @@ -0,0 +1,72 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +public class CollectionLensTest { + + private List xs; + + @Before + public void setUp() { + xs = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + } + + @Test + public void asCopyUsesMappingFunctionToFocusOnCollectionThroughCopy() { + Lens.Simple, List> asCopy = CollectionLens.asCopy(ArrayList::new); + + assertEquals(xs, view(asCopy, xs)); + assertNotSame(xs, view(asCopy, xs)); + + List updatedList = asList("foo", "bar"); + assertSame(updatedList, set(asCopy, updatedList, xs)); + } + + @Test + public void asSetFocusesOnCollectionAsSet() { + Lens.Simple, Set> asSet = CollectionLens.asSet(); + + assertEquals(new HashSet<>(xs), view(asSet, xs)); + assertEquals(singleton("foo"), view(asSet, asList("foo", "foo"))); + assertEquals(emptySet(), view(asSet, emptyList())); + + assertEquals(asList("foo", "bar"), set(asSet, new HashSet<>(asList("foo", "bar")), xs)); + assertEquals(asList("foo", "foo", "bar"), + set(asSet, + new HashSet<>(asList("foo", "bar")), + new ArrayList<>(asList("foo", "foo", "bar", "baz")))); + assertEquals(emptyList(), set(asSet, emptySet(), xs)); + assertEquals(emptyList(), set(asSet, singleton("foo"), emptyList())); + } + + @Test + public void asStreamFocusesOnCollectionAsStream() { + Lens.Simple, Stream> asStream = CollectionLens.asStream(); + + assertEquals(xs, view(asStream, xs).collect(toList())); + assertEquals(asList("foo", "bar"), set(asStream, Stream.of("foo", "bar"), xs)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/EitherLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/EitherLensTest.java new file mode 100644 index 000000000..fb518dd9b --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/EitherLensTest.java @@ -0,0 +1,40 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.adt.Either; +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Test; + +import java.util.Optional; + +import static com.jnape.palatable.lambda.adt.Either.left; +import static com.jnape.palatable.lambda.adt.Either.right; +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static org.junit.Assert.assertEquals; + +public class EitherLensTest { + + @Test + public void rightFocusesOnRightValues() { + Lens.Simple, Optional> right = EitherLens.right(); + + assertEquals(Optional.of(1), view(right, right(1))); + assertEquals(Optional.empty(), view(right, left("fail"))); + assertEquals(right(2), set(right, Optional.of(2), right(1))); + assertEquals(right(1), set(right, Optional.empty(), right(1))); + assertEquals(right(2), set(right, Optional.of(2), left("fail"))); + assertEquals(left("fail"), set(right, Optional.empty(), left("fail"))); + } + + @Test + public void leftFocusesOnLeftValues() { + Lens.Simple, Optional> left = EitherLens.left(); + + assertEquals(Optional.of("fail"), view(left, left("fail"))); + assertEquals(Optional.empty(), view(left, right(1))); + assertEquals(left("foo"), set(left, Optional.of("foo"), left("fail"))); + assertEquals(left("fail"), set(left, Optional.empty(), left("fail"))); + assertEquals(left("foo"), set(left, Optional.of("foo"), right(1))); + assertEquals(right(1), set(left, Optional.empty(), right(1))); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/ListLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/ListLensTest.java new file mode 100644 index 000000000..2a71ae703 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/ListLensTest.java @@ -0,0 +1,52 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +public class ListLensTest { + + private List xs; + + @Before + public void setUp() throws Exception { + xs = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + } + + @Test + public void asCopyFocusesOnListThroughCopy() { + Lens.Simple, List> asCopy = ListLens.asCopy(); + + assertEquals(xs, view(asCopy, xs)); + assertNotSame(xs, view(asCopy, xs)); + + List update = asList("foo", "bar", "baz", "quux"); + assertSame(update, set(asCopy, update, xs)); + } + + @Test + public void atFocusesOnElementAtIndex() { + Lens, List, Optional, String> at0 = ListLens.at(0); + + assertEquals(Optional.of("foo"), view(at0, xs)); + assertEquals(Optional.empty(), view(at0, emptyList())); + assertEquals(asList("quux", "bar", "baz"), set(at0, "quux", xs)); + assertEquals(emptyList(), set(at0, "quux", emptyList())); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java new file mode 100644 index 000000000..bd05cf541 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java @@ -0,0 +1,120 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static com.jnape.palatable.lambda.lens.lenses.MapLens.keys; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +public class MapLensTest { + + private Map m; + + @Before + public void setUp() { + m = new HashMap() {{ + put("foo", 1); + put("bar", 2); + put("baz", 3); + }}; + } + + @Test + public void asCopyFocusesOnMapThroughCopy() { + Lens.Simple, Map> asCopy = MapLens.asCopy(); + + assertEquals(m, view(asCopy, m)); + assertNotSame(m, view(asCopy, m)); + + Map update = new HashMap() {{ + put("quux", 0); + }}; + assertSame(update, set(asCopy, update, m)); + } + + @Test + public void atKeyFocusesOnValueAtKey() { + Lens, Map, Optional, Integer> atFoo = MapLens.atKey("foo"); + + assertEquals(Optional.of(1), view(atFoo, m)); + + Map updated = set(atFoo, -1, m); + assertEquals(new HashMap() {{ + put("foo", -1); + put("bar", 2); + put("baz", 3); + }}, updated); + assertSame(m, updated); + } + + @Test + public void keysFocusesOnKeys() { + Lens, Map, Set, Set> keys = keys(); + + assertEquals(m.keySet(), view(keys, m)); + + Map updated = set(keys, new HashSet<>(asList("bar", "baz", "quux")), m); + assertEquals(new HashMap() {{ + put("bar", 2); + put("baz", 3); + put("quux", null); + }}, updated); + assertSame(m, updated); + } + + @Test + public void valuesFocusesOnValues() { + Lens, Map, Collection, Collection> values = MapLens.values(); + + assertEquals(m.values(), view(values, m)); + + Map updated = set(values, asList(1, 2), m); + assertEquals(new HashMap() {{ + put("foo", 1); + put("bar", 2); + }}, updated); + assertSame(m, updated); + } + + @Test + public void invertedFocusesOnMapWithKeysAndValuesSwitched() { + Lens.Simple, Map> inverted = MapLens.inverted(); + + assertEquals(new HashMap() {{ + put(1, "foo"); + put(2, "bar"); + put(3, "baz"); + }}, view(inverted, m)); + + Map updated = set(inverted, new HashMap() {{ + put(2, "bar"); + put(3, "baz"); + }}, m); + assertEquals(new HashMap() {{ + put("bar", 2); + put("baz", 3); + }}, updated); + assertSame(m, updated); + + Map withDuplicateValues = new HashMap() {{ + put("foo", 1); + put("bar", 1); + }}; + assertEquals(new HashMap() {{ + put(1, "foo"); + }}, view(inverted, withDuplicateValues)); + } +} \ No newline at end of file diff --git a/src/test/java/com/jnape/palatable/lambda/lens/lenses/OptionalLensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/lenses/OptionalLensTest.java new file mode 100644 index 000000000..708a57704 --- /dev/null +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/OptionalLensTest.java @@ -0,0 +1,23 @@ +package com.jnape.palatable.lambda.lens.lenses; + +import com.jnape.palatable.lambda.lens.Lens; +import org.junit.Test; + +import java.util.Optional; + +import static com.jnape.palatable.lambda.lens.functions.Set.set; +import static com.jnape.palatable.lambda.lens.functions.View.view; +import static org.junit.Assert.assertEquals; + +public class OptionalLensTest { + + @Test + public void asOptionalWrapsValuesInOptional() { + Lens.Simple> asOptional = OptionalLens.asOptional(); + + assertEquals(Optional.of("foo"), view(asOptional, "foo")); + assertEquals(Optional.empty(), view(asOptional, null)); + assertEquals("bar", set(asOptional, Optional.of("bar"), "foo")); + assertEquals("foo", set(asOptional, Optional.empty(), "foo")); + } +} \ No newline at end of file