- Notifications
You must be signed in to change notification settings - Fork0
Functional patterns for Java
License
BackendJava/lambda
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Functional patterns for Java 8
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>1.5.4</version></dependency>
build.gradle
(Gradle):
compilegroup:'com.jnape.palatable',name:'lambda',version:'1.5.4'
First, the obligatorymap
/filter
/reduce
example:
IntegersumOfEvenIncrements =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 iscurried, so we could have also done this:
Fn1<Iterable<Integer>,Integer>sumOfEvenIncrementsFn =map((Integerx) ->x +1) .then(filter(x ->x %2 ==0)) .then(reduceLeft((x,y) ->x +y));IntegersumOfEvenIncrements =sumOfEvenIncrementsFn.apply(asList(1,2,3,4,5));//-> 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 ?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:
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.then(subtract);// same asFn1<Integer,Integer>alsoNoOp =subtract.compose(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(10,inGroupsOf(3,unfoldr(x ->Optional.of(tuple(x *3,x +1)),1)));//-> [[3, 6, 9], [12, 15, 18], [21, 24, 27]]
Check out thetests orjavadoc for more examples.
In addition to the functions above, lambda also supports a few first-classalgebraic data types.
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 1System.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 5 elements deep, with added support for index-based accessor methods.
HNilnil =HList.nil();SingletonHList<Integer>singleton =nil.cons(5);Tuple2<Integer,Integer>tuple2 =singleton.cons(4);Tuple3<Integer,Integer,Integer>tuple3 =tuple2.cons(3);Tuple4<Integer,Integer,Integer,Integer>tuple4 =tuple3.cons(2);Tuple5<Integer,Integer,Integer,Integer,Integer>tuple5 =tuple4.cons(1);System.out.println(tuple2._1());// prints 4System.out.println(tuple5._5());// prints 5
Additionally,HList
provides convenience static factory methods for directly constructing lists of up to 5 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);
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 2System.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 falseSystem.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.
CoProduct3<String,Integer,Character>string =CoProduct3.a("string");CoProduct3<String,Integer,Character>integer =CoProduct3.b(1);CoProduct3<String,Integer,Character>character =CoProduct3.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 =CoProduct3.a("string");CoProduct3<String,Integer,Character>integer =CoProduct3.b(1);CoProduct3<String,Integer,Character>character =CoProduct3.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 =CoProduct2.a("string");CoProduct3<String,Integer,Character>coProduct3 =coProduct2.diverge();// still just the coProduct2 value, adapted to the coProduct3 shape
There areCoProduct
specializations for type unions of up to 5 different types:CoProduct2
throughCoProduct5
, 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);//-> 1List<Integer>values =left.match(l ->Collections.emptyList(),Collections::singletonList);//-> []
Check out the tests formore examples of ways to interact withEither
.
Lambda also ships with a first-classlens type, 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 thejavadoc for more info.
Wherever possible,lambda maintains 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.
lambda is part ofpalatable, which is distributed underThe MIT License.
About
Functional patterns for Java
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Languages
- Java100.0%