Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OOP and FP: become a better programmer

OOP and FP: become a better programmer

Object Oriented Programming dominated the software development landscape in the last decade of XX century and in the first of XXI. In more recent years, even as a consequence of the advent of multicore CPUs, Functional Programming and its principles started attracting more interest, becoming at least equally relevant in our industry. The biggest mistake made by programmers nowadays is considering OOP and FP as two mutually exclusive paradigms. This misconception is also the product of a misunderstanding about what OOP actually means; spoiler: it hasn't anything to do with inheritance. In reality many functional constructs (e.g. pattern matching) are just the dual of typical OOP patterns (e.g. visitor) with pros and cons mostly dependant on the context. The final purpose of this talk is twofold: debunking the misconceptions on OOP and showing that OOP and FP are actually complementary techniques that can happily coexist in the same codebase. It is the duty of experienced developers to fill their toolbox with both OOP and FP tools and to know from time to time how to choose and employ the tool that is the best fit for the problem at hand.

Mario Fusco

May 08, 2022
Tweet

More Decks by Mario Fusco

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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; }
  8. 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
  9. 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
  10. 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 !?!
  11. 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.
  12. 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); }
  13. Simone Bordet Mario Fusco Example #3, v2 Map<Integer, List<Student>> studentByGradYear

    = students.stream() .collect(groupingBy(student::getGradYear));
  14. 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(); }
  15. Simone Bordet Mario Fusco Example #4, v2 List<String> errors =

    Files.lines(Paths.get(fileName)) .filter(l -> l.startsWith("ERROR")) .limit(40) .collect(toList());
  16. 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());
  17. 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(); }
  18. 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
  19. 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());
  20. 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); }); }
  21. 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 !
  22. 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));
  23. 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));
  24. 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 { }
  25. 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; }
  26. 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 { }
  27. 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
  28. 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. } }
  29. 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
  30. 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
  31. 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.