# lambda
**Repository Path**: mirrors_andyglick/lambda
## Basic Information
- **Project Name**: lambda
- **Description**: Functional patterns for Java
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-09-24
- **Last Updated**: 2026-02-28
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
λ
======
[](https://travis-ci.org/palatable/lambda)
[](http://search.maven.org/#search%7Cga%7C1%7Ccom.jnape.palatable.lambda)
Functional patterns for Java
#### Table of Contents
- [Background](#background)
- [Installation](#installation)
- [Examples](#examples)
- [Semigroups](#semigroups)
- [Monoids](#monoids)
- [Functors](#functors)
- [Bifunctors](#bifunctors)
- [Profunctors](#profunctors)
- [Applicatives](#applicatives)
- [Monads](#monads)
- [Traversables](#traversables)
- [ADTs](#adts)
- [Maybe](#maybe)
- [HList](#hlist)
- [Tuples](#tuples)
- [HMaps](#hmaps)
- [CoProducts](#coproducts)
- [Either](#either)
- [Lenses](#lenses)
- [Notes](#notes)
- [License](#license)
Background
----------
Lambda was born out of a desire to use some of the same canonical functions (e.g. `unfoldr`, `takeWhile`, `zipWith`) and functional patterns (e.g. `Functor` and friends) that are idiomatic in other languages and make them available for Java.
Some things a user of lambda most likely values:
- Lazy evaluation
- Immutability by design
- Composition
- Higher-level abstractions
- Parametric polymorphism
Generally, everything that lambda produces is lazily-evaluated (except for terminal operations like `reduce`), immutable (except for `Iterator`s, since it's effectively impossible), composable (even between different arities, where possible), foundational (maximally contravariant), and parametrically type-checked (even where this adds unnecessary constraints due to a lack of higher-kinded types).
Although the library is currently (very) small, these values should always be the driving forces behind future growth.
Installation
------------
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
3.1.0
```
`build.gradle` ([Gradle](https://docs.gradle.org/current/userguide/dependency_management.html)):
```gradle
compile group: 'com.jnape.palatable', name: 'lambda', version: '3.1.0'
```
Examples
--------
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
```
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)
.andThen(filter(x -> x % 2 == 0))
.andThen(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]
```
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]
```
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")
```
Let's compose two functions:
```Java
Fn1 add = x -> x + 1;
Fn1 subtract = x -> x -1;
Fn1 noOp = add.andThen(subtract);
// same as
Fn1 alsoNoOp = subtract.compose(add);
```
And partially apply some:
```Java
Fn2 add = (x, y) -> x + y;
Fn1 add1 = add.apply(1);
add1.apply(2);
//-> 3
```
And have fun with 3s:
```Java
Iterable> multiplesOf3InGroupsOf3 =
take(3, 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.
Semigroups
----
[Semigroups](https://en.wikipedia.org/wiki/Semigroup) are supported via `Semigroup`, a subtype of `Fn2`, and add left and right folds over an `Iterable`.
```Java
Semigroup add = (augend, addend) -> augend + addend;
add.apply(1, 2); //-> 3
add.foldLeft(0, asList(1, 2, 3)); //-> 6
```
Lambda ships some default logical semigroups for lambda types and core JDK types. Common examples are:
- `AddAll` for concatenating two `Collection`s
- `Collapse` for collapsing two `Tuple2`s together
- `Merge` for merging two `Either`s using left-biasing semantics
Check out the [semigroup](https://palatable.github.io/lambda/javadoc/com/jnape/palatable/lambda/semigroup/builtin/package-summary.html) package for more examples.
Monoids
----
[Monoids](https://en.wikipedia.org/wiki/Monoid) are supported via `Monoid`, a subtype of `Semigroup` with an `A #identity()` method, and add left and right reduces over an `Iterable`, as well as `foldMap`.
```Java
Monoid multiply = monoid((x, y) -> x * y, 1);
multiple.reduceLeft(emptyList()); //-> 1
multiply.reduceLeft(asList(1, 2, 3)); //-> 6
multiply.foldMap(Integer::parseInt, asList("1", "2", "3")); //-> also 6
```
Some commonly used lambda monoid implementations include:
- `Present` for merging together two `Optional`s
- `Join` for joining two `String`s
- `And` for logical conjunction of two `Boolean`s
- `Or` for logical disjunction of two `Boolean`s
Additionally, instances of `Monoid` can be trivially synthesized from instances of `Semigroup` via the `Monoid#monoid` static factory method, taking the `Semigroup` and the identity element `A` or a supplier of the identity element `Supplier`.
Check out the [monoid](https://palatable.github.io/lambda/javadoc/com/jnape/palatable/lambda/monoid/builtin/package-summary.html) package for more examples.
Functors
----
Functors are implemented via the `Functor` interface, and are sub-typed by every function type that lambda exports, as well as many of the [ADTs](#adts).
```Java
public final class Slot implements Functor {
private final A a;
public Slot(A a) {
this.a = a;
}
public A getA() {
return a;
}
@Override
public Slot fmap(Function super A, ? extends B> fn) {
return new Slot<>(fn.apply(a));
}
}
Slot intSlot = new Slot<>(1);
Slot stringSlot = intSlot.fmap(x -> "number: " + x);
stringSlot.getA(); //-> "number: 1"
```
Examples of functors include:
- `Fn*`, `Semigroup`, and `Monoid`
- `SingletonHList` and `Tuple*`
- `Choice*`
- `Either`
- `Const`, `Identity`, and `Compose`
- `Lens`
Implementing `Functor` is as simple as providing a definition for the covariant mapping function `#fmap` (ideally satisfying the [two laws](https://hackage.haskell.org/package/base-4.9.1.0/docs/Data-Functor.html)).
### Bifunctors
Bifunctors -- functors that support two parameters that can be covariantly mapped over -- are implemented via the `Bifunctor` interface.
```Java
public final class Pair implements Bifunctor {
private final A a;
private final B b;
public Pair(A a, B b) {
this.a = a;
this.b = b;
}
public A getA() {
return a;
}
public B getB() {
return b;
}
@Override
public Pair biMap(Function super A, ? extends C> lFn,
Function super B, ? extends D> rFn) {
return new Pair<>(lFn.apply(a), rFn.apply(b));
}
}
Pair stringIntPair = new Pair<>("str", 1);
Pair intBooleanPair = stringIntPair.biMap(String::length, x -> x % 2 == 0);
intBooleanPair.getA(); //-> 3
intBooleanPair.getB(); //-> false
```
Examples of bifunctors include:
- `Tuple*`
- `Choice*`
- `Either`
- `Const`
Implementing `Bifunctor` requires implementing *either* `biMapL` and `biMapR` *or* `biMap`. As with `Functor`, there are a [few laws](https://hackage.haskell.org/package/base-4.9.1.0/docs/Data-Bifunctor.html) that well-behaved instances of `Bifunctor` should adhere to.
### Profunctors
Profunctors -- functors that support one parameter that can be mapped over contravariantly, and a second parameter that can be mapped over covariantly -- are implemented via the `Profunctor` interface.
```Java
Fn1 add2 = (x) -> x + 2;
add2.diMap(Integer::parseInt, Object::toString).apply("1"); //-> "3"
```
Examples of profunctors include:
- `Fn*`
- `Lens`
Implementing `Profunctor` requires implementing *either* `diMapL` and `diMapR` *or* `diMap`. As with `Functor` and `Bifunctor`, there are [some laws](https://hackage.haskell.org/package/profunctors-5.2/docs/Data-Profunctor.html) that well behaved instances of `Profunctor` should adhere to.
### Applicatives
Applicative functors -- functors that can be applied together with a 2-arity or higher function -- are implemented via the `Applicative` interface.
```Java
public final class Slot implements Applicative {
private final A a;
public Slot(A a) {
this.a = a;
}
public A getA() {
return a;
}
@Override
public Slot fmap(Function super A, ? extends B> fn) {
return pure(fn.apply(a));
}
@Override
public Slot pure(B b) {
return new Slot<>(b);
}
@Override
public Slot zip(Applicative, Slot> appFn) {
return pure(appFn.>>coerce().getA().apply(getA()));
}
}
Fn2 add = (x, y) -> x + y;
Slot x = new Slot<>(1);
Slot y = new Slot<>(2);
Slot z = y.zip(x.fmap(add)); //-> Slot{a=3}
```
Examples of applicative functors include:
- `Fn*`, `Semigroup`, and `Monoid`
- `SingletonHList` and `Tuple*`
- `Choice*`
- `Either`
- `Const`, `Identity`, and `Compose`
- `Lens`
In addition to implementing `fmap` from `Functor`, implementing an applicative functor involves providing two methods: `pure`, a method that lifts a value into the functor; and `zip`, a method that applies a lifted function to a lifted value, returning a new lifted value. As usual, there are [some laws](https://hackage.haskell.org/package/base-4.9.1.0/docs/Control-Applicative.html) that should be adhered to.
### Monads
Monads are applicative functors that additionally support a chaining operation, `flatMap :: (a -> f b) -> f a -> f b`: a function from the functor's parameter to a new instance of the same functor over a potentially different parameter. Because the function passed to `flatMap` can return a different instance of the same functor, functors can take advantage of multiple constructions that yield different functorial operations, like short-circuiting, as in the following example using `Either`:
```Java
class Person {
Optional occupation() {
return Optional.empty();
}
}
class Occupation {
}
public static void main(String[] args) {
Fn1> parseId = str -> Either.trying(() -> Integer.parseInt(str), __ -> str + " is not a valid id");
Map database = new HashMap<>();
Fn1> lookupById = id -> Either.fromOptional(Optional.ofNullable(database.get(id)),
() -> "No person found for id " + id);
Fn1> getOccupation = p -> Either.fromOptional(p.occupation(), () -> "Person was unemployed");
Either occupationOrError =
parseId.apply("12") // Either
.flatMap(lookupById) // Either
.flatMap(getOccupation); // Either
}
```
In the previous example, if any of `parseId`, `lookupById`, or `getOccupation` fail, no further `flatMap` computations can succeed, so the result short-circuits to the first `left` value that is returned. This is completely predictable from the type signature of `Monad` and `Either`: `Either` is a `Monad`, so the single arity `flatMap` can have nothing to map in the case where there is no `R` value. With experience, it generally becomes quickly clear what the logical behavior of `flatMap` *must* be given the type signatures.
That's it. Monads are neither [elephants](http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html) nor are they [burritos](https://blog.plover.com/prog/burritos.html); they're simply types that support a) the ability to lift a value into them, and b) a chaining function `flatMap :: (a -> f b) -> f a -> f b` that can potentially return different instances of the same monad. If a type can do those two things (and obeys [the laws](https://wiki.haskell.org/Monad_laws)), it is a monad.
Further, if a type is a monad, it is necessarily an `Applicative`, which makes it necessarily a `Functor`, so *lambda* enforces this tautology via a hierarchical constraint.
### Traversables
Traversable functors -- functors that can be "traversed from left to right" -- are implemented via the `Traversable` interface.
```Java
public abstract class Maybe implements Traversable {
private Maybe() {
}
@Override
public abstract Applicative, App> traverse(
Function super A, ? extends Applicative> fn,
Function super Traversable, ? extends Applicative extends Traversable, App>> pure);
@Override
public abstract Maybe fmap(Function super A, ? extends B> fn);
private static final class Just extends Maybe {
private final A a;
private Just(A a) {
this.a = a;
}
@Override
public Applicative, App> traverse(
Function super A, ? extends Applicative> fn,
Function super Traversable, ? extends Applicative extends Traversable, App>> pure) {
return fn.apply(a).fmap(Just::new);
}
@Override
public Maybe fmap(Function super A, ? extends B> fn) {
return new Just<>(fn.apply(a));
}
}
private static final class Nothing extends Maybe {
@Override
@SuppressWarnings("unchecked")
public Applicative, App> traverse(
Function super A, ? extends Applicative> fn,
Function super Traversable, ? extends Applicative extends Traversable, App>> pure) {
return pure.apply((Maybe) this).fmap(x -> (Maybe) x);
}
@Override
@SuppressWarnings("unchecked")
public Maybe fmap(Function super A, ? extends B> fn) {
return (Maybe) this;
}
}
}
Maybe just1 = Maybe.just(1);
Maybe nothing = Maybe.nothing();
Either> traversedJust = just1.traverse(x -> right(x + 1), empty -> left("empty"))
.fmap(x -> (Maybe) x)
.coerce(); //-> Right(Just(2))
Either> traversedNothing = nothing.traverse(x -> right(x + 1), empty -> left("empty"))
.fmap(x -> (Maybe) x)
.coerce(); //-> Left("empty")
```
Examples of traversable functors include:
- `SingletonHList` and `Tuple*`
- `Choice*`
- `Either`
- `Const` and `Identity`
- `LambdaIterable` for wrapping `Iterable` in an instance of `Traversable`
In addition to implementing `fmap` from `Functor`, implementing a traversable functor involves providing an implementation of `traverse`.
As always, there are [some laws](https://hackage.haskell.org/package/base-4.9.1.0/docs/Data-Traversable.html) that should be observed.
ADTs
----
Lambda also supports a few first-class [algebraic data types](https://www.wikiwand.com/en/Algebraic_data_type).
### Maybe
`Maybe` is the _lambda_ analog to `java.util.Optional`. It behaves in much of the same way as `j.u.Optional`, except that it quite intentionally does not support the inherently unsafe `j.u.Optional#get`.
```Java
Maybe maybeInt = Maybe.just(1); // Just 1
Maybe maybeString = Maybe.nothing(); // Nothing
```
Also, because it's a _lambda_ type, it takes advantage of the full functor hierarchy, as well as some helpful conversion functions:
```Java
Maybe just = Maybe.maybe("string"); // Just "string"
Maybe nothing = Maybe.maybe(null); // Nothing
Maybe maybeX = Maybe.just(1);
Maybe maybeY = Maybe.just(2);
maybeY.zip(maybeX.fmap(x -> y -> x + y)); // Just 3
maybeY.zip(nothing()); // Nothing
Maybe.nothing().zip(maybeX.fmap(x -> y -> x + y)); // Nothing
Either right = maybeX.toEither(() -> "was empty"); // Right 1
Either left = Maybe.nothing().toEither(() -> "was empty"); // Left "was empty"
Maybe.fromEither(right); // Just 1
Maybe.fromEither(left); // Nothing
```
Finally, for compatibility purposes, `Maybe` and `j.u.Optional` can be trivially converted back and forth:
```Java
Maybe just1 = Maybe.just(1); // Just 1
Optional present1 = just1.toOptional(); // Optional.of(1)
Optional empty = Optional.empty(); // Optional.empty()
Maybe nothing = Maybe.fromOptional(empty); // Nothing
```
***Note***: One compatibility difference between `j.u.Optional` and `Maybe` is how `map`/`fmap` behave regarding functions that return `null`: `j.u.Optional` re-wraps `null` results from `map` operations in another `j.u.Optional`, whereas `Maybe` considers this to be an error, and throws an exception. The reason `Maybe` throws in this case is because `fmap` is not an operation to be called speculatively, and so any function that returns `null` in the context of an `fmap` operation is considered to be erroneous. Instead of calling `fmap` with a function that might return `null`, the function result should be wrapped in a `Maybe` and `flatMap` should be used, as illustrated in the following example:
```Java
Function nullResultFn = __ -> null;
Optional.of(1).map(nullResultFn); // Optional.empty()
Maybe.just(1).fmap(nullResultFn); // throws NullPointerException
Maybe.just(1).flatMap(nullResultFn.andThen(Maybe::maybe)); // Nothing
```
### Heterogeneous Lists (HLists)
HLists are type-safe heterogeneous lists, meaning they can store elements of different types in the same list while facilitating certain type-safe interactions.
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()));
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
```
#### Tuples
One of the primary downsides to using `HList`s in Java is how quickly the type signature grows.
To address this, tuples in lambda are specializations of `HList`s up to 8 elements deep, with added support for index-based accessor methods.
```Java
HNil nil = HList.nil();
SingletonHList singleton = nil.cons(8);
Tuple2 tuple2 = singleton.cons(7);
Tuple3 tuple3 = tuple2.cons(6);
Tuple4 tuple4 = tuple3.cons(5);
Tuple5 tuple5 = tuple4.cons(4);
Tuple6 tuple6 = tuple5.cons(3);
Tuple7 tuple7 = tuple6.cons(2);
Tuple8 tuple8 = tuple7.cons(1);
System.out.println(tuple2._1()); // prints 7
System.out.println(tuple8._8()); // prints 8
```
Additionally, `HList` provides convenience static factory methods for directly constructing lists of up to 8 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);
Tuple6 tuple6 = HList.tuple(1, 2, 3, 4, 5, 6);
Tuple7 tuple7 = HList.tuple(1, 2, 3, 4, 5, 6, 7);
Tuple8 tuple8 = HList.tuple(1, 2, 3, 4, 5, 6, 7, 8);
```
`Index` can be used for type-safe retrieval and updating of elements at specific indexes:
```Java
HCons>> hList = cons(1, cons("2", cons('3', nil())));
HCons> tuple = tuple(1, "2", '3');
Tuple5 longerHList = tuple(1, "2", '3', 4.0d, false);
Index>>> characterIndex =
Index.index().after().after();
characterIndex.get(hList); // '3'
characterIndex.get(tuple); // '3'
characterIndex.get(longerHList); // '3'
characterIndex.set('4', hList); // HList{ 1 :: "2" :: '4' }
```
Finally, all `Tuple*` classes are instances of both `Functor` and `Bifunctor`:
```Java
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"
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
```
### Heterogeneous Maps
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
```
### CoProducts
`CoProduct`s generalize unions of disparate types in a single consolidated type, and the `ChoiceN` ADTs represent canonical implementations of these coproduct types.
```Java
CoProduct3 string = Choice3.a("string");
CoProduct3 integer = Choice3.b(1);
CoProduct3 character = Choice3.c('a');
```
Rather than supporting explicit value unwrapping, which would necessarily jeopardize type safety, `CoProduct`s support a `match` method that takes one function per possible value type and maps it to a final common result type:
```Java
CoProduct3 string = Choice3.a("string");
CoProduct3 integer = Choice3.b(1);
CoProduct3 character = Choice3.c('a');
Integer result = string.match(String::length, identity(), Character::charCount); // 6
```
Additionally, because a `CoProduct2` guarantees a subset of a `CoProduct3`, the `diverge` method exists between `CoProduct` types of single magnitude differences to make it easy to use a more convergent `CoProduct` where a more divergent `CoProduct` is expected:
```Java
CoProduct2 coProduct2 = Choice2.a("string");
CoProduct3 coProduct3 = coProduct2.diverge(); // still just the coProduct2 value, adapted to the coProduct3 shape
```
There are `CoProduct` and `Choice` specializations for type unions of up to 8 different types: `CoProduct2` through `CoProduct8`, and `Choice2` through `Choice8`, respectively.
### Either
`Either` represents a specialized `CoProduct2`, which resolve to one of two possible values: a left value wrapping an `L`, or a right value wrapping an `R` (typically an exceptional value or a successful value, respectively).
As with `CoProduct2`, rather than supporting explicit value unwrapping, `Either` supports many useful comprehensions to help facilitate type-safe interactions:
```Java
Either right = Either.right(1);
Either left = Either.left("Head fell off");
Integer result = right.orElse(-1);
//-> 1
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