Download - OOP and FP: become a better programmer - Simone Bordet, Mario Fusco - Codemotion Rome 2015
Simone Bordet
Mario Fusco
OOP and FP
Become aBetter Programmer
Our definitionOf OOP and FP
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
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
Goals
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
Example #1“accumulator”
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
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
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
Example #2“what do you do ?”
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;
}
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
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
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 !?!
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.
Example #3“let's group them”
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);
}
Simone Bordet
Mario Fusco
Example #3, v2
Map<Integer, List<Student>> studentByGradYear =
students.stream()
.collect(groupingBy(student::getGradYear));
Example #4“separation of concerns”
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();
}
Simone Bordet
Mario Fusco
Example #4, v2
List<String> errors = Files.lines(Paths.get(fileName))
.filter(l -> l.startsWith("ERROR"))
.limit(40)
.collect(toList());
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());
Example #5“grep -B 1”
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();
}
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
Simone Bordet
Mario Fusco
Example #5, v2
Files.lines(Paths.get(filename))
.reduce(new LinkedList<String[]>(),
(list, line) -> {
if (!list.isEmpty())
list.getLast()[1] = line;
list.offer(new String[]{line, null});
return list;
},
(l1, l2) -> {
l1.getLast()[1] = l2.getFirst()[0];
l1.addAll(l2); return l1;
}).stream()
.filter(ss -> ss[1] != null && ss[1].startsWith("ERROR"))
.collect(Collectors.toList());
Example #6“callback hell”
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);
});
}
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 !
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));
Simone Bordet
Mario Fusco
Example #6, v3
List<CompletableFuture<SearchEngineResult>> results =
engines.stream()
.map(e ->
new Pair<>(e, http.newRequest(e.url("codemotion"))))
.map(p ->
p._2.thenCombine(response -> p._1.parse(response.getContentAsString())))
.collect(toList());
CompletableFuture.supplyAsync(() -> results.stream()
.map(future -> future.join())
.collect(toList()))
.thenApply(list -> lastAction.perform(list));
Example #7“statefulness”
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 {
}
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;
}
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 { }
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
Example #8“encapsulation”
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.
}
}
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
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
Conclusions
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.
Simone Bordet
Mario Fusco
Questions&
Answers