Upload
mario-fusco
View
4.884
Download
4
Tags:
Embed Size (px)
Citation preview
FP in JavaProject Lambda and beyond
by Mario [email protected]: @mariofusco
Project Lambda – A Brief History
• 2006 – Gosling: "We will never have closures in Java"
• 2007 – 3 different proposals for closures in Java
• 2008 – Reinhold: "We will never have closures in Java"
• 2009 – Start of project Lambda (JSR 335)
public boolean javaWillHaveClosure() {
return currentYear % 2 == 1;
}
From Single Method Interfaces …
public interface Comparator<T> {
int compare(T o1, T o2);
}
Collections.sort(strings, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
});
� Bulky syntax
� Confusion surrounding the meaning of names and this
� Inability to capture non-final local variables
� Inability to abstract over control flow
Functional
Interface
… To Lambda Expressions
Collections.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
Lambda expression are always converted to
instance of a functional interface
Compiler figures out the types
No need of changing the JVM to create a new
type for lambda expressions
Anatomy of a lambda expression
s -> s.length()
(int x, int y) -> x + y
() -> 42
(x, y, z) -> {
if (x) {
return y;
} else {
return z;
}
}
The formal parameters of
a lambda expression may
have either inferred or
declared types
A lambda expression is like a method: it provides a list of formal parameters and a body
A lambda body is either a
single expression or a block
Return is implicit and
can be omitted
Common JDK8 functional interfaces
� Predicate � a property of the object passed as argument
� Block � an action to be performed with the object passed as argument
� Function � transform a T to a U
� BiFunction � transform a (T, U) to a V
� Supplier � provide an instance of a T (such as a factory)
� UnaryOperator � a unary operator from T -> T
� BinaryOperator � a binary operator from (T, T) -> T
Why Lambdas?
�API designers can build more powerful, expressive APIs
�More room for generalization
o Pass behaviors to a method together with normal data
� Libraries remain in control of computation
o e.g. internal vs. external iteration
�More opportunities for optimization
o Laziness
o Parallelism
o Out-of-order execution
�More regular and then more readable code
o e.g. nested loops vs. pipelined (fluent) operations
�Better composability and reusability
An Example: Sorting
Comparator<Person> byAge = new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.getAge() – p2.getAge();
}
};
Collections.sort(people, byAge);
Comparator<Person> byAge = (p1, p2) -> p1.getAge() – p2.getAge();
Functional interface
Lambda expression
Collections.sort(people, (p1, p2) -> p1.getAge() – p2.getAge());
Can We Do Better?
Collections.sort(people, comparing(Person::getAge));
Method reference
Comparator<Person> byAge = Comparators.comparing(p -> p.getAge());
Comparator<Person> byAge = Comparators.comparing(Person::getAge);
Collections.sort(people, comparing(Person::getAge)
.compose(comparing(Person::getName)));
Collections.sort(people, comparing(Person::getAge).reverse());
Reusability
Readability
Composability
Extension methods
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void forEach(Block<? super E> block) {
while (hasNext())
block.accept(next());
}
}
� Add methods to existing interfaces without breaking the backward compatibility
� Primary goal is API evolution, but useful as an inheritance mechanism on its own
� Add multiple inheritance of behavior to the always existed multiple inheritance of
type, but no multiple inheritance of state
Extension methods
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void forEach(Block<? super E> block) {
while (hasNext())
block.accept(next());
}
}
� Add methods to existing interfaces without breaking the backward compatibility
� Primary goal is API evolution, but useful as an inheritance mechanism on its own
� Add multiple inheritance of behavior to the always existed multiple inheritance of
type, but no multiple inheritance of state
� Can be used to declare “optional” methods
default void remove() { throw new UnsupportedOperationException(); }
Internal VS External iteration
for (Employee e : employees) {
e.setSalary(e.getSalary() * 1.03);
}
employees.forEach(e -> e.setSalary(e.getSalary() * 1.03));
Inherently serial
Client has to manage iteration
Nested loops are poorly readable
+ Library is in control � opportunity for internal optimizations as parallelization,
lazy evaluation, out-of-order execution
+ More what, less how � better readability
+ Fluent (pipelined) operations � better readability
+ Client can pass behaviors into the API as data �
possibility to abstract and generalize over behavior �
more powerful, expressive APIs
Not only a syntactic change!
Streams - Efficiency with laziness
� Represents a stream of values
� Not a data structure: doesn't store values
� Source can be Collection, array, generating function, I/O ....
� Encourages a pipelined ( "fluent" ) usage style
� Operations are divided between intermediate and terminal
� Lazy in nature: only terminal operations actually trigger a computation
employees.stream()
.filter(e -> e.getIncome() > 50000)
.map(e -> e.getName())
.forEach(System.out::println);
Streams - Efficiency with laziness
� Represents a stream of values
� Not a data structure: doesn't store values
� Source can be Collection, array, generating function, I/O ....
� Encourages a pipelined ( "fluent" ) usage style
� Operations are divided between intermediate and terminal
� Lazy in nature: only terminal operations actually trigger a computation
� Also available a parallel stream (using the Fork/Join framework)
employees.stream()
.filter(e -> e.getIncome() > 50000)
.map(e -> e.getName())
.forEach(System.out::println);
parallel()
So we have lambdas in Java …
… now what?
The OOP/FP dualism - OOPpublic class Bird { }
public class Cat {
private Bird catch;
private boolean full;
public void capture(Bird bird) {
catch = bird;
}
public void eat() {
full = true;
catch = null;
}
}
Cat cat = new Cat();
Bird bird = new Bird();
cat.capture(bird);
cat.eat();
The story
The OOP/FP dualism - FPpublic class Bird { }
public class Cat {
public CatWithCatch capture(Bird bird) { return new CatWithCatch(bird); }
}
public class CatWithCatch {
private final Bird catch;
public CatWithCatch(Bird bird) { catch = bird; }
public FullCat eat() { return new FullCat(); }
}
public class FullCat { }
BiFunction<Cat, Bird, FullCat> story =
((BiFunction<Cat, Bird, CatWithCatch>)Cat::capture)
.compose(CatWithCatch::eat);
FullCat fullCat = story.apply( new Cat(), new Bird() );
Immutability
Emphasis on verbs
instead of names
No need to test internal state: correctness enforced by the compiler
Better Logging with Lambdas
if (log.isDebugEnabled()) {
log.debug("The answer is " + answer);
}
Can we delay the String creation and execute it only when
strictly necessary without (explicitly) using an if?
log.debug(()-> "The answer is " + answer);
public void debug(Callable<String> lambda) {
if (isDebugEnabled()) {
debug(lambda.call());
}
}
Invokes answer.toString() and does the Strings
concatenation even when not necessary
Side-effect isolation � Reusabilityclass Player {
String name;
int score;
}
public void declareWinner(Player p) {
System.out.println(p.name + " wins!");
}
public void winner(Player p1, Player p2) {
if (p1.score > p2.score) declareWinner(p1)
else declareWinner(p2);
}
Side-effect isolation � Reusabilityclass Player {
String name;
int score;
}
public void declareWinner(Player p) {
System.out.println(p.name + " wins!");
}
public void winner(Player p1, Player p2) {
if (p1.score > p2.score) declareWinner(p1)
else declareWinner(p2);
}
public Player maxScore(Player p1, Player p2) {
return p1.score > p2.score ? p1 : p2;
}
public void winner(Player p1, Player p2) {
declareWinner(maxScore(p1, p2));
}
Separate
computational logic
from side effects
Side-effect isolation � Reusabilityclass Player {
String name;
int score;
}
public void declareWinner(Player p) {
System.out.println(p.name + " wins!");
}
public void winner(Player p1, Player p2) {
if (p1.score > p2.score) declareWinner(p1)
else declareWinner(p2);
}
public Player maxScore(Player p1, Player p2) {
return p1.score > p2.score ? p1 : p2;
}
public void winner(Player p1, Player p2) {
declareWinner(maxScore(p1, p2));
}
Separate
computational logic
from side effects
declareWinner(players.stream().reduce(this::maxScore).get())
reuse maxScore as a BinaryOperator
to compute the winner among a list of players
Using Streamspublic boolean isPrimeImperative(int number) {
if (number < 2) {
return false;
}
for (int i = 2; i < (int) Math.sqrt(number) + 1; i++) {
if ( number % i == 0 ) {
return false;
}
}
return true;
}
public boolean isPrimeFunctional(int number) {
return number > 1 &&
Streams.intRange(2, (int) Math.sqrt(number) + 1)
.noneMatch(divisor -> number % divisor == 0);
}
Working with infinite Streams
Stream<Integer> integers = Streams.iterate(1, i -> i + 1);
int[] result = integers.map(i -> i ^ 2).limit(25).toArray();
take 25 (map (^2) [1..])
(take 25 (squares-of (integers)))
Stream.from(1).map(_ ^ 2).take(25).toArray
Null references? No, Thanks
� Errors source � NPE is by far the most common exception in Java
� Bloatware source � Worsen readability by making necessary to fill our
code with null checks
� Meaningless � Don't have any semantic meaning and in particular are the
wrong way to model the absence of a value in a statically typed language
� Breaks Java philosophy � Java always hides pointers to developers, except
in one case: the null pointer
� A hole in the type system � Null has the bottom type, meaning that it can
be assigned to any reference type: this is a problem because, when
propagated to another part of the system, you have no idea what that null
was initially supposed to be
Tony Hoare, who invented the null reference in 1965 while working on
an object oriented language called ALGOL W, called its invention his
“billion dollar mistake”
Options: the functional alternativepublic abstract class Option<A> implements Iterable<A> {
private Option() { }
public abstract <B> Option<B> map(Function<A, B> mapper);
public abstract <B> Option<B> flatMap(Function<A, Option<B>> mapper);
public abstract Option<A> filter(Predicate<A> predicate);
public abstract A getOrElse(A def);
public abstract boolean isDefined();
public static <A> Some<A> some(A value) {
if (value == null) throw new NullPointerException();
return new Some<A>(value);
}
public static <A> None<A> none() { return None.NONE; }
public static <A> Option<A> option(A value) {
return value == null ? none() : some(value);
}
public static final class None<A> extends Option<A> { ... }
public static final class Some<A> extends Option<A> { ... }
}
Somepublic static final class Some<A> extends Option<A> {
private final A value;
private Some(A value) { this.value = value; }
public <B> Option<B> map(Function<A, B> mapper) {
return some( mapper.apply(value) );
}
public <B> Option<B> flatMap(Function<A, Option<B>> mapper) {
return (Option<B>) mapper.apply(value);
}
public Option<A> filter(Predicate<? super A> predicate) {
return predicate.test(value)) ? this : None.NONE;
}
public A getOrElse(A def) { return value; }
public boolean isDefined() { return false; }
}
Nonepublic static final class None<A> extends Option<A> {
public static final None NONE = new None();
private None() { }
public <B> Option<B> map(Function<A, B> mapper) { return NONE; }
public <B> Option<B> flatMap(Function<A, Option<B>> mapper) {
return NONE;
}
public Option<A> filter(Predicate<A> predicate) { return NONE; }
public A getOrElse(A def) { return def; }
public boolean isDefined() { return false; }
}
Example: if the value associated with a given key
is a String representing a positive integer returns
that integer, but returns zero in all other case@Test
public void testReturnPositiveIntegersOrZero() {
Map<String, String> param = new HashMap<String, String>();
param.put("a", "5");
param.put("b", "true");
param.put("c", "-3");
// the value of the key "a" is a String representing a
// positive int so return it
assertEquals(5, readPositiveIntParam(param, "a"));
// returns zero since the value of the key "b" is not an int
assertEquals(0, readPositiveIntParam(param, "b"));
// returns zero since the value of the key "c" is a negative int
assertEquals(0, readPositiveIntParam(param, "c"));
// returns zero since there is no key "d" in the map
assertEquals(0, readPositiveIntParam(param, "d"));
}
Null vs. Optionint readPositiveIntParam(Map<String, String> params, String name) {
String value = params.get(name);
if (value == null) return 0;
int i = 0;
try {
i = Integer.parseInt(value);
} catch (NumberFormatException e) { }
return i < 0 ? 0 : i;
}
int readPositiveIntParam(Map<String, String> params, String name) {
return asOption(params.get(name))
.flatMap(s -> {
try { return some(Integer.parseInt(s)); }
catch (NumberFormatException e) { return none(); }
})
.filter(i -> i > 0)
.getOrElse(0);
}
Exceptions? Yes, but …� Often abused, especially for flow control
� Checked Exceptions harm API extensibility/modificability
� Not composable: in presence of multiple errors only the first one is
reported
� In the end just a GLORIFIED MULTILEVEL GOTO
Exceptions? Yes, but …� Often abused, especially for flow control
� Checked Exceptions harm API extensibility/modificability
� Not composable: in presence of multiple errors only the first one is
reported
� In the end just a GLORIFIED MULTILEVEL GOTO
Either/Validation: the functional alternative
� The functional way of returning a value which can actually be one of two
values: the error/exception (Left) or the correct value (Right)
Validation<Exception, Value>
Exceptions? Yes, but …� Often abused, especially for flow control
� Checked Exceptions harm API extensibility/modificability
� Not composable: in presence of multiple errors only the first one is
reported
� In the end just a GLORIFIED MULTILEVEL GOTO
Either/Validation: the functional alternative
� The functional way of returning a value which can actually be one of two
values: the error/exception (Left) or the correct value (Right)
� Composable: can accumulate multiple errors
Validation<Exception, Value>Validation<List<Exception>, Value>
SalaryCalculatorpublic class SalaryCalculator {
// B = basic + 20%
public double plusAllowance(double d) { return d * 1.2; }
// C = B + 10%
public double plusBonus(double d) { return d * 1.1; }
// D = C - 30%
public double plusTax(double d) { return d * 0.7; }
// E = D - 10%
public double plusSurcharge(double d) { return d * 0.9; }
public double calculate(double basic, boolean... bs) {
double salary = basic;
if (bs[0]) salary = plusAllowance(salary);
if (bs[1]) salary = plusBonus(salary);
if (bs[2]) salary = plusTax(salary);
if (bs[3]) salary = plusSurcharge(salary);
return salary;
}
}
Endomorphisms & Monoids
interface Endomorphism<A> extends Function<A, A> { }
interface Monoid<A> {
A append(A a1, A a2);
A zero();
}
interface EndoMonoid<A> extends Monoid<Endomorphism<A>> {
@Override
default Endomorphism<A> append(Endomorphism<A> a1, Endomorphism<A> a2) {
return (A a) -> a2.apply(a1.apply(a));
}
@Override
default Endomorphism<A> zero() {
return a -> a;
}
}
FluentEndoMonoidpublic class FluentEndoMonoid<A> implements EndoMonoid<A> {
private final Endomorphism<A> endo;
public FluentEndoMonoid(Endomorphism<A> endo) { this.endo = endo; }
public FluentEndoMonoid(Endomorphism<A> endo, boolean b) {
this.endo = b ? endo : zero();
}
public FluentEndoMonoid<A> add(Endomorphism<A> other) {
return new FluentEndoMonoid<A>(append(endo, other));
}
public FluentEndoMonoid<A> add(Endomorphism<A> other, boolean b) {
return add(b ? other : zero());
}
public Endomorphism<A> get() { return endo; }
public static <A> FluentEndoMonoid<A> endo(Endomorphism<A> f, boolean b) {
return new FluentEndoMonoid<A>(f, b);
}
}
Functional SalaryCalculator
public class SalaryCalculator {
public double calculate(double basic, boolean... bs) {
return endo((Endomorphism<Double>) this::plusAllowance, bs[0])
.add(this::plusBonus, bs[1])
.add(this::plusTax, bs[2])
.add(this::plusSurcharge, bs[3])
.get()
.apply(basic);
}
}
The bottom line
Java is getting functional
EMBRACE IT!
References
Mario Fusco
Red Hat – Senior Software Engineer
twitter: @mariofusco
Q A
Thanks … Questions?