presentation-funktionale-programmierung



presentation-funktionale-programmierung

0 0


presentation-funktionale-programmierung

Funktionale Programmierung mit Java 8 für Pragmatiker

On Github dctr / presentation-funktionale-programmierung

Funktionale Programmierung

  • Mit Java 8
  • Für Pragmatiker
Praxisorientiert, ohne mathematische Aspekte

FP Teaser

FP = Funktionale Programmierung

- Erfordert Grundlagen - "Motivation" später - Jetzt nur Teaser - Wir erhalten das Mysterium

Imperative Programmierung

  • Beschreibe wie etwas getan werden muss
  • Schritt für Schritt Anleitung
Allgemein bekannt - hoffentlich

Funktionale Programmierung

  • Beschreibe was getan werden muss
  • Beschäftigt sich nicht mit dem wie
  • Nutzt Funktionen höherer Ordnung
- Fkt. höherer Ordnung = Fkt. die Fkt. engegennehmen - Callbacks, Delegates

Voraussetzungen

Grundbaustein: Kapselung dieses was

Das Command Pattern

- Wie eben im Vortrag von Thomas gehört - Command Pettern kapselt Verhalten hinter Interface - Z.B. zur Übergabe von Aufgaben an einen Manager - Asynchrone Queue, Undo/Redo-Manager

Interface

@FunctionalInterface
public interface Command {
    void execute(Object param);
}
Annotation erstmal ignorieren

Aufruf mit dedizierter Klasse

public class MyCommand implements Command {
    void execute(Object param) {
        // Do something meaningful.
    }
}

executor.doSomeAction(new MyCommand());

Aufruf mit anonymer Klasse

executor.doSomeAction(new Command() {
    void execute(Object param) {
        // Do something meaningful.
    }
});

Aufruf mit Lambda-Ausdruck

executor.doSomeAction(param -> {
    // Do something meaningful.
});
Nebenbei auch Lambdas erklärt, und das mit Absicht.

Kurzschreibweisen für Lambdas

List<String> teamSchadow =
    Arrays.asList("Dominik", "Sandro", "Jochen", ...);
teamSchadow.forEach((String name) -> {
    System.out.println(name)
});
teamSchadow.forEach(name -> System.out.println(name));
teamSchadow.forEach(System.out::println);
Consumer<String> myPrintLn = name -> System.out.println(name);
teamSchadow.forEach(myPrintLn);
- Automatische Typinferenz - Keine geschweiften Klammern bei Einzeilern - Methodenreferenz (yey, function pointers!), wenn Parameter 1:1 übernommen wird

@FunctionalInterface

  • FIs haben eine (abstrakte) Methode
  • Dadurch automatische Typinferenz möglich
  • Lambda nur Kurzschreibweise für anonyme Klasse
Die Annotation von eben

Anwendungsfälle

  • Command Pattern
  • Callbacks
  • ... u. v. m.
  • Coole Dinge in diesem Vortrag - aka FP
Callbacks & asynchrone Programmierung, s. auch mein Vert.X-Vortrag

Fragen soweit?

  • Command Pattern
  • Lambda-Ausdrücke
  • FunctionalInterface
- Grundlagen sollten ab hier klar sein - Grundidee = Kapselung von Logik

Motivation

Theoretische Vorteile von FP

  • Lambda-Kalkül
  • Mathematische Beweisbarkeit von Korrektheit
..., aber wir wollten Pragmatismus

Konvertierung von OOP zu FP

... und jetzt nochmal für Pragmatiker, bitte!

Beispiel: Summiere Preise über 20 EUR, ermäßigt um 10 %

Wie in alten Tagen

Collection<BigDecimal> prices = MyApi.getPrices();
BigDecimal price;
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
int i = 0;

while (i < prices.size()) {
    price = prices.get(i);
    if (price.compareTo(BigDecimal.valueOf(20)) > 0) {
        totalOfDiscountedPrices = totalOfDiscountedPrices
            .add(price.multiply(BigDecimal.valueOf(0.9)));
    }
    i++
}

System.out.println("Result: " + totalOfDiscountedPrices);

NUR Syntaktischer Zucker

Collection<BigDecimal> prices = MyApi.getPrices();
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;

for (BigDecimal price : prices) {
    if (price.compareTo(BigDecimal.valueOf(20)) > 0) {
        totalOfDiscountedPrices = totalOfDiscountedPrices
            .add(price.multiply(BigDecimal.valueOf(0.9)));
    }
}

System.out.println("Result: " + totalOfDiscountedPrices);
Erlaubt kompaktere Syntax, Funktions- und vor allem Denkweise ist aber gleich.

Nachteile imperativer Programmierung

  • Vermischt das
    • was (Auszuführende Aktion, aka Command / Logik)
    • mit dem wie (iterieren, verzweigen, ...)
  • Redundanter, manuell erstellter Code (lies: Fehler)
    • Boilerplate-Code (Deklaration von Command-Klassen)
    • Low-Level Code (Verzweigungen, Schleifen, ...)
    • Parallelisierung (Threads starten, Synchronisation, ...)

Eine neue Welt

final BigDecimal totalOfDiscountedPrices = MyApi
    .getPrices().stream()
    .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
    .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
    .reduce(BigDecimal.ZERO, BigDecimal::add);

System.out.println("Result: " + totalOfDiscountedPrices);
- WIE unter der Haube gekapselt in Verben (filter, map, reduce) - WAS auf Einzelelement-Basis definiert

Praktische Vorteile von FP

  • Unveränderbarkeit (Lambda-Scope geschlossen)
  • Keine Seiteneffekte (durch Unveränderbarkeit)
  • Prägnant, ausdrucksstark, näher an natürlicher Sprache
  • Weniger Fehleranfällig (da weniger Code)

⇒ wartbarer

- geschlossener Scope in Java nicht umgesetzt, siehe später - NatSpr + intuitiver (gewöhnungsbedürftig, wie OO auch) - Fehleranfälligkeit: weniger Code, (keine Variablenmuation)

Umsetzung - Teil 1

Java Boardmittel & die "Grundverben"

Die Java 8 Stream API

  • java.util.stream definiert Stream<T>
  • java.util.function definiert FunctionalInterfaces
    • R Function<T,R>::apply(T t)
    • boolean Predicate<T>::test(T t)
    • void Consumer<T>::accept(T t)
    • T Supplier<T>::get()
    • ...
- Streams stellen "Grundverben" bereit - FIs werden von "Grundverben" entgegengenommen - FIs = Commands

stream() ⇒ 1:n

Stream<T> Collection<T>::stream()

collection.stream()
    .someFpVerb(command);
  • Erzeugt "Strom" von Elementen
  • someFpVerb(command) wird für jedes Element ein Mal aufgerufen
  • Vgl. Iterator: iterable.forEach(command)

map() ⇒ n:n

Stream<R> Stream<T>::map(Function<? super T, ? extends R> mapper)

teamSchadow.stream()
    .map((String name) -> {
      return name.toUpperCase();
    });

// "DOMINIK", "SANDRO", "JOCHEN", ...
  • Manipulation von Elementen des Stream durch Function
  • Rückgabetyp der Function bestimmt Typ des anschießenden Streams
- Single-Line-Statement - Kein Return oder Klammern benötigt

filter() ⇒ n:m (m <= n)

Stream<T> Stream<T>::filter(Predicate<? super T> predicate)

teamSchadow.stream()
    .filter(name -> name.startsWith("D"))
    .map(String::toUpperCase);

// "DOMINIK", ...
  • Return-Typ von Predicate ist boolean
  • filter() prüft, ob Predicate true zurückgibt
  • Nur "true"-Elemente werden propagiert
Fluent Interface

reduce() ⇒ n:1

T Stream<T>::reduce(T identity, BinaryOperator<T> accumulator)

teamSchadow.stream()
    .reduce("", (collector, name) -> {
        return collector + ", " + name;
    });

// "Dominik, Sandro, Jochen, ..."
  • BinaryOperator kombiniert Element mit Ergebnis der vorausgegangenen Operation
  • Initialer Wert i. d. R. Identität (s. erster Parameter)
  • Rückgabewert ist Element des Typs, kein Stream!
String "", + 0, * 1, ...

... und viele mehr

In java.util.stream

  • long count()
  • Stream<T> sorted(Comparator<? super T> comparator)
  • Stream<T> limit(long maxSize)
  • ...

Inhärente Parallelisierbarkeit der Stream-API

streamParallel() statt stream(), so einfach geht das.

WIE unter der Haube, Threading ist auch so ein WIE

Inhärente Parallelisierbarkeit!

Die Einfachheit der Anpassung (und damit einhergehend die Folienanzahl) wird der Mächtigkeit kaum gerecht.

Inhärente Parallelisierbarkeit!!

Daher strecke ich auf drei Folien :-)

Umsetzung - Teil 2

Bibliotheken, Frameworks & Sprachen

Andere Java-APIs und Framkeworks

  • Optional<T> (Javas halbherziger Versuch NPEs zu umschiffen)
  • ... und ein paar andere

  • Functional Java (viel Conveniece)

  • Google Guava (mit Observer-Pattern)
  • ...
APIs die FIs entgegennehmen

ReactiveX (RxJava, RxJS, Rx.NET, RxPY, ...)

ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming.

  • Bekannt, verbreitet, mächtig
  • Umfangreicher als java.util.stream
    • join(), merge(), zip()
    • doOnNext(), doOnError(), doOnCompleted()
    • ...
  • Dezent komplexer in Möglichkeiten (und Einarbeitung)
- @Thomas: Am nächsten Entwicklertag Observer-Pattern? - Im Projekt beim Kunden erfolgreich im Einsatz

Andere (JVM-)Sprachen

  • Clojure
    • Lisp-Dialekt
    • JVM, CLR, JS
  • Scala
    • Nicht strikt-funktional, wie Java auch
    • Aber im Vgl. "optimierter" für FP
    • JVM, JS
  • ...

Umsetzung - Teil 3

Eigenen Code funktional zugänglich machen

Denken und programmieren in Streams und FIs

  • Fluent API anbieten
  • Stream<T>s erzeugen / zurückgeben
  • Higher order functions als Parameter akzeptieren
  • FuntionalInterfaces implementieren (um als Parameter genutzt werden zu können)
- Ausführliche Beispiele würden Rahmen sprengen - Fokus im Folgenden auf einigen Entwurfsmustern

Funktionale Entwurfsmuster

Beispiele für Entwicklung mit FP

Dependency Injection

public static int totalAssetValues(final List<Asset> assets,
    final Predicate<Asset> assetSelector) {
    return assets.stream()
        .filter(assetSelector)
        .mapToInt(Asset::getValue)
        .sum();
}

System.out.println("Total of assets: " + totalAssetValues(assets,
    asset -> true));
System.out.println("Total of bonds: " + totalAssetValues(assets,
    asset -> asset.getType() == AssetType.BOND));
System.out.println("Total of stocks: " + totalAssetValues(assets,
    asset -> asset.getType() == AssetType.STOCK));

Macht auch testen einfacher (z. B. asset -> throw new EnumConstantNotPresentException("on purpose");)

- Dependency hier assetSelector - Siehe auch Punkt oben "FIs als Parameter akzeptieren"

Execute Around Method

1. Klassisch

public class Mailer {
    public void from(final String address) { /*... */ }
    public void to(final String address) { /*... */ }
    public void subject(final String line) { /*... */ }
    public void body(final String message) { /*... */ }
    public void send() { privateValidate(); privateSend(); }
}
Mailer mailer = new Mailer();
mailer.from("build@agiledeveloper.com");
mailer.to("venkats@agiledeveloper.com");
mailer.subject("build notification");
mailer.body("...your code sucks...");
mailer.send();

2. Funktional

public class FluentMailer {
private FluentMailer() {}
    public FluentMailer from(final String address) { /*... */; return this; }
    public FluentMailer to(final String address) { /*... */; return this; }
    public FluentMailer subject(final String line) { /*... */; return this; }
    public FluentMailer body(final String message) { /*... */; return this; }
    public static void send(final Consumer<FluentMailer> mailerConsumer) {
        final FluentMailer mailer = new FluentMailer();
        mailerConsumer.accept(mailer);
        mailer.privateValidate();
        mailer.privateSend();
    }
}
FluentMailer.send(mailer -> mailer
    .from("build@agiledeveloper.com")
    .to("venkats@agiledeveloper.com")
    .subject("build notification")
    .body("...much better..."));
- Fluent (return this) - send() aktzeptiert FI - send() managed mailer

Vorteile des "Execute Around Method"-Pattern

  • Fluent ⇒ Keine Wiederholung von mailer.*
  • Gekapselter Scope & Lifetime
    • wäre es in 1. erlaubt, mailer zu speichern und send() mehrfach aufzurufen?
    • kein Zugriff auf mailer nach send() in 2.
  • Auch gut zum Resourcen-Management (DB-Verbindung oder Dateien öffnen und schließen, Lock-Verwaltung, Exception-Handling, etc.)
    • Spart Boilerplate-Aufräum-Code
    • Spart Boilerplate try/finally-Blöcke
    • Beugt vergessen dieses Boilerplate-Codes vor

Lazy Evaluation

  • Kostspielige Operationen sollten nur ausgeführt werden wenn nötig
  • Eventuell entscheidet eine leichtgewichtige Vorbedingung

1. JVM-funktionsweise ausnutzen

lightBoolean && heavy1() && heavy2()
  • Möglich bei boolean-Ausdrücken
  • Nicht möglich bei z. B. Funktionsparametern
myMethod(lightBoolean, heavy1(), heavy2());

1. Auswertung in Lambdas kapseln

Lambdas werden "vor Ort" ausgewertet, aber nicht aufgerufen

Supplier<Boolean> lightLambda1 = () -> heavy1();
Supplier<Boolean> lightLambda2 = () -> heavy2();

myMethod(lightBoolean, lightLambda1, lightLambda2);

2. Stream ist inhärent faul

List<String> names = Arrays.asList("Brad", "Kate", "Kim",
    "Jack", "Joe", "Mike", "Susan", "George",
    "Robert", "Julia", "Parker", "Benson");

final String firstNameWith3Letters = names.stream()
    .filter(name -> length(name) == 3)
    .map(name -> toUpper(name))
    .findFirst()
    .get();

Was wäre Log-Ausgabe der Lambdas?

2. Intermediate VS terminal

getting length for "Brad"
getting length for "Kate"
getting length for "Kim"
converting "Kim" to uppercase
KIM
  • Intermediates (filter(), map(), ...) geben einen Stream zurück, der die Operation "cachet"
  • Terminals (findFirst(), reduce(), ...) fragen nur so viele Elemente an, wie benötigt
  • Erlaubt effizienten Umgang mit infiniten Streams (z. B. Stream<Integer> pimeNumbers)
- Reduce fragt natürlich ab bis Ende - Terminals sind idR die, die keinen Stream zurückgeben, sondern T

ABER

Probleme mit "reiner" FP in Java

Nicht 100%-ig umgesetzt bzw. überhaupt umsetzbar

  • Dinge fehlen
  • Lambda-Scope nicht geschlossen
  • Mischbetrieb mit OOP möglich
  • ...

  • Erlaubt Seiteneffekte (Lambdas können z. B. aüßere Variablen zugreifen)
  • Kann Code unübersichtlicher machen als vorher

Aber FP ist in, daher muss Java es können...

Performance in Java

  • Nicht so performant wie "echte" FP-Sprachen
  • Lambdas sind implizit anonyme Klassen
Da Java stark auf sein Typsystem ausgerichtet ist.

Fazit

Nichtsdestotrotz sehr hilfreich für den Alltag!

  • Denkweise (subjektiv) eingängig und angenehm
  • Lesbarkeit
  • Parallelisierbarkeit
  • Vorbereitung auf andere Sprachen
  • Macht Spaß!
- Spaß = Einarbeiten in neue Denkweisen
Funktionale Programmierung Mit Java 8 Für Pragmatiker Praxisorientiert, ohne mathematische Aspekte