Functional patterns for Java
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 likereduce
), immutable (except forIterator
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.
Add the following dependency to your:
pom.xml
(Maven):
<dependency>
<groupId>com.jnape.palatable</groupId>
<artifactId>lambda</artifactId>
<version>5.4.0</version>
</dependency>
build.gradle
(Gradle):
compilegroup:'com.jnape.palatable',name:'lambda',version:'5.4.0'
First, the obligatorymap
/filter
/reduce
example:
Maybe<Integer>sumOfEvenIncrements=
reduceLeft((x,y) ->x+y,
filter(x->x%2==0,
map(x->x+1,asList(1,2,3,4,5))));
//-> Just 12
Every function in lambda iscurried,so we could have also done this:
Fn1<Iterable<Integer>,Maybe<Integer>>sumOfEvenIncrementsFn=
map((Integerx) ->x+1)
.fmap(filter(x->x%2==0))
.fmap(reduceLeft((x,y) ->x+y));
Maybe<Integer>sumOfEvenIncrements=sumOfEvenIncrementsFn.apply(asList(1,2,3,4,5));
//-> Just 12
How about the positive squares below 100:
Iterable<Integer>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 usedunfoldr
:
Iterable<Integer>positiveSquaresBelow100=unfoldr(x-> {
intsquare=x*x;
returnsquare<100?Maybe.just(tuple(square,x+1)):Maybe.nothing();
},1);
//-> [1, 4, 9, 16, 25, 36, 49, 64, 81]
What if we want the cross product of a domain and codomain:
Iterable<Tuple2<Integer,String>>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:
Fn1<Integer,Integer>add=x->x+1;
Fn1<Integer,Integer>subtract=x->x-1;
Fn1<Integer,Integer>noOp=add.fmap(subtract);
// same as
Fn1<Integer,Integer>alsoNoOp=subtract.contraMap(add);
And partially apply some:
Fn2<Integer,Integer,Integer>add= (x,y) ->x+y;
Fn1<Integer,Integer>add1=add.apply(1);
add1.apply(2);
//-> 3
And have fun with 3s:
Iterable<Iterable<Integer>>multiplesOf3InGroupsOf3=
take(3,inGroupsOf(3,unfoldr(x->Maybe.just(tuple(x*3,x+1)),1)));
//-> [[3, 6, 9], [12, 15, 18], [21, 24, 27]]
Check out thetestsorjavadocfor more examples.
Semigroupsare supported viaSemigroup<A>
,a subtype ofFn2<A,A,A>
,and add left and right folds over anIterable<A>
.
Semigroup<Integer>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 twoCollection
sCollapse
for collapsing twoTuple2
s togetherMerge
for merging twoEither
s using left-biasing semantics
Check out thesemigrouppackage for more examples.
Monoidsare supported viaMonoid<A>
,a subtype ofSemigroup<A>
with anA #identity()
method, and add left and right reduces over anIterable<A>
,as well asfoldMap
.
Monoid<Integer>multiply=monoid((x,y) ->x*y,1);
multiply.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 twoOptional
sJoin
for joining twoString
sAnd
for logical conjunction of twoBoolean
sOr
for logical disjunction of twoBoolean
s
Additionally, instances ofMonoid<A>
can be trivially synthesized from instances ofSemigroup<A>
via theMonoid#monoid
static factory method, taking theSemigroup
and the identity elementA
or a supplier of the identity elementSupplier<A>
.
Check out themonoidpackage for more examples.
Functors are implemented via theFunctor
interface, and are sub-typed by every function type that lambda exports, as well as many of theADTs.
publicfinalclassSlot<A>implementsFunctor<A,Slot> {
privatefinalAa;
publicSlot(Aa) {
this.a=a;
}
publicAgetA() {
returna;
}
@Override
public<B>Slot<B>fmap(Function<?superA,?extendsB>fn) {
returnnewSlot<>(fn.apply(a));
}
}
Slot<Integer>intSlot=newSlot<>(1);
Slot<String>stringSlot=intSlot.fmap(x->"number:"+x);
stringSlot.getA();//-> "number: 1"
Examples of functors include:
Fn*
,Semigroup
,andMonoid
SingletonHList
andTuple*
Choice*
Either
Const
,Identity
,andCompose
Lens
ImplementingFunctor
is as simple as providing a definition for the covariant mapping function#fmap
(ideally satisfying thetwo laws).
Bifunctors -- functors that support two parameters that can be covariantly mapped over -- are implemented via theBifunctor
interface.
publicfinalclassPair<A,B>implementsBifunctor<A,B,Pair> {
privatefinalAa;
privatefinalBb;
publicPair(Aa,Bb) {
this.a=a;
this.b=b;
}
publicAgetA() {
returna;
}
publicBgetB() {
returnb;
}
@Override
public<C,D>Pair<C,D>biMap(Function<?superA,?extendsC>lFn,
Function<?superB,?extendsD>rFn) {
returnnewPair<>(lFn.apply(a),rFn.apply(b));
}
}
Pair<String,Integer>stringIntPair=newPair<>("str",1);
Pair<Integer,Boolean>intBooleanPair=stringIntPair.biMap(String::length,x->x%2==0);
intBooleanPair.getA();//-> 3
intBooleanPair.getB();//-> false
Examples of bifunctors include:
Tuple*
Choice*
Either
Const
ImplementingBifunctor
requires implementingeitherbiMapL
andbiMapR
orbiMap
.As withFunctor
,there are afew lawsthat well-behaved instances ofBifunctor
should adhere to.
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 theProfunctor
interface.
Fn1<Integer,Integer>add2= (x) ->x+2;
add2.<String,String>diMap(Integer::parseInt,Object::toString).apply("1");//-> "3"
Examples of profunctors include:
Fn*
Lens
ImplementingProfunctor
requires implementingeitherdiMapL
anddiMapR
ordiMap
.As withFunctor
andBifunctor
,there aresome lawsthat well behaved instances ofProfunctor
should adhere to.
Applicative functors -- functors that can be applied together with a 2-arity or higher function -- are implemented via theApplicative
interface.
publicfinalclassSlot<A>implementsApplicative<A,Slot> {
privatefinalAa;
publicSlot(Aa) {
this.a=a;
}
publicAgetA() {
returna;
}
@Override
public<B>Slot<B>fmap(Function<?superA,?extendsB>fn) {
returnpure(fn.apply(a));
}
@Override
public<B>Slot<B>pure(Bb) {
returnnewSlot<>(b);
}
@Override
public<B>Slot<B>zip(Applicative<Function<?superA,?extendsB>,Slot>appFn) {
returnpure(appFn.<Slot<Function<?superA,?extendsB>>>coerce().getA().apply(getA()));
}
}
Fn2<Integer,Integer,Integer>add= (x,y) ->x+y;
Slot<Integer>x=newSlot<>(1);
Slot<Integer>y=newSlot<>(2);
Slot<Integer>z=y.zip(x.fmap(add));//-> Slot{a=3}
Examples of applicative functors include:
Fn*
,Semigroup
,andMonoid
SingletonHList
andTuple*
Choice*
Either
Const
,Identity
,andCompose
Lens
In addition to implementingfmap
fromFunctor
,implementing an applicative functor involves providing two methods:pure
,a method that lifts a value into the functor; andzip
,a method that applies a lifted function to a lifted value, returning a new lifted value. As usual, there aresome lawsthat should be adhered to.
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 toflatMap
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 usingEither
:
classPerson{
Optional<Occupation>occupation() {
returnOptional.empty();
}
}
classOccupation{
}
publicstaticvoidmain(String[]args) {
Fn1<String,Either<String,Integer>>parseId=str->Either.trying(() ->Integer.parseInt(str),__->str+"is not a valid id");
Map<Integer,Person>database=newHashMap<>();
Fn1<Integer,Either<String,Person>>lookupById=id->Either.fromOptional(Optional.ofNullable(database.get(id)),
() ->"No person found for id"+id);
Fn1<Person,Either<String,Occupation>>getOccupation=p->Either.fromOptional(p.occupation(), () ->"Person was unemployed");
Either<String,Occupation>occupationOrError=
parseId.apply("12")// Either<String, Integer>
.flatMap(lookupById)// Either<String, Person>
.flatMap(getOccupation);// Either<String, Occupation>
}
In the previous example, if any ofparseId
,lookupById
,orgetOccupation
fail, no furtherflatMap
computations can succeed, so the result short-circuits to the firstleft
value that is returned. This is completely predictable from the type signature ofMonad
andEither
:Either<L, R>
is aMonad<R>
,so the single arityflatMap
can have nothing to map in the case where there is noR
value. With experience, it generally becomes quickly clear what the logical behavior offlatMap
mustbe given the type signatures.
That's it. Monads are neitherelephantsnor are theyburritos;they're simply types that support a) the ability to lift a value into them, and b) a chaining functionflatMap:: (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 obeysthe laws), it is a monad.
Further, if a type is a monad, it is necessarily anApplicative
,which makes it necessarily aFunctor
,solambdaenforces this tautology via a hierarchical constraint.
Traversable functors -- functors that can be "traversed from left to right" -- are implemented via theTraversable
interface.
publicabstractclassMaybe<A>implementsTraversable<A,Maybe> {
privateMaybe() {
}
@Override
publicabstract<B,AppextendsApplicative>Applicative<Maybe<B>,App>traverse(
Function<?superA,?extendsApplicative<B,App>>fn,
Function<?superTraversable<B,Maybe>,?extendsApplicative<?extendsTraversable<B,Maybe>,App>>pure);
@Override
publicabstract<B>Maybe<B>fmap(Function<?superA,?extendsB>fn);
privatestaticfinalclassJust<A>extendsMaybe<A> {
privatefinalAa;
privateJust(Aa) {
this.a=a;
}
@Override
public<B,AppextendsApplicative>Applicative<Maybe<B>,App>traverse(
Function<?superA,?extendsApplicative<B,App>>fn,
Function<?superTraversable<B,Maybe>,?extendsApplicative<?extendsTraversable<B,Maybe>,App>>pure) {
returnfn.apply(a).fmap(Just::new);
}
@Override
public<B>Maybe<B>fmap(Function<?superA,?extendsB>fn) {
returnnewJust<>(fn.apply(a));
}
}
privatestaticfinalclassNothing<A>extendsMaybe<A> {
@Override
@SuppressWarnings("unchecked")
public<B,AppextendsApplicative>Applicative<Maybe<B>,App>traverse(
Function<?superA,?extendsApplicative<B,App>>fn,
Function<?superTraversable<B,Maybe>,?extendsApplicative<?extendsTraversable<B,Maybe>,App>>pure) {
returnpure.apply((Maybe<B>)this).fmap(x-> (Maybe<B>)x);
}
@Override
@SuppressWarnings("unchecked")
public<B>Maybe<B>fmap(Function<?superA,?extendsB>fn) {
return(Maybe<B>)this;
}
}
}
Maybe<Integer>just1=Maybe.just(1);
Maybe<Integer>nothing=Maybe.nothing();
Either<String,Maybe<Integer>>traversedJust=just1.traverse(x->right(x+1),empty->left("empty"))
.fmap(x-> (Maybe<Integer>)x)
.coerce();//-> Right(Just(2))
Either<String,Maybe<Integer>>traversedNothing=nothing.traverse(x->right(x+1),empty->left("empty"))
.fmap(x-> (Maybe<Integer>)x)
.coerce();//-> Left( "empty" )
Examples of traversable functors include:
SingletonHList
andTuple*
Choice*
Either
Const
andIdentity
LambdaIterable
for wrappingIterable
in an instance ofTraversable
In addition to implementingfmap
fromFunctor
,implementing a traversable functor involves providing an implementation oftraverse
.
As always, there aresome lawsthat should be observed.
Lambda also supports a few first-classalgebraic data types.
Maybe
is thelambdaanalog tojava.util.Optional
.It behaves in much of the same way asj.u.Optional
,except that it quite intentionally does not support the inherently unsafej.u.Optional#get
.
Maybe<Integer>maybeInt=Maybe.just(1);// Just 1
Maybe<String>maybeString=Maybe.nothing();// Nothing
Also, because it's alambdatype, it takes advantage of the full functor hierarchy, as well as some helpful conversion functions:
Maybe<String>just=Maybe.maybe("string");// Just "string"
Maybe<String>nothing=Maybe.maybe(null);// Nothing
Maybe<Integer>maybeX=Maybe.just(1);
Maybe<Integer>maybeY=Maybe.just(2);
maybeY.zip(maybeX.fmap(x->y->x+y));// Just 3
maybeY.zip(nothing());// Nothing
Maybe.<Integer>nothing().zip(maybeX.fmap(x->y->x+y));// Nothing
Either<String,Integer>right=maybeX.toEither(() ->"was empty");// Right 1
Either<String,Integer>left=Maybe.<Integer>nothing().toEither(() ->"was empty");// Left "was empty"
Maybe.fromEither(right);// Just 1
Maybe.fromEither(left);// Nothing
Finally, for compatibility purposes,Maybe
andj.u.Optional
can be trivially converted back and forth:
Maybe<Integer>just1=Maybe.just(1);// Just 1
Optional<Integer>present1=just1.toOptional();// Optional.of(1)
Optional<String>empty=Optional.empty();// Optional.empty()
Maybe<String>nothing=Maybe.fromOptional(empty);// Nothing
Note:One compatibility difference betweenj.u.Optional
andMaybe
is howmap
/fmap
behave regarding functions that returnnull
:j.u.Optional
re-wrapsnull
results frommap
operations in anotherj.u.Optional
,whereasMaybe
considers this to be an error, and throws an exception. The reasonMaybe
throws in this case is becausefmap
is not an operation to be called speculatively, and so any function that returnsnull
in the context of anfmap
operation is considered to be erroneous. Instead of callingfmap
with a function that might returnnull
,the function result should be wrapped in aMaybe
andflatMap
should be used, as illustrated in the following example:
Function<Integer,Object>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
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 forHList
prevents ill-typed expressions:
HCons<Integer,HCons<String,HNil>>hList=HList.cons(1,HList.cons("foo",HList.nil()));
System.out.println(hList.head());// prints 1
System.out.println(hList.tail().head());// prints "foo"
HNilnil=hList.tail().tail();
//nil.head() won't type-check
One of the primary downsides to usingHList
s in Java is how quickly the type signature grows.
To address this, tuples in lambda are specializations ofHList
s up to 8 elements deep, with added support for index-based accessor methods.
HNilnil=HList.nil();
SingletonHList<Integer>singleton=nil.cons(8);
Tuple2<Integer,Integer>tuple2=singleton.cons(7);
Tuple3<Integer,Integer,Integer>tuple3=tuple2.cons(6);
Tuple4<Integer,Integer,Integer,Integer>tuple4=tuple3.cons(5);
Tuple5<Integer,Integer,Integer,Integer,Integer>tuple5=tuple4.cons(4);
Tuple6<Integer,Integer,Integer,Integer,Integer,Integer>tuple6=tuple5.cons(3);
Tuple7<Integer,Integer,Integer,Integer,Integer,Integer,Integer>tuple7=tuple6.cons(2);
Tuple8<Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer>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:
SingletonHList<Integer>singleton=HList.singletonHList(1);
Tuple2<Integer,Integer>tuple2=HList.tuple(1,2);
Tuple3<Integer,Integer,Integer>tuple3=HList.tuple(1,2,3);
Tuple4<Integer,Integer,Integer,Integer>tuple4=HList.tuple(1,2,3,4);
Tuple5<Integer,Integer,Integer,Integer,Integer>tuple5=HList.tuple(1,2,3,4,5);
Tuple6<Integer,Integer,Integer,Integer,Integer,Integer>tuple6=HList.tuple(1,2,3,4,5,6);
Tuple7<Integer,Integer,Integer,Integer,Integer,Integer,Integer>tuple7=HList.tuple(1,2,3,4,5,6,7);
Tuple8<Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer>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:
HCons<Integer,HCons<String,HCons<Character,HNil>>>hList=cons(1,cons("2",cons('3',nil())));
HCons<Integer,Tuple2<String,Character>>tuple=tuple(1,"2",'3');
Tuple5<Integer,String,Character,Double,Boolean>longerHList=tuple(1,"2",'3',4.0d,false);
Index<Character,HCons<Integer,?extendsHCons<String,?extendsHCons<Character,?>>>>characterIndex=
Index.<Character>index().<String>after().after();
characterIndex.get(hList);// '3'
characterIndex.get(tuple);// '3'
characterIndex.get(longerHList);// '3'
characterIndex.set('4',hList);// HList{ 1:: "2":: '4' }
Finally, allTuple*
classes are instances of bothFunctor
andBifunctor
:
Tuple2<Integer,String>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<String,Boolean,Integer>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
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.
TypeSafeKey<String>stringKey=TypeSafeKey.typeSafeKey();
TypeSafeKey<Integer>intKey=TypeSafeKey.typeSafeKey();
HMaphmap=HMap.hMap(stringKey,"string value",
intKey,1);
Optional<String>stringValue=hmap.get(stringKey);// Optional[ "string value" ]
Optional<Integer>intValue=hmap.get(intKey);// Optional[1]
Optional<Integer>anotherIntValue=hmap.get(anotherIntKey);// Optional.empty
CoProduct
s generalize unions of disparate types in a single consolidated type, and theChoiceN
ADTs represent canonical implementations of these coproduct types.
CoProduct3<String,Integer,Character,?>string=Choice3.a("string");
CoProduct3<String,Integer,Character,?>integer=Choice3.b(1);
CoProduct3<String,Integer,Character,?>character=Choice3.c('a');
Rather than supporting explicit value unwrapping, which would necessarily jeopardize type safety,CoProduct
s support amatch
method that takes one function per possible value type and maps it to a final common result type:
CoProduct3<String,Integer,Character,?>string=Choice3.a("string");
CoProduct3<String,Integer,Character,?>integer=Choice3.b(1);
CoProduct3<String,Integer,Character,?>character=Choice3.c('a');
Integerresult=string.<Integer>match(String::length,identity(),Character::charCount);// 6
Additionally, because aCoProduct2<A, B,?>
guarantees a subset of aCoProduct3<A, B, C,?>
,thediverge
method exists betweenCoProduct
types of single magnitude differences to make it easy to use a more convergentCoProduct
where a more divergentCoProduct
is expected:
CoProduct2<String,Integer,?>coProduct2=Choice2.a("string");
CoProduct3<String,Integer,Character,?>coProduct3=coProduct2.diverge();// still just the coProduct2 value, adapted to the coProduct3 shape
There areCoProduct
andChoice
specializations for type unions of up to 8 different types:CoProduct2
throughCoProduct8
,andChoice2
throughChoice8
,respectively.
Either<L, R>
represents a specializedCoProduct2<L, R>
,which resolve to one of two possible values: a left value wrapping anL
,or a right value wrapping anR
(typically an exceptional value or a successful value, respectively).
As withCoProduct2
,rather than supporting explicit value unwrapping,Either
supports many useful comprehensions to help facilitate type-safe interactions:
Either<String,Integer>right=Either.right(1);
Either<String,Integer>left=Either.left("Head fell off");
Integerresult=right.orElse(-1);
//-> 1
List<Integer>values=left.match(l->Collections.emptyList(),Collections::singletonList);
//-> []
Check out the tests formore examplesof ways to interact withEither
.
Lambda also ships with a first-classlenstype, as well as a small library of useful general lenses:
Lens<List<String>,List<String>,Optional<String>,String>stringAt0=ListLens.at(0);
List<String>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
,andset
.As the name implies,view
andset
are used to retrieve values and store values, respectively, whereasover
is used to apply a function to the value a lens is focused on, alter it, and store it (you can think ofset
as a specialization ofover
usingconstantly
).
Lenses can be easily created. Consider the followingPerson
class:
publicfinalclassPerson{
privatefinalintage;
publicPerson(intage) {
this.age=age;
}
publicintgetAge() {
returnage;
}
publicPersonsetAge(intage) {
returnnewPerson(age);
}
publicPersonsetAge(LocalDatedob) {
returnsetAge((int)YEARS.between(dob,LocalDate.now()));
}
}
...and a lens for getting and settingage
as anint
:
Lens<Person,Person,Integer,Integer>ageLensWithInt=Lens.lens(Person::getAge,Person::setAge);
//or, when each pair of type arguments match...
Lens.Simple<Person,Integer>alsoAgeLensWithInt=Lens.simpleLens(Person::getAge,Person::setAge);
If we wanted a lens for theLocalDate
version ofsetAge
,we could use the same method references and only alter the type signature:
Lens<Person,Person,Integer,LocalDate>ageLensWithLocalDate=Lens.lens(Person::getAge,Person::setAge);
Compatible lenses can be trivially composed:
Lens<List<Integer>,List<Integer>,Optional<Integer>,Integer>at0=ListLens.at(0);
Lens<Map<String,List<Integer>>,Map<String,List<Integer>>,List<Integer>,List<Integer>>atFoo=MapLens.atKey("foo",emptyList());
view(atFoo.andThen(at0),singletonMap("foo",asList(1,2,3)));// Optional[1]
Lens provides independentmap
operations for each parameter, so incompatible lenses can also be composed:
Lens<List<Integer>,List<Integer>,Optional<Integer>,Integer>at0=ListLens.at(0);
Lens<Map<String,List<Integer>>,Map<String,List<Integer>>,Optional<List<Integer>>,List<Integer>>atFoo=MapLens.atKey("foo");
Lens<Map<String,List<Integer>>,Map<String,List<Integer>>,Optional<Integer>,Integer>composed=
atFoo.mapA(optL->optL.orElse(singletonList(-1)))
.andThen(at0);
view(composed,singletonMap("foo",emptyList()));// Optional.empty
Check out the tests or thejavadocfor more info.
Wherever possible,lambdamaintains interface compatibility with similar, familiar core Java types. Some examples of where this works well is with bothFn1
andPredicate
,which extendj.u.f.Function
andj.u.f.Predicate
,respectively. In these examples, they also override any implemented methods to return theirlambda-specific counterparts (Fn1.compose
returningFn1
instead ofj.u.f.Function
as an example).
Unfortunately, due to Java's type hierarchy and inheritance inconsistencies, this is not always possible. One surprising example of this is howFn1
extendsj.u.f.Function
,butFn2
does not extendj.u.f.BiFunction
.This is becausej.u.f.BiFunction
itself does not extendj.u.f.Function
,but it does define methods that collide withj.u.f.Function
.For this reason, bothFn1
andFn2
cannot extend their Java counterparts without sacrificing their own inheritance hierarchy. These types of asymmetries are, unfortunately, not uncommon; however, wherever these situations arise, measures are taken to attempt to ease the transition in and out of core Java types (in the case ofFn2
,a supplemental#toBiFunction
method is added). I do not take these inconveniences for granted, and I'm regularly looking for ways to minimize the negative impact of this as much as possible. Suggestions and use cases that highlight particular pain points here are particularly appreciated.
These are officially supported libraries that extend lambda's core functionality and are developed under the same governance and processes as lambda.
- Shōki- Purely functional, persistent data structures for the JVM
These are open-sourced community projects that rely onlambdafor significant functionality, but are not necessarily affiliated with lambda and have their own separate maintainers. If you uselambdain your own open-sourced project, feel free to create an issue and I'll be happy to review the project and add it to this section!
- Enhanced Iterables- Kevin Schuetz@kschuetz
- Collection Views- Kevin Schuetz@kschuetz
- WuWei- Michael Anderson@nomicflux-
ST
monad for safe mutability - Kraftwerk- Kevin Schuetz@kschuetz- random data generators and combinators
lambdais part ofpalatable,which is distributed underThe MIT License.