🏋️‍♂️ Refactoring sans les for

🏋️‍♂️ Refactoring sans les for

🏋️‍♂️ Refactoring sans les for

Speaker

Plan

  1. Introduction

  2. 🔬 Anatomie d'une boucle

  3. 🔃 Récursion

  4. 🌊 Stream

  5. 🏆 Qui est le meilleur ?

  6. Conclusion

Back to Basics

#backToBasics

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

🔬 Anatomie d'une boucle

🔬 Anatomie d'une boucle

Transformation - Java 1.4

// Good old for style
public List transform(List input) {
    List 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;
}

Transformation - Java 5

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

Transformation - Java 8

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

Filtre

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

Accumulation

public Accumulator compute(List<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

fun transformR(input: List<Element>): List<Result> =
    if (input.isEmpty()) listOf()
    else listOf(transform(input.first())) +
            transformR(input.drop(1))

Parcours - Scala

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

  • `fact(x) = fact(x, 1)`
  • `fact(x-1, x xx 1)`
  • `fact(x-2, x xx (x-1))`
  • `fact(... , x xx (x-1) xx (x-2) xx ...)`
  • `fact(1, x xx (x-1) xx (x-2) xx ... xx 2)`
  • ⚠️ Nécessite une optimisation par le compilateur

Principe récursion terminale

tailRecFunc(scope, state) =
  if (isFinish(scope)) computeResult(state)
  else
    (head, subScope) := scope
    newState := reduce(state, head)
    tailRecFunc(subScope, newState)

Récursion terminale - Java

Game Over

Insert Kotlin or Scala
To continue

Récursion terminale - Kotlin

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)
}

Récursion terminale - Scala

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

Création 1/2

// Create from a Collection
Stream<Element> s0 = col.stream();
Stream<Element> s1 = col.parallelStream();

// Create from array
Stream<Element> s2 = Arrays.stream(array);
Stream<Element> s3 = Stream.of(array);

// Manually
Stream<Element> s4 = Stream.of(elt1, elt2 /* , ...*/);
Stream<Element> s5 = Stream.empty();

Création 2/2

// Some IntStream
IntStream.range(0, 9); // 0,1,2,3,4,5,6,7,8
IntStream.rangeClosed(0, 9);  // 0,1,2,3,4,5,6,7,8,9
IntStream.iterate(0, i -> i + 1); // 0,1,2,3,...
IntStream ints = new Random().ints();

// With Path
Path path = Paths.get("plop.md");
Stream<String> lines = Files.lines(path);
Stream<Path> walk = Files.walk(path);

// With a Spliterator
Iterable<Element> elts = // ...
StreamSupport.stream(elts.spliterator(), false);

Transformation - map

// input: List<Element> 
// transform : (Element) -> Result
input.stream()
    .map(element -> transform(element))
    // : Stream<Result> 
    // ...
    ;

Filtre - filter

// input: List<Element> 
// isSomething : (Element) -> boolean

input.stream()
    .filter(element -> isSomething(element))
    // : Stream<Element>
    // ...
    ;

Imbrication - flatMap 1/2

// input: List<Element> 
// Element#getChildren : () -> Collection<Child>

input.stream()
    .flatMap(element -> 
        element.getChildren().stream())
    // : Stream<Child>
    // ...
    ;

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 1/2

// 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) 
        );

Accumulation - Reduce 2/2

Cas particulier

Si Element == Accumulator, on peut utiliser un reduce

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

// input: List<Element> 
// Element#merge : (Element) -> Element

Element result = 
    input.stream()
        .reduce(
            new Element(),
            (elt1, elt2) -> elt1.merge(elt2)
        );

Accumulation - collect & Collectors

Les Stream#collect sont justes une généralisation du reduce

// 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();
    // ...
}

Collectors classiques

Dans java.util.stream.Collectors

  • toSet, toList pour construire une collection

  • toMap pour construire un Map

  • groupBy pour grouper en une Map<K,List<V>>

  • joining pour construire une String

  • summarizingInt, summarizingDouble, summarizingLong pour les statistiques

  • ...

Et l'index ?

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

🤗

À 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

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

La lisibilité est importante

On peut utiliser intelligemment les aspects paresseux

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 ou non directement sur les collections

  • API collection immutable ou mutable

  • Pas de réutilisation de Java

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

  • De gros changements arrivent dans la 2.13

Scala for

🤢

val monkeyFood = (for {
        fruit <- fruits
        it = emojify(fruit)
        if "🍌" == it || "🥥" == it
    } yield it)
  • 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

Disclaimer

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

24803 ops/s
24803 ops/s
24801 ops/s
24801 ops/s
23318 ops/s
23318 ops/s
23123 ops/s
23123 ops/s
21267 ops/s
21267 ops/s
20108 ops/s
20108 ops/s
18195 ops/s
18195 ops/s
17943 ops/s
17943 ops/s
17287 ops/s
17287 ops/s
11082 ops/s
11082 ops/s
1_000 points sur OpenJDK (HotSpot) 8.0.202
`"tailrec" ~~ 40.3mus > "for" ~~ 47.0mus > "stream" ~~ 49.7mus > "collection" ~~ 55.0mus`

MonteCarlo - performance 10M points

2.482 ops/s
2.482 ops/s
2.472 ops/s
2.472 ops/s
2.31 ops/s
2.31 ops/s
2.278 ops/s
2.278 ops/s
2.111 ops/s
2.111 ops/s
1.987 ops/s
1.987 ops/s
1.967 ops/s
1.967 ops/s
1.664 ops/s
1.664 ops/s
1.634 ops/s
1.634 ops/s
0.277 ops/s
0.277 ops/s
10_000_000 points sur OpenJDK (HotSpot) 8.0.202
`"tailrec" ~~ 403ms > "for" ~~ 474ms > "stream" ~~ 508ms > "collection" ~~ 601ms`

MonteCarlo - performance parallèle

2.482 ops/s
2.482 ops/s
2.472 ops/s
2.472 ops/s
2.31 ops/s
2.31 ops/s
2.278 ops/s
2.278 ops/s
2.111 ops/s
2.111 ops/s
1.987 ops/s
1.987 ops/s
1.967 ops/s
1.967 ops/s
1.664 ops/s
1.664 ops/s
1.634 ops/s
1.634 ops/s
1.571 ops/s
1.571 ops/s
0.468 ops/s
0.468 ops/s
0.277 ops/s
0.277 ops/s
0.194 ops/s
0.194 ops/s
10_000_000 points sur OpenJDK (HotSpot) 8.0.202

😱 le code parallèle

MonteCarlo - performance parallèle 2

7.654 ops/s
7.654 ops/s
7.224 ops/s
7.224 ops/s
2.482 ops/s
2.482 ops/s
2.472 ops/s
2.472 ops/s
2.31 ops/s
2.31 ops/s
2.278 ops/s
2.278 ops/s
2.111 ops/s
2.111 ops/s
1.987 ops/s
1.987 ops/s
1.967 ops/s
1.967 ops/s
1.664 ops/s
1.664 ops/s
1.634 ops/s
1.634 ops/s
1.571 ops/s
1.571 ops/s
0.598 ops/s
0.598 ops/s
0.468 ops/s
0.468 ops/s
0.277 ops/s
0.277 ops/s
0.194 ops/s
0.194 ops/s
10_000_000 points sur OpenJDK (HotSpot) 8.0.202

SplittableRandom

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 List transform(List input) {
    List 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 List<Result> transform(List<Element> input) {
    List<Result> results = new ArrayList<>();
    input.forEach(element -> {
        Result res = transform(element);
        results.add(res); // 😨
    });
    return results;
}

Pas tous du même avis

Exemple colonnes d'Excel - for

A, B, ..., Z, AA, AB, ..., ZZ, AAA, ...
fun excelColumn(n: Int): String {
    var chars = ""
    var current = n

    while (true) {
        current--
        chars = ('A'.toInt() + current % 26).toChar() + chars
        if (current < 26) break
        current /= 26
    }
    return chars
}

Exemple colonnes d'Excel - récursif

fun excelColumn(n: Int): String {
    tailrec fun aux(n: Int, acc: String): String {
        val current = n - 1
        val rest = current % 26
        val result = ('A'.toInt() + rest).toChar() + acc
        return if (current < 26) result
        else aux(current / 26, result)
    }
    return aux(n, "")
}

Exemple colonnes d'Excel - séquence

fun excelColumn(n: Int): String =
    generateSequence<Pair<Int?, String>>(n to "") { (n, acc) ->
        if (n == null) null
        else {
            val current = n - 1
            val result = ('A'.toInt() + current % 26).toChar() + acc

            if (current < 26) null to result
            else (current / 26) to result
        }
    }.last().second

Conclusion

Conclusion

Bilan

goto firstQuote

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

RxJava, Reactor, Spark, Kafka stream, ...

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
  • Immutablilté
  • `=>` Ceci est une présentation sur la programmation fonctionnelle
  • Functional
    Functional

FP 🍌

Crafters

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

Fin

🍕 ou ❓