🏋️‍♂️ Refactoring sans les for

🏋️‍♂️ Refactoring sans les for

🏋️‍♂️ Refactoring sans les for

Back to Basics

#backToBasics

Les frameworks et bibliothèques naissent et meurent, les bases restent.

Plan

  1. Introduction

  2. 🔬 Anatomie d'une boucle

  3. 🔃 Récursion

  4. 🌊 Stream

  5. 🏆 Qui est le meilleur ?

  6. Conclusion

🔬 Anatomie d'une boucle

🔬 Anatomie d'une boucle

Transformation

// Java 5 for Each
public Collection<Result> transform(Collection<Element> input) {
    Collection<Result> results = new ArrayList<Result>();
    for (Element element : input) {
        Result res = transform(element);
        results.add(res);
    }
    return results;
}

Filtre

public Collection<Element> filter(Collection<Element> input) {
    Collection<Element> results = new ArrayList<>();
    for (Element element : input) {
        if(isSomething(element)) {
            results.add(element);
        }
    }
    return results;
}

Accumulation

public Accumulator compute(Collection<Element> input) {
    Accumulator accumulator = initialValue;
    for (Element element : input) {
        // accumulate :: (Accumulator, Element) -> Accumulator
        accumulator = accumulate(accumulator, element);
    }
    return accumulator;
}

Imbrication

for (Element element : input) {
    for (Child child: element.getChildren()) {
        // transform, filter, accumulate, ...
    }
}

Et le reste

toto: for (Element element : input) {
    tata: for (Child child : element.getChildren()) {
        if(cond1) {
            continue toto;
        } else if (cond2) {
            break tata;
        }
        // ...
    }
}

🔃 Récursion

🔃 Récursion

Parcours - Java

public List<Result> transformR(List<Element> input) {
    if (input.isEmpty()) { // end of recursion
        return Collections.emptyList();
    }
    // Deconstruct
    Element head = input.get(0);
    List<Element> tail = input.subList(1, input.size() - 1);

    // Handle head
    Result transformed = transform(head);

    // Recursion
    List<Result> results = new ArrayList<>();
    results.add(transformed);
    results.addAll(transformR(tail));
    return results;
}

Parcours - Kotlin & Scala

  • fun transformR(input: List<Element>): List<Result> =
        if (input.isEmpty()) listOf()
        else listOf(transform(input.first())) +
                transformR(input.drop(1))
    
  • def transformR(input: List[Element]): List[Result] =
      input match {
        case Nil          => Nil
        case head :: tail => transform(head) :: transformR(tail)
      }
    

Filtre & Sortie rapide - Java

public Element find(List<Element> input) {
    if (input.isEmpty()) {
        return null;
    }
    Element head = input.get(0);
    if (isSomething(head)) {
        return head;
    }
    List<Element> tail = input.subList(1, input.size());
    return find(tail);
}

Récursion non terminale

`x! = x xx (x-1) xx ... xx 2 xx 1` `1! = 0! = 1`
  • `fact(x)`
  • `x xx fact(x-1)`
    • `x`
  • `x xx (x-1) xx fact(x-2)`
    • `x - 1`
    • `x`
  • `x xx (x-1) xx (x-2) xx ...`
    • ...
    • `x - 1`
    • `x`
  • `x xx (x-1) xx (x-2) xx ... xx 2 xx 1`
    • `1`
    • `2`
    • ...
    • `x - 1`
    • `x`

Récursion terminale

Principe récursion terminale

  • tailRecFunc(scope, state) =
      if (isFinish(scope)) computeResult(state)
      else
        (head, subScope) := scope
        newState := reduce(state, head)
        tailRecFunc(subScope, newState)
    
  • public Accumulator compute(Collection<Element> input) {
        Accumulator accumulator = initialValue;
        for (Element element : input) {
            // accumulate :: (Accumulator, Element) -> Accumulator
            accumulator = accumulate(accumulator, element);
        }
        return accumulator;
    }
    

Récursion terminale - Java

Game Over

Insert Kotlin or Scala
To continue

Récursion terminale - Kotlin & Scala

  • fun factorial(n: Int): Int {
        tailrec fun aux(n: Int, acc: Int): Int =
            if (n < 2) acc
            else aux(n - 1, n * acc)
    
        return aux(n, 1)
    }
    
  • def factorial(n: Int): Int = {
      @tailrec
      def aux(n: Int, acc: Int): Int =
        if (n < 2) acc
        else aux(n - 1, n * acc)
    
      aux(n, 1)
    }
    

🌊 Stream

🌊 Stream

filter, map, & flatMap

  • List<Integer> ints = new ArrayList<>();
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < i; j++) {
            if (j % 2 == 1) {
                ints.add(j * j);
            }
        }
    }
    
  • List<Integer> ints = IntStream.range(0, 9)
            .flatMap(i -> IntStream.range(0, i))
            .filter(j -> j % 2 == 1)
            .map(j -> j * j)
            .boxed()
            .collect(Collectors.toList());
    

Imbrication - flatMap 2/2

  • 1 2 3
  • 1.1 1.2 1.3
    2 3
  • 1.1 1.2 1.3 2 3
  • 1.1 1.2 1.3
    2.1 2.2 2.3 2.4
    3
  • 1.1 1.2 1.3 2.1 2.2 2.3 2.4 3
  • 1.1 1.2 1.3 2.1 2.2 2.3 2.4
  • 1.1 1.2 1.3 2.1 2.2 2.3 2.4

Stream is Lazy

String[] data = "lorem ... amet".split(" ");
Stream<String> stream = Arrays.stream(data)
    .map(s -> {
        System.out.print(s + ",");
        return s.toUpperCase();
    }).filter(s -> {
        System.out.print(s + ",");
        return s.startsWith("A");
    }).peek(s -> System.out.println(s));

System.out.println("Stream created");
  • lorem,...,amet,LOREM,...,AMET,AMET
    Stream created
  • Stream created

Sloth

Opérations paresseuses & terminales

Opérations paresseuses
map
filter
flatMap
peek
distinct
limit et skip
...
Opérations terminales
reduce et collect
findAny et findFirst
forEach
count
...

Accumulation - Reduce

// input: List<Element>
// accumulate : (Accumulator, Element) -> Accumulator
// Accumulator#merge : (Accumulator) -> Accumulator
Accumulator result =
    input.stream()
        .reduce(
            new Accumulator(),
            (acc, elt) -> accumulate(acc, elt),
            (acc1, acc2) -> acc1.merge(acc2)
        );
Cas particulier

Si Element == Accumulator, le accumulate peut servir de merge

Les count, min, max, sum, ... sont des réductions particulières

Accumulation - collect & Collectors

// T: Type du Stream
// A: Type de l'accumulateur
// R: Type du résultat
interface Collector<T,A,R> {
    Supplier<A> supplier();
    BiConsumer<A,T> accumulator();
    BinaryOperator<A> combiner();
    Function<A,R> finisher();
    // ...
}
java.util.stream.Collectors

toSet, toList, toMap, groupBy, joining, summarizingInt, summarizingDouble, summarizingLong, ...

Et si j'ai besoin de l'index ?

  • 😢 pas faisable facilement et proprement en Java

  • ✌️ mais il y a Kotlin et Scala...

Nouveautés Java 9+

Java 9
Stream#takeWhile et Stream#dropWhile
Stream#iterate avec un prédicat
Stream#ofNullable
Optional#stream
Collectors#flatMapping et Collectors#filtering
String#chars et String#codePoints
Java 10
Collectors#toUnmodifiableXXX pour les List, Set et Map
Java 11
String#lines
Java 12
Collectors#teeing

Bilan Stream

🤗

  • On peut utiliser intelligemment les aspects paresseux

  • Sans bonne raison, ne faites pas de Stream parallèle

    La lisibilité est importante

  • À proscrire

    Les effets de bord ! (on tolère les logs dans le peek)

    Les opérations non associatives dans des Stream parallèles

    Les streams sur des Integer, Double, Long

Bilan Stream - Java

😡

  • T reduce(T identity, BinaryOperator<T> accumulator) et
    Optional<T> reduce(BinaryOperator<T> accumulator)

  • IntStream, DoubleStream, LongStream, avec mapToObj, mapToXXX, ...

  • Le boilerplate, par exemple .collect(Collectors.toList()), Collectors.groupBy, ...

  • Pas de tuples en Java

  • Des 🐛 dans le lazy du flatMap, voir Java Stream API was Broken Before JDK10

Bilan Stream - Kotlin

😍

  • API lazy avec les Sequence ou non directement sur les collections

  • API collection immutable ou mutable

  • 🎭 utilise juste les classes de Java

Bilan Stream - Scala

😻

  • API lazy avec les Stream/LazyList ou non directement sur les collections

  • API collection immutable ou mutable

  • Pas de réutilisation de Java

  • API de Stream et LazyList avec la possibilité de construction récursive

  • De gros changements dans la 2.13

  • Le for de Scala est du sucre syntaxique qui produit des map, filter, flatMap

    Du coup on peut l'utiliser sur d'autre objects qui ont map, filter, flatMap

🏆 Qui est le meilleur ?

🏆 Qui est le meilleur ?

Relation d'ordre

Si on veut déterminer le meilleur, ils nous faut une relation d'ordre, laquelle ?

  • 🏎 le plus rapide ?

  • 💾 le moins couteux en mémoire ?

  • 🧱 le plus maintenable ?

  • 🈂 le plus lisible ?

  • ...

Java & performances

Rust

Rewrite everything in Rust
Rewrite everything in Rust

MonteCarlo π

`((pi * r^2) / 4 ) / r^2 = pi / 4 ~~ ("nb. in") / ("nb. total")`
avec `r=1`
`pi ~~`

MonteCarlo - Point

public class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) { this.x = x; this.y = y; }

    public boolean inCircle() {
        return (x * x) + (y * y) <= 1;
    }

    private static final Random rnd = new Random();
    public static Point newPoint() {
        return new Point(rnd.nextDouble(), rnd.nextDouble());
    }

    public static double compute(int count, int inCircle) {
        return ((double) inCircle / count) * 4;
    }
}

MonteCarlo - Java for

public static double monteCarloFor(int count) {
    int inCircle = 0;
    for (int i = 0; i < count; i++) {
        Point p = newPoint();

        if (p.inCircle()) {
            inCircle++;
        }
    }
    return compute(count, inCircle);
}

MonteCarlo - Java stream

public static double monteCarloStream(int count) {
    int inCircle = (int) Stream.generate(Point::newPoint)
            .limit(count)
            .filter(Point::inCircle)
            .count();

    return compute(count, inCircle);
}

MonteCarlo - Java stream parallèle

public static double monteCarloStreamParallel(int count) {
    int inCircle = (int) Stream.generate(Point::newPoint)
            .unordered()
            .parallel()
            .limit(count)
            .filter(Point::inCircle)
            .count();

    return compute(count, inCircle);
}

MonteCarlo - Kotlin for

fun monteCarloFor(count: Int): Double {
    var inCircle = 0
    for (i in 0 until count) {
        val p = newPoint()

        if (p.inCircle()) {
            inCircle++
        }
    }
    return compute(count, inCircle)
}

MonteCarlo - Kotlin tailrec

fun monteCarloRecursion(count: Int): Double {
    tailrec fun aux(count: Int, inCircle: Int): Int =
        if (count == 0) inCircle
        else {
            val p = newPoint()
            aux(count - 1, if (p.inCircle()) inCircle + 1 else inCircle)
        }

    val inCircle = aux(count, 0)
    return compute(count, inCircle)
}

MonteCarlo - Kotlin collection

fun monteCarloCollection(count: Int): Double {
    val inCircle = (1..count)
        .map { newPoint() }
        .count { it.inCircle() }

    return compute(count, inCircle)
}

MonteCarlo - Kotlin séquence

fun monteCarloSequence(count: Int): Double {
    val inCircle = generateSequence { newPoint() }
        .take(count)
        .count { it.inCircle() }

    return compute(count, inCircle)
}

MonteCarlo - Kotlin séquence parallèle *

fun monteCarloSequenceParallel(count: Int): Double {
    val inCircle = sequence {
        yieldAll(generateSequence { newPoint() })
    }
        .take(count)
        .count { it.inCircle() }

    return compute(count, inCircle)
}

MonteCarlo - Scala for

def monteCarloFor(count: Int): Double = {
    var inCircle = 0
    for (_ <- 1 to count) {
      val p = newPoint()

      if (p.inCircle()) {
        inCircle += 1
      }
    }
    compute(count, inCircle)
  }

MonteCarlo - Scala tailrec

def monteCarloRecursion(count: Int): Double = {
    @tailrec
    def aux(count: Int, inCircle: Int): Int =
      if (count == 0) inCircle
      else {
        val p = newPoint()
        aux(count - 1, inCircle + (if (p.inCircle()) 1 else 0))
      }

    val inCircle = aux(count, 0)
    compute(count, inCircle)
  }

MonteCarlo - Scala collection

def monteCarloCollection(count: Int): Double = {
    val inCircle = (1 to count)
      .map(_ => newPoint())
      .count(_.inCircle())

    compute(count, inCircle)
  }

MonteCarlo - Scala stream

def monteCarloStream(count: Int): Double = {
    val inCircle = Stream.fill(count) {
      newPoint()
    }
      .count(_.inCircle())

    compute(count, inCircle)
  }

MonteCarlo - Scala stream parallèle

def monteCarloStreamParallel(count: Int): Double = {
    val inCircle = Stream.fill(count) {
      newPoint()
    }
      .par
      .count(_.inCircle())

    compute(count, inCircle)
  }

Disclaimer

Avec Java 11, Kotlin v1.3.31, Scala v2.13.0, sur un iMac13.2, avec un Intel core i7 3.4GHz - 4 cores, et le JRE HotSpot 11.0.3 AdoptOpenJDK

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial experiments, perform baseline and negative tests that provide experimental control, make sure the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

🎳 venez lire, tester, critiquer, proposer des PR sur le dépôt Github

MonteCarlo - performance 1000 points

24345 ops/s
24345 ops/s
24335 ops/s
24335 ops/s
24325 ops/s
24325 ops/s
24241 ops/s
24241 ops/s
23606 ops/s
23606 ops/s
20876 ops/s
20876 ops/s
20850 ops/s
20850 ops/s
20046 ops/s
20046 ops/s
19711 ops/s
19711 ops/s
10953 ops/s
10953 ops/s
1_000 points sur JRE 11.0.3 hs-adpt
`"tailrec" ~~ 41.1mus > "for" ~~ 41.3mus > "collection" ~~ 48.0mus > "stream" ~~ 49.9mus`

MonteCarlo - performance 10M points

2.434 ops/s
2.434 ops/s
2.432 ops/s
2.432 ops/s
2.431 ops/s
2.431 ops/s
2.403 ops/s
2.403 ops/s
2.361 ops/s
2.361 ops/s
2.355 ops/s
2.355 ops/s
2.078 ops/s
2.078 ops/s
1.835 ops/s
1.835 ops/s
1.233 ops/s
1.233 ops/s
0.36 ops/s
0.36 ops/s
10_000_000 points sur JRE 11.0.3 hs-adpt
`"tailrec" ~~ 411ms > "for" ~~ 416ms > "stream" ~~ 424ms > "collection" ~~ 545ms`

MonteCarlo - performance parallèle

2.434 ops/s
2.434 ops/s
2.432 ops/s
2.432 ops/s
2.43 ops/s
2.43 ops/s
2.403 ops/s
2.403 ops/s
2.361 ops/s
2.361 ops/s
2.355 ops/s
2.355 ops/s
2.078 ops/s
2.078 ops/s
1.835 ops/s
1.835 ops/s
1.571 ops/s
1.571 ops/s
1.233 ops/s
1.233 ops/s
0.468 ops/s
0.468 ops/s
0.36 ops/s
0.36 ops/s
0.249 ops/s
0.249 ops/s
10_000_000 points sur JRE 11.0.3 hs-adpt

😱 le code parallèle

MonteCarlo - performance parallèle 2

8.132 ops/s
8.132 ops/s
6.171 ops/s
6.171 ops/s
2.434 ops/s
2.434 ops/s
2.432 ops/s
2.432 ops/s
2.43 ops/s
2.43 ops/s
2.403 ops/s
2.403 ops/s
2.361 ops/s
2.361 ops/s
2.355 ops/s
2.355 ops/s
2.078 ops/s
2.078 ops/s
1.835 ops/s
1.835 ops/s
1.233 ops/s
1.233 ops/s
0.36 ops/s
0.36 ops/s
10_000_000 points sur JRE 11.0.3 hs-adpt

Ce qui change

  • 🎲 depuis Java 8 SplittableRandom

  • public class Point {
        private final double x;
        private final double y;
    
        public Point(double x, double y) { this.x = x; this.y = y; }
    
        public boolean inCircle() {
            return (x * x) + (y * y) <= 1;
        }
    
        private static final Random rnd = new Random();
        private static final SplittableRandom rnd = new SplittableRandom();
        public static Point newPoint() {
            return new Point(rnd.nextDouble(), rnd.nextDouble());
        }
    
        public static double compute(int count, int inCircle) {
            return ((double) inCircle / count) * 4;
        }
    }
    

Separation of Concerns

  • List<String> errors = new ArrayList<>();
    int errorCount = 0;
    File file = new File(fileName);
    String line = file.readLine();
    while(errorCount < 40 && line != null) {
        if(line.startWith("ERROR")) {
            errors.add(line);
            errorCount++;
        }
        line = file.readLine();
    }
    
  • List<String> errors =
        Files.lines(Paths.get(fileName))
            .filter(line -> line.startsWith("ERROR"))
            .limit(40)
            .collect(toList());
    

Prédisposition aux 🐛

// Good old for style
public Collection transform(Collection input) {
    Collection results = new ArrayList();
    for (int i = 0; i < input.size(); i++) {
        Element element = (Element) input.get(i);
        Result res = transform(element);
        results.add(res);
    }
    return results;
}

Prédisposition aux 🐛 dangereux

// Java 8 forEach & lambda expression
public Collection<Result> transform(Collection<Element> input) {
    Collection<Result> results = new ArrayList<>();
    input.forEach(element -> {
        Result res = transform(element);
        results.add(res); // 😨
    });
    return results;
}

Conclusion

Conclusion

Bilan

goto firstQuote

Les frameworks et bibliothèques naissent et meurent, les bases et les styles de programmation restent.

Il suffit de choisir le langage, les frameworks, et les bibliothèques qui correspondent aux styles.
Choisissiez un style qui correspond à vos contraintes et vos goûts.

Si vous avez du mal à choisir

FP

  • Lambda et fonctions d'ordre supérieur
  • Pas d'effet de bord
  • immutabilité
  • `=>` Ceci est une présentation sur la programmation fonctionnelle

FP 🍌

Crafters

  • ❓ Quand vous codez, posez-vous des questions !
  • 🔪 Aiguisez votre esprit critique !
  • 🗣 Partagez vos questionnements, vos solutions, vos idées farfelues !

Speaker

Merci

  • Questions ?

  • (les retours sont bienvenus)