The story of Simon, an experienced OOP Java developer, exposed to the new lambda features of JDK 8. His friend Mario, a long-bearded FP geek, will try to convince him that FP can help him develop more readable and maintainable code. A journey into the discovery of the main new feature - lambda expressions - of JDK 8
3. Simone Bordet
Mario Fusco
Our Definition of OOP
In this presentation “OOP” will mean:
Idiomatic Java 7 programming style:
Use of mutable variables and state
Use of classes and void methods
Use of external iteration (for loops)
Use of threads
4. Simone Bordet
Mario Fusco
Our definition of FP
In this presentation “FP” will mean:
Java 8 programming style, with:
Immutable variables and state
Use classes, but avoid void methods
Internal iteration via Stream
CompletableFuture, not Thread
6. Simone Bordet
Mario Fusco
Goals
This session is about OOP and FP
NOT about OOP versus FP
We want to show that in order to be a better
programmer, you have to know both
In some cases it's better to apply one paradigm
In other cases it's better to apply the other
We will hint at some guideline that helps deciding
8. Simone Bordet
Mario Fusco
Example #1, v1
public String sum(List<Student> students) {
StringBuilder sb = new StringBuilder();
for (Student s : students)
sb.append(s.getName()).append(“, “);
return sb.toString();
}
OOP style
External iteration
Mutable variables
9. Simone Bordet
Mario Fusco
Example #1, v2
public String sum(List<Student> students) {
StringBuilder sb = new StringBuilder();
students.stream()
.forEach(s -> sb.append(s.getName()).append(“, “));
return sb.toString();
}
BAD style
Use of mutable accumulator
10. Simone Bordet
Mario Fusco
Example #1, v3
public String sum(List<Student> students) {
String names = students.stream()
.map(s -> s.getName() + “, “)
.reduce(“”, (a, b) -> a + b);
return names;
}
FP style
Internal iteration
Mapping input to output
No mutable variables
12. Simone Bordet
Mario Fusco
Example #2, v1
What does this code do ?
List<Student> students = ...;
int min = 0;
for (Student s : students) {
if (s.getGradYear() != 2014)
continue;
int score = s.getGradScore();
if (score > min)
min = score;
}
13. Simone Bordet
Mario Fusco
Example #2, v2
Calculates the max, not the min !
And only for 2014 students !
List<Student> students = ...;
students.stream()
.filter(s -> s.getGradYear() == 2014)
.mapToInt(Student::getScore)
.max();
Somehow clearer to read
Less possibility of mistakes
max() is a method, not a variable name
14. Simone Bordet
Mario Fusco
Example #2, v3
But how do you get 2 results iterating once ?
List<Student> students = ...;
int min = Integer.MAX_VALUE, max = 0;
for (Student s : students) {
int score = s.getGradScore();
min = Math.min(min, score);
max = Math.max(max, score);
}
Easy and readable
15. Simone Bordet
Mario Fusco
Example #2, v4
FP version:
Pair<Integer, Integer> result = students.stream()
.map(s -> new Pair<>(s.getScore(), s.getScore()))
.reduce(new Pair<>(Integer.MAX_VALUE, 0), (acc,elem)-> {
new Pair<>(Math.min(acc._1, elem._1),
Math.max(acc._2, elem._2))
});
What !?!
16. Simone Bordet
Mario Fusco
Example #2, v5
How about parallelizing this ?
Pair<Integer, Integer> result = students.stream().parallel()
.map(s -> new Pair<>(s.getScore(), s.getScore()))
.reduce(new Pair<>(Integer.MAX_VALUE, 0), (acc,elem)-> {
new Pair<>(Math.min(acc._1, elem._1),
Math.max(acc._2, elem._2))
});
Neat, but .parallel() can only be used under very
strict conditions.
18. Simone Bordet
Mario Fusco
Example #3, v1
Group students by their graduation year
Map<Integer, List<Student>> studentByGradYear = new HashMap<>();
for (Student student : students) {
int year = student.getGradYear();
List<Student> list = studentByGradYear.get(year);
if (list == null) {
list = new ArrayList<>();
studentByGradYear.put(year, list);
}
list.add(student);
}
19. Simone Bordet
Mario Fusco
Example #3, v2
Map<Integer, List<Student>> studentByGradYear =
students.stream()
.collect(groupingBy(student::getGradYear));
21. Simone Bordet
Mario Fusco
Example #4, v1
Read first 40 error lines from a log file
List<String> errorLines = new ArrayList<>();
int errorCount = 0;
BufferedReader file = new BufferedReader(...);
String line = file.readLine();
while (errorCount < 40 && line != null) {
if (line.startsWith("ERROR")) {
errorLines.add(line);
errorCount++;
}
line = file.readLine();
}
22. Simone Bordet
Mario Fusco
Example #4, v2
List<String> errors = Files.lines(Paths.get(fileName))
.filter(l -> l.startsWith("ERROR"))
.limit(40)
.collect(toList());
23. Simone Bordet
Mario Fusco
Example #4
List<String> errorLines = new ArrayList<>();
int errorCount = 0;
BufferedReader file = new BufferedReader(new FileReader(filename));
String line = file.readLine();
while (errorCount < 40 && line != null) {
if (line.startsWith("ERROR")) {
errorLines.add(line);
errorCount++;
}
line = file.readLine();
}
return errorLines;
return Files.lines(Paths.get(fileName))
.filter(l -> l.startsWith("ERROR")
.limit(40)
.collect(toList());
25. Simone Bordet
Mario Fusco
Example #5, v1
Find lines starting with “ERROR” and previous line
List<String> errorLines = new ArrayList<>();
String previous = null;
String current = reader.readLine();
while (current != null) {
if (current.startsWith("ERROR")) {
if (previous != null)
errorLines.add(previous);
errorLines.add(current);
}
previous = current;
current = reader.readLine();
}
26. Simone Bordet
Mario Fusco
Example #5, v2
Not easy – immutability is now an obstacle
Must read the whole file in memory
This does not work:
Stream.generate(() -> reader.readLine())
readLine() throws and can't be used in lambdas
29. Simone Bordet
Mario Fusco
Example #6, v1
Find a term, in parallel, on many search engines, then execute
an action
final List<SearchEngineResult> result =
new CopyOnWriteArrayList<>();
final AtomicInteger count = new AtomicInteger(engines.size());
for (Engine e : engines) {
http.newRequest(e.url("codemotion")).send(r -> {
String c = r.getResponse().getContentAsString();
result.add(e.parse(c));
boolean finished = count.decrementAndGet() == 0;
if (finished)
lastAction.perform(result);
});
}
30. Simone Bordet
Mario Fusco
Example #6, v1
Code smells
Mutable concurrent accumulators: result and count
Running the last action within the response callback
What if http.newRequest() returns a CompletableFuture ?
Then I would be able to compose those futures !
Let's try to write it !
31. Simone Bordet
Mario Fusco
Example #6, v2
CompletableFuture<List<SearchEngineResult>> result =
CompletableFuture.completed(new CopyOnWriteArrayList<>());
for (Engine e : engines) {
CompletableFuture<Response> request =
http.sendRequest(e.url("codemotion"));
result = result.thenCombine(request, (list, response) -> {
String c = response.getContentAsString();
list.add(e.parse(c));
return list;
});
}
result.thenAccept(list -> lastAction.perform(list));
34. Simone Bordet
Mario Fusco
Example #7, v1
class Cat {
private Bird prey;
private boolean full;
void chase(Bird bird) { prey = bird; }
void eat() { prey = null; full = true; }
boolean isFull() { return full; }
}
class Bird {
}
35. Simone Bordet
Mario Fusco
Example #7, v1
It is not evident how to use it:
new Cat().eat() ???
The use case is instead:
Cat useCase(Cat cat, Bird bird) {
cat.chase(bird);
cat.eat();
assert cat.isFull();
return cat;
}
36. Simone Bordet
Mario Fusco
Example #7, v2
How about we use types to indicate state ?
class Cat {
CatWithPrey chase(Bird bird) {
return new CatWithPrey(bird);
}
}
class CatWithPrey {
private final Bird prey;
public CatWithPrey(Bird bird) { prey = bird; }
FullCat eat() { return new FullCat(); }
}
class FullCat { }
37. Simone Bordet
Mario Fusco
Example #7, v2
Now it is evident how to use it:
FullCat useCase(Cat cat, Bird bird) {
return cat.chase(bird).eat();
}
BiFunction<Cat, Bird, CatWithPrey> chase = Cat::chase;
BiFunction<Cat, Bird, FullCat> useCase =
chase.andThen(CatWithPrey::eat);
More classes, but clearer semantic
39. Simone Bordet
Mario Fusco
Example #8, v1
interface Shape2D {
Shape2D move(int deltax, int deltay)
}
class Circle implements Shape {
private final Point center;
private final int radius;
Circle move(int deltax, int deltay) {
// translate the center
}
}
class Polygon implements Shape {
private final Point[] points;
Polygon move(int deltax, int deltay) {
// Translate each point.
}
}
40. Simone Bordet
Mario Fusco
Example #8, v1
for (Shape shape : shapes)
shape.move(1, 2);
How do you do this using an FP language ?
What is needed is dynamic polymorphism
Some FP language does not have it
Other FP languages mix-in OOP features
41. Simone Bordet
Mario Fusco
Example #8, v2
defn move [shape, deltax, deltay] (
// Must crack open shape, then
// figure out what kind of shape is
// and then translate only the points
)
OOP used correctly provides encapsulation
FP must rely on OOP features to provide the same
Data types are not enough
Pattern matching is not enough
Really need dynamic polimorphism
43. Simone Bordet
Mario Fusco
Conclusions
If you come from an OOP background
Study FP
If you come from an FP background
Study OOP
Poly-paradigm programming is more generic,
powerful and effective than polyglot programming.