diff --git a/README.md b/README.md index 57c889944..60e512918 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ λ ====== [![Build Status](https://travis-ci.org/palatable/lambda.svg)](https://travis-ci.org/palatable/lambda) -[![Lambda](https://img.shields.io/maven-central/v/com.jnape.palatable/lambda.svg?maxAge=2592000)](http://search.maven.org/#search%7Cga%7C1%7Ccom.jnape.palatable.lambda) +[![Lambda](https://img.shields.io/maven-central/v/com.jnape.palatable/lambda.svg)](http://search.maven.org/#search%7Cga%7C1%7Ccom.jnape.palatable.lambda) Functional patterns for Java 8 @@ -15,6 +15,7 @@ Functional patterns for Java 8 - [Tuples](#tuples) - [HMaps](#hmaps) - [Either](#either) + - [Lenses](#lenses) - [Notes](#notes) - [License](#license) @@ -43,17 +44,17 @@ Add the following dependency to your: `pom.xml` ([Maven](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html)): ```xml - - com.jnape.palatable - lambda - 1.3 - + + com.jnape.palatable + lambda + 1.5 + ``` `build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)): ```gradle - compile group: 'com.jnape.palatable', name: 'lambda', version: '1.3' +compile group: 'com.jnape.palatable', name: 'lambda', version: '1.5' ``` @@ -62,77 +63,77 @@ Add the following dependency to your: First, the obligatory `map`/`filter`/`reduce` example: ```Java - Integer sumOfEvenIncrements = - reduceLeft((x, y) -> x + y, - filter(x -> x % 2 == 0, - map(x -> x + 1, asList(1, 2, 3, 4, 5)))); - //-> 12 +Integer sumOfEvenIncrements = + reduceLeft((x, y) -> x + y, + filter(x -> x % 2 == 0, + map(x -> x + 1, asList(1, 2, 3, 4, 5)))); +//-> 12 ``` Every function in lambda is [curried](https://www.wikiwand.com/en/Currying), so we could have also done this: ```Java - Fn1, Integer> sumOfEvenIncrementsFn = - map((Integer x) -> x + 1) - .then(filter(x -> x % 2 == 0)) - .then(reduceLeft((x, y) -> x + y)); - - Integer sumOfEvenIncrements = sumOfEvenIncrementsFn.apply(asList(1, 2, 3, 4, 5)); - //-> 12 +Fn1, Integer> sumOfEvenIncrementsFn = + map((Integer x) -> x + 1) + .then(filter(x -> x % 2 == 0)) + .then(reduceLeft((x, y) -> x + y)); + +Integer sumOfEvenIncrements = sumOfEvenIncrementsFn.apply(asList(1, 2, 3, 4, 5)); +//-> 12 ``` How about the positive squares below 100: ```Java - Iterable positiveSquaresBelow100 = - takeWhile(x -> x < 100, map(x -> x * x, iterate(x -> x + 1, 1))); - //-> [1, 4, 9, 16, 25, 36, 49, 64, 81] +Iterable positiveSquaresBelow100 = + takeWhile(x -> x < 100, map(x -> x * x, iterate(x -> x + 1, 1))); +//-> [1, 4, 9, 16, 25, 36, 49, 64, 81] ``` We could have also used `unfoldr`: ```Java - Iterable positiveSquaresBelow100 = unfoldr(x -> { - int square = x * x; - return square < 100 ? Optional.of(tuple(square, x + 1)) : Optional.empty(); - }, 1); - //-> [1, 4, 9, 16, 25, 36, 49, 64, 81] +Iterable positiveSquaresBelow100 = unfoldr(x -> { + int square = x * x; + return square < 100 ? Optional.of(tuple(square, x + 1)) : Optional.empty(); + }, 1); +//-> [1, 4, 9, 16, 25, 36, 49, 64, 81] ``` What if we want the cross product of a domain and codomain: ```Java - Iterable> crossProduct = - take(10, cartesianProduct(asList(1, 2, 3), asList("a", "b", "c"))); - //-> (1,"a"), (1,"b"), (1,"c"), (2,"a"), (2,"b"), (2,"c"), (3,"a"), (3,"b"), (3,"c") +Iterable> crossProduct = + take(10, cartesianProduct(asList(1, 2, 3), asList("a", "b", "c"))); +//-> (1,"a"), (1,"b"), (1,"c"), (2,"a"), (2,"b"), (2,"c"), (3,"a"), (3,"b"), (3,"c") ``` Let's compose two functions: ```Java - Fn1 add = x -> x + 1; - Fn1 subtract = x -> x -1; +Fn1 add = x -> x + 1; +Fn1 subtract = x -> x -1; - Fn1 noOp = add.then(subtract); - // same as - Fn1 alsoNoOp = subtract.compose(subtract); +Fn1 noOp = add.then(subtract); +// same as +Fn1 alsoNoOp = subtract.compose(subtract); ``` And partially apply some: ```Java - Fn2 add = (x, y) -> x + y; +Fn2 add = (x, y) -> x + y; - Fn1 add1 = add.apply(1); - add1.apply(2); - //-> 3 +Fn1 add1 = add.apply(1); +add1.apply(2); +//-> 3 ``` And have fun with 3s: ```Java - Iterable> multiplesOf3InGroupsOf3 = - take(10, inGroupsOf(3, unfoldr(x -> Optional.of(tuple(x * 3, x + 1)), 1))); - //-> [[3, 6, 9], [12, 15, 18], [21, 24, 27]] +Iterable> multiplesOf3InGroupsOf3 = + take(10, inGroupsOf(3, unfoldr(x -> Optional.of(tuple(x * 3, x + 1)), 1))); +//-> [[3, 6, 9], [12, 15, 18], [21, 24, 27]] ``` Check out the [tests](https://github.com/palatable/lambda/tree/master/src/test/java/com/jnape/palatable/lambda/functions/builtin) or [javadoc](http://palatable.github.io/lambda/javadoc/) for more examples. @@ -149,13 +150,13 @@ HLists are type-safe heterogeneous lists, meaning they can store elements of dif The following illustrates how the linear expansion of the recursive type signature for `HList` prevents ill-typed expressions: ```Java - HCons> hList = HList.cons(1, HList.cons("foo", HList.nil())); +HCons> hList = HList.cons(1, HList.cons("foo", HList.nil())); - System.out.println(hList.head()); // prints 1 - System.out.println(hList.tail().head()); // prints "foo" +System.out.println(hList.head()); // prints 1 +System.out.println(hList.tail().head()); // prints "foo" - HNil nil = hList.tail().tail(); - //nil.head() won't type-check +HNil nil = hList.tail().tail(); +//nil.head() won't type-check ``` #### Tuples @@ -165,40 +166,40 @@ One of the primary downsides to using `HList`s in Java is how quickly the type s To address this, tuples in lambda are specializations of `HList`s up to 5 elements deep, with added support for index-based accessor methods. ```Java - HNil nil = HList.nil(); - SingletonHList singleton = nil.cons(5); - Tuple2 tuple2 = singleton.cons(4); - Tuple3 tuple3 = tuple2.cons(3); - Tuple4 tuple4 = tuple3.cons(2); - Tuple5 tuple5 = tuple4.cons(1); - - System.out.println(tuple2._1()); // prints 4 - System.out.println(tuple5._5()); // prints 5 +HNil nil = HList.nil(); +SingletonHList singleton = nil.cons(5); +Tuple2 tuple2 = singleton.cons(4); +Tuple3 tuple3 = tuple2.cons(3); +Tuple4 tuple4 = tuple3.cons(2); +Tuple5 tuple5 = tuple4.cons(1); + +System.out.println(tuple2._1()); // prints 4 +System.out.println(tuple5._5()); // prints 5 ``` Additionally, `HList` provides convenience static factory methods for directly constructing lists of up to 5 elements: ```Java - SingletonHList singleton = HList.singletonHList(1); - Tuple2 tuple2 = HList.tuple(1, 2); - Tuple3 tuple3 = HList.tuple(1, 2, 3); - Tuple4 tuple4 = HList.tuple(1, 2, 3, 4); - Tuple5 tuple5 = HList.tuple(1, 2, 3, 4, 5); +SingletonHList singleton = HList.singletonHList(1); +Tuple2 tuple2 = HList.tuple(1, 2); +Tuple3 tuple3 = HList.tuple(1, 2, 3); +Tuple4 tuple4 = HList.tuple(1, 2, 3, 4); +Tuple5 tuple5 = HList.tuple(1, 2, 3, 4, 5); ``` Finally, all `Tuple*` classes are instances of both `Functor` and `Bifunctor`: ```Java - Tuple2 mappedTuple2 = tuple(1, 2).biMap(x -> x + 1, Object::toString); +Tuple2 mappedTuple2 = tuple(1, 2).biMap(x -> x + 1, Object::toString); - System.out.println(mappedTuple2._1()); // prints 2 - System.out.println(mappedTuple2._2()); // prints "2" +System.out.println(mappedTuple2._1()); // prints 2 +System.out.println(mappedTuple2._2()); // prints "2" - Tuple3 mappedTuple3 = tuple("foo", true, 1).biMap(x -> !x, x -> x + 1); +Tuple3 mappedTuple3 = tuple("foo", true, 1).biMap(x -> !x, x -> x + 1); - System.out.println(mappedTuple3._1()); // prints "foo" - System.out.println(mappedTuple3._2()); // prints false - System.out.println(mappedTuple3._3()); // prints 2 +System.out.println(mappedTuple3._1()); // prints "foo" +System.out.println(mappedTuple3._2()); // prints false +System.out.println(mappedTuple3._3()); // prints 2 ``` ### Heterogeneous Maps @@ -206,14 +207,14 @@ Finally, all `Tuple*` classes are instances of both `Functor` and `Bifunctor`: HMaps are type-safe heterogeneous maps, meaning they can store mappings to different value types in the same map; however, whereas HLists encode value types in their type signatures, HMaps rely on the keys to encode the value type that they point to. ```Java - TypeSafeKey stringKey = TypeSafeKey.typeSafeKey(); - TypeSafeKey intKey = TypeSafeKey.typeSafeKey(); - HMap hmap = HMap.hMap(stringKey, "string value", - intKey, 1); - - Optional stringValue = hmap.get(stringKey); // Optional["string value"] - Optional intValue = hmap.get(intKey); // Optional[1] - Optional anotherIntValue = hmap.get(anotherIntKey); // Optional.empty +TypeSafeKey stringKey = TypeSafeKey.typeSafeKey(); +TypeSafeKey intKey = TypeSafeKey.typeSafeKey(); +HMap hmap = HMap.hMap(stringKey, "string value", + intKey, 1); + +Optional stringValue = hmap.get(stringKey); // Optional["string value"] +Optional intValue = hmap.get(intKey); // Optional[1] +Optional anotherIntValue = hmap.get(anotherIntKey); // Optional.empty ``` ### Either @@ -223,18 +224,97 @@ Binary tagged unions are represented as `Either`s, which resolve to one of Rather than supporting explicit value unwrapping, `Either` supports many useful comprehensions to help facilitate type-safe interactions. For example, `Either#match` is used to resolve an `Either` to a different type. ```Java - Either right = Either.right(1); - Either left = Either.left("Head fell off"); +Either right = Either.right(1); +Either left = Either.left("Head fell off"); - Boolean successful = right.match(l -> false, r -> true); - //-> true - - List values = left.match(l -> Collections.emptyList(), Collections::singletonList); - //-> [] +Boolean successful = right.match(l -> false, r -> true); +//-> true + +List values = left.match(l -> Collections.emptyList(), Collections::singletonList); +//-> [] ``` Check out the tests for [more examples](https://github.com/palatable/lambda/blob/master/src/test/java/com/jnape/palatable/lambda/adt/EitherTest.java) of ways to interact with `Either`. +Lenses +---- + +Lambda also ships with a first-class lens type, as well as a small library of useful general lenses: + +```Java +Lens, List, Optional, String> stringAt0 = ListLens.at(0); + +List strings = asList("foo", "bar", "baz"); +view(stringAt0, strings); // Optional[foo] +set(stringAt0, "quux", strings); // [quux, bar, baz] +over(stringAt0, s -> s.map(String::toUpperCase).orElse(""), strings); // [FOO, bar, baz] +``` + +There are three functions that lambda provides that interface directly with lenses: `view`, `over`, and `set`. As the name implies, `view` and `set` are used to retrieve values and store values, respectively, whereas `over` is used to apply a function to the value a lens is focused on, alter it, and store it (you can think of `set` as a specialization of `over` using `constantly`). + +Lenses can be easily created. Consider the following `Person` class: + +```Java +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())); + } +} +``` + +...and a lens for getting and setting `age` as an `int`: + +```Java +Lens ageLensWithInt = Lens.lens(Person::getAge, Person::setAge); + +//or, when each pair of type arguments match... + +Lens.Simple alsoAgeLensWithInt = Lens.simpleLens(Person::getAge, Person::setAge); +``` + +If we wanted a lens for the `LocalDate` version of `setAge`, we could use the same method references and only alter the type signature: + +```Java +Lens ageLensWithLocalDate = Lens.lens(Person::getAge, Person::setAge); +``` + +Compatible lenses can be trivially composed: + +```Java +Lens, List, Optional, Integer> at0 = ListLens.at(0); +Lens>, Map>, List, List> atFoo = MapLens.atKey("foo", emptyList()); + +view(atFoo.andThen(at0), singletonMap("foo", asList(1, 2, 3))); // Optional[1] +``` + +Lens provides independent `map` operations for each parameter, so incompatible lenses can also be composed: + +```Java +Lens, List, Optional, Integer> at0 = ListLens.at(0); +Lens>, Map>, Optional>, List> atFoo = MapLens.atKey("foo"); +Lens>, Map>, Optional, Integer> composed = + atFoo.mapA(optL -> optL.orElse(singletonList(-1))) + .andThen(at0); + +view(composed, singletonMap("foo", emptyList())); // Optional.empty +``` + +Check out the tests or the [javadoc](http://palatable.github.io/lambda/javadoc/) for more info. + Notes ----- diff --git a/pom.xml b/pom.xml index 491ad2e47..7d79c9c8a 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ lambda - 1.5 + 1.5.1 jar Lambda diff --git a/src/main/java/com/jnape/palatable/lambda/lens/Lens.java b/src/main/java/com/jnape/palatable/lambda/lens/Lens.java index 6717baefc..62b24ccfb 100644 --- a/src/main/java/com/jnape/palatable/lambda/lens/Lens.java +++ b/src/main/java/com/jnape/palatable/lambda/lens/Lens.java @@ -126,10 +126,10 @@ * 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 + * @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 { @@ -142,8 +142,8 @@ public interface Lens extends Functor { * 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 + * @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() { @@ -152,7 +152,51 @@ default , FB extends Functor> Fixed @Override default Lens fmap(Function fn) { - return this.compose(Lens.lens(id(), (s, t) -> fn.apply(t))); + return compose(Lens.lens(id(), (s, t) -> fn.apply(t))); + } + + /** + * Contravariantly map S to R, yielding a new lens. + * + * @param fn the mapping function + * @param the type of the new "larger" value for reading + * @return the new lens + */ + default Lens mapS(Function fn) { + return compose(lens(fn, (r, t) -> t)); + } + + /** + * Covariantly map T to U, yielding a new lens. + * + * @param fn the mapping function + * @param the type of the new "larger" value for putting + * @return the new lens + */ + default Lens mapT(Function fn) { + return fmap(fn); + } + + /** + * Covariantly map A to C, yielding a new lens. + * + * @param fn the mapping function + * @param the type of the new "smaller" value that is read + * @return the new lens + */ + default Lens mapA(Function fn) { + return andThen(lens(fn, (a, b) -> b)); + } + + /** + * Contravariantly map B to Z, yielding a new lens. + * + * @param fn the mapping function + * @param the type of the new "smaller" update value + * @return the new lens + */ + default Lens mapB(Function fn) { + return andThen(lens(id(), (a, z) -> fn.apply(z))); } /** @@ -184,10 +228,10 @@ default Lens compose(Lens g) { * * @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 + * @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, @@ -207,8 +251,8 @@ public , FB extends Functor> FT apply(Function The type of both "larger" values - * @param The type of both "smaller" values + * @param the type of both "larger" values + * @param the type of both "smaller" values * @return the lens */ @SuppressWarnings("unchecked") @@ -221,8 +265,8 @@ static Lens.Simple simpleLens(Function gett * 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 + * @param the type of both "larger" values + * @param the type of both "smaller" values */ @FunctionalInterface interface Simple extends Lens { @@ -244,10 +288,10 @@ default Lens.Simple andThen(Lens.Simple f) { /** * 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 + * @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> @@ -259,12 +303,12 @@ interface Fixed, FA extends Functor> * 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 + * @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> 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 index 49982aad7..009c26f18 100644 --- a/src/main/java/com/jnape/palatable/lambda/lens/lenses/ListLens.java +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/ListLens.java @@ -8,6 +8,7 @@ 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.lenses.OptionalLens.unLiftA; /** * Lenses that operate on {@link List}s. @@ -44,4 +45,18 @@ public static Lens, List, Optional, X> at(int index) { return xs; }); } + + /** + * Convenience static factory method for creating a lens that focuses on an element in a list at a particular index, + * returning defaultValue if there is no value at that index. + * + * @param index the index to focus on + * @param defaultValue the value to use if there is no element at index + * @param the list element type + * @return the element at the index, or defaultValue + */ + @SuppressWarnings("unchecked") + public static Lens.Simple, X> at(int index, X defaultValue) { + return unLiftA(at(index), defaultValue)::apply; + } } diff --git a/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java index 0f3e91370..6e18502aa 100644 --- a/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/MapLens.java @@ -12,6 +12,7 @@ import static com.jnape.palatable.lambda.lens.Lens.lens; import static com.jnape.palatable.lambda.lens.Lens.simpleLens; import static com.jnape.palatable.lambda.lens.functions.View.view; +import static com.jnape.palatable.lambda.lens.lenses.OptionalLens.unLiftA; import static java.util.stream.Collectors.toSet; /** @@ -50,6 +51,21 @@ public static Lens, Map, Optional, V> atKey(K k) { }); } + /** + * Convenience static factory method for creating a lens that focuses on a value at a key in a map, falling back to + * defaultV if the value is missing. + * + * @param k the key to focus on + * @param defaultValue the default value to use in case of a missing value at key + * @param the key type + * @param the value type + * @return a lens that focuses on the value at the key + */ + @SuppressWarnings("unchecked") + public static Lens.Simple, V> atKey(K k, V defaultValue) { + return unLiftA(atKey(k), defaultValue)::apply; + } + /** * Convenience static factory method for creating a lens that focuses on the keys of a map. * 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 index 8737a8cf2..ec8eba0e6 100644 --- a/src/main/java/com/jnape/palatable/lambda/lens/lenses/OptionalLens.java +++ b/src/main/java/com/jnape/palatable/lambda/lens/lenses/OptionalLens.java @@ -23,4 +23,122 @@ private OptionalLens() { public static Lens.Simple> asOptional() { return simpleLens(Optional::ofNullable, (v, optV) -> optV.orElse(v)); } + + /** + * Given a lens and a default S, lift S into Optional. + * + * @param lens the lens + * @param defaultS the S to use if an empty Optional value is given + * @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 with S lifted + */ + public static Lens, T, A, B> liftS(Lens lens, S defaultS) { + return lens.mapS(optS -> optS.orElse(defaultS)); + } + + /** + * Given a lens, lift T into Optional. + * + * @param lens the lens + * @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 with T lifted + */ + public static Lens, A, B> liftT(Lens lens) { + return lens.mapT(Optional::ofNullable); + } + + /** + * Given a lens, lift A into Optional. + * + * @param lens the lens + * @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 with A lifted + */ + public static Lens, B> liftA(Lens lens) { + return lens.mapA(Optional::ofNullable); + } + + /** + * Given a lens and a default B, lift B into Optional. + * + * @param lens the lens + * @param defaultB the B to use if an empty Optional value is given + * @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 with B lifted + */ + public static Lens> liftB(Lens lens, B defaultB) { + return lens.mapB(optB -> optB.orElse(defaultB)); + } + + /** + * Given a lens with S lifted into Optional, flatten S back down. + * + * @param lens the lens + * @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 with S flattened + */ + public static Lens unLiftS(Lens, T, A, B> lens) { + return lens.mapS(Optional::ofNullable); + } + + /** + * Given a lens with T lifted into Optional and a default T, flatten T back + * down. + * + * @param lens the lens + * @param defaultT the T to use if lens produces an empty Optional + * @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 with T flattened + */ + public static Lens unLiftT(Lens, A, B> lens, T defaultT) { + return lens.mapT(optT -> optT.orElse(defaultT)); + } + + /** + * Given a lens with A lifted into Optional and a default A, flatten A back + * down. + * + * @param lens the lens + * @param defaultA the A to use if lens produces an empty Optional + * @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 with A flattened + */ + public static Lens unLiftA(Lens, B> lens, A defaultA) { + return lens.mapA(optA -> optA.orElse(defaultA)); + } + + /** + * Given a lens with B lifted, flatten B back down. + * + * @param lens the lens + * @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 with B flattened + */ + public static Lens unLiftB(Lens> lens) { + return lens.mapB(Optional::ofNullable); + } } diff --git a/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java b/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java index c9deff678..c7e1419a9 100644 --- a/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java +++ b/src/test/java/com/jnape/palatable/lambda/lens/LensTest.java @@ -7,11 +7,13 @@ import java.util.List; 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.functions.Set.set; import static com.jnape.palatable.lambda.lens.functions.View.view; +import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; @@ -51,6 +53,19 @@ public void functorProperties() { assertEquals(false, set(LENS.fmap(Set::isEmpty), 1, singletonList("foo"))); } + @Test + public void mapsIndividuallyOverParameters() { + Lens lens = lens(s -> s.charAt(0), (s, b) -> s.length() == b); + Lens, Optional, Optional, Optional> theGambit = lens + .mapS((Optional optS) -> optS.orElse("")) + .mapT(Optional::ofNullable) + .mapA(Optional::ofNullable) + .mapB((Optional optI) -> optI.orElse(-1)); + + Lens.Fixed, Optional, Optional, Optional, Identity>, Identity>> fixed = theGambit.fix(); + assertEquals(Optional.of(true), fixed.apply(optC -> new Identity<>(optC.map(c -> parseInt(Character.toString(c)))), Optional.of("321")).runIdentity()); + } + @Test public void composition() { Map> map = singletonMap("foo", asList("one", "two", "three")); 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 index 2a71ae703..e01373f77 100644 --- a/src/test/java/com/jnape/palatable/lambda/lens/lenses/ListLensTest.java +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/ListLensTest.java @@ -49,4 +49,14 @@ public void atFocusesOnElementAtIndex() { assertEquals(asList("quux", "bar", "baz"), set(at0, "quux", xs)); assertEquals(emptyList(), set(at0, "quux", emptyList())); } + + @Test + public void atWithDefaultValueFocusesOnElementAtIndex() { + Lens, List, String, String> at0 = ListLens.at(0, "missing"); + + assertEquals("foo", view(at0, xs)); + assertEquals("missing", 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 index bd05cf541..e89c7582c 100644 --- a/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/MapLensTest.java @@ -15,6 +15,7 @@ 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 java.util.Collections.emptyMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; @@ -60,6 +61,22 @@ public void atKeyFocusesOnValueAtKey() { assertSame(m, updated); } + @Test + public void atKeyWithDefaultValueFocusedOnValueAtKey() { + Lens, Map, Integer, Integer> atFoo = MapLens.atKey("foo", -1); + + assertEquals((Integer) 1, view(atFoo, m)); + assertEquals((Integer) (-1), view(atFoo, emptyMap())); + + Map updated = set(atFoo, 11, m); + assertEquals(new HashMap() {{ + put("foo", 11); + put("bar", 2); + put("baz", 3); + }}, updated); + assertSame(m, updated); + } + @Test public void keysFocusesOnKeys() { Lens, Map, Set, Set> keys = keys(); 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 index 708a57704..b5908d600 100644 --- a/src/test/java/com/jnape/palatable/lambda/lens/lenses/OptionalLensTest.java +++ b/src/test/java/com/jnape/palatable/lambda/lens/lenses/OptionalLensTest.java @@ -1,16 +1,29 @@ 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.Optional; +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 com.jnape.palatable.lambda.lens.lenses.OptionalLens.liftA; +import static com.jnape.palatable.lambda.lens.lenses.OptionalLens.liftB; +import static com.jnape.palatable.lambda.lens.lenses.OptionalLens.liftS; +import static com.jnape.palatable.lambda.lens.lenses.OptionalLens.liftT; import static org.junit.Assert.assertEquals; public class OptionalLensTest { + private Lens lens; + + @Before + public void setUp() throws Exception { + lens = lens(s -> s.charAt(0), (s, b) -> s.length() == b); + } + @Test public void asOptionalWrapsValuesInOptional() { Lens.Simple> asOptional = OptionalLens.asOptional(); @@ -20,4 +33,48 @@ public void asOptionalWrapsValuesInOptional() { assertEquals("bar", set(asOptional, Optional.of("bar"), "foo")); assertEquals("foo", set(asOptional, Optional.empty(), "foo")); } + + @Test + public void liftSLiftsSToOptional() { + assertEquals((Character) '3', view(liftS(lens, "3"), Optional.empty())); + } + + @Test + public void liftTLiftsTToOptional() { + assertEquals(Optional.of(true), set(liftT(lens), 3, "123")); + } + + @Test + public void liftALiftsAToOptional() { + assertEquals(Optional.of('1'), view(liftA(lens), "123")); + } + + @Test + public void liftBLiftsBToOptional() { + assertEquals(true, set(OptionalLens.liftB(lens, 1), Optional.empty(), "1")); + } + + @Test + public void unLiftSPullsSOutOfOptional() { + Lens, Optional, Optional, Optional> liftedToOptional = liftS(liftT(liftA(liftB(lens, 3))), "123"); + assertEquals(Optional.of('f'), view(OptionalLens.unLiftS(liftedToOptional), "f")); + } + + @Test + public void unLiftTPullsTOutOfOptional() { + Lens, Optional, Optional, Optional> liftedToOptional = liftS(liftT(liftA(liftB(lens, 3))), "123"); + assertEquals(true, set(OptionalLens.unLiftT(liftedToOptional, false), Optional.of(3), Optional.of("321"))); + } + + @Test + public void unLiftAPullsAOutOfOptional() { + Lens, Optional, Optional, Optional> liftedToOptional = liftS(liftT(liftA(liftB(lens, 3))), "123"); + assertEquals((Character) '1', view(OptionalLens.unLiftA(liftedToOptional, '4'), Optional.empty())); + } + + @Test + public void unLiftBPullsBOutOfOptional() { + Lens, Optional, Optional, Optional> liftedToOptional = liftS(liftT(liftA(liftB(lens, 3))), "123"); + assertEquals(Optional.of(true), set(OptionalLens.unLiftB(liftedToOptional), 3, Optional.of("321"))); + } } \ No newline at end of file