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

Funktionale Programmierung in der Praxis

Funktionale Programmierung in der Praxis

Ein Vortrag im Rahmen der Vorlesung "Funktionale Programmierung" an der Universität Rostock.

Jens Grassel

January 31, 2020
Tweet

More Decks by Jens Grassel

Other Decks in Programming

Transcript

  1. Werdegang Am Anfang war die Suchmaschine... • so um Entwicklung

    einer eigenen Suchmaschine • in Java (ohne EE und ohne Spring ;-)) • Gr¨ undung einer eigenen Firma daf¨ ur • Gewinn eines Innovationspreises • neues Projekt (Forschung und Entwicklung) ⇒ Daten-Integrations-Plattform • Grenzen von Java erreicht ⇒ “Geht es nicht auch besser?”
  2. Die Qual der Wahl... Es gibt eine Vielzahl von Programmiersprachen,

    aber . Wir wollten funktional programmieren (Affinit¨ at zu Lisp). . Wir hatten ein existierendes ¨ Okosystem (Integration). Verschiedene M¨ oglichkeiten • Lisp-Dialekte (Common Lisp, Scheme, Clojure, SBCL) • ML-Dialekte (SML, OCaml, Haskell) • Sonstige (Erlang, Scala) Am Ende blieb die Wahl zwischen Clojure und Scala.
  3. Warum Scala? Wir entschieden uns f¨ ur Scala... • Wir

    wollten in der JVM bleiben. • Wir wollten eine statisch typisierte Programmiersprache. • zum Entscheidungszeitpunkt schon etwas “erwachsen” • Interoperabilit¨ at mit existierendem Java-Code • Verf¨ ugbarkeit von Bibliotheken, Treibern(!) und Toolkits
  4. M¨ uhsam ern¨ ahrt sich das Eichh¨ ornchen... Nur Mut,

    auch Zwerge haben klein angefangen! • erstmal noch “Java in Scala” mit funktionalem “Feenstaub” drauf • immutable Variablen • Pattern Matching • Monaden gemieden
  5. Weitere Schritte: Funktoren Was man so tut, wenn man keine

    Ahnung hat... val a: List[Int] = ??? val b = for (i <- 0 until a.length) yield a(i) + 1 Es gibt Funktoren, einfach gesprochen: “map” val b = a.map(_ + 1) Und es gibt noch mehr... • contravariante Funktoren (contramap ⇒ “prepend”) • invariante Funktoren (imap ⇒ map + contramap)
  6. Weitere Schritte: Monoide ( / ) Was ist ein Monoid?

    Eine Halbgruppe mit einem neutralen Element. Wozu braucht man das? ⇒ Zum Zusammenfassen von Mengen zum Beispiel. Aber, das geht doch auch so. @ 1 + 2 res0: Int = 3 Stimmt, doch was ist damit? @ Option(1) + Option(2) type mismatch Compilation Failed
  7. Weitere Schritte: Monoide ( / ) Es geht nat¨ urlich,

    wenn man unbedingt will: @ Option(1).map(a => Option(2).map(b => a +b)) res0: Option[Option[Int]] = Some(Some(3)) @ Option(1).map(a => Option(2).map(b => a +b)) .flatten res1: Option[Int] = Some(3) Mit den entsprechenden Instanzen verf¨ ugbarer Monoide geht sowas: @ Option(1) |+| Option(2) res0: Option[Int] = Some(3) Denkt das Szenario weiter in Richtung ADTs, die kombiniert werden k¨ onnen!
  8. Weitere Schritte: Monaden ( / ) Was ist ein Monad

    eigentlich? Ein Monoid in der Kategorie der Endofunktoren. Aber das ist nicht wichtig! Was sind die praktischen Anwendungsm¨ oglichkeiten? • Sequenzierung (“Hurra, wir k¨ onnen Batch-Skripte schreiben!”) • “Lifting” von Werten in einen Monad (via pure) • Kapselung von Effekten • starke Garantien hinsichtlich des Verhaltens (“Monad Laws”)
  9. Weitere Schritte: Monaden ( / ) Durch Nutzung von Monaden

    beschreiben wir das Programm als abstrakten Syntaxbaum (AST) ⇒ “Free Monads” • “Programm” wird erst bei Bedarf ausgef¨ uhrt • M¨ oglichkeiten zur Optimierung (Interpreter) • konsistente Fehlerbehandlung durch MonadError Der gr¨ oßte Nachteil ⇒ Monads do not compose! • Monad-Transformer • Konstruktor, der einen Monad bekommt und wieder einen zur¨ uckliefert • Nachteile: unhandlich, “teuer”
  10. Das Streben nach “pure” FP... Warum “pure”? • keine Seiteneffekte

    • lazy evaluation • Ausgabe von Funktionen nur abh¨ angig von ihrer Eingabe Das alles f¨ uhrt zu referentieller Transparenz und besserem Verst¨ andnis des Programmverhaltens (via “informal reasoning”).
  11. You have reached the next level! Mit wachsender Erfahrung werden

    auch “fortgeschrittene” Techniken allt¨ aglich angewendet. • Folds statt Pattern Matching • IO-Monad (cats-effect) • Rekursionsschemata • Refined Types • Optics
  12. Kein “low level” optimierter Code mehr! ( / ) case

    class Street(number: Int, name: String) case class Address(city: String, street: Street) case class Company(name: String, address: Address) case class Employee(name: String, company: Company) val employee = ??? Jetzt wollen wir den ersten Buchstaben des Straßennamens der Firma großschreiben!
  13. Kein “low level” optimierter Code mehr! ( / ) So

    in etwa s¨ ahe das ohne Optics aus... employee.copy( company = employee.company.copy( address = employee.company.address.copy( street = employee.company.address.street.copy( name = employee.company.address.street.name .capitalize ) ) ) )
  14. Kein “low level” optimierter Code mehr! ( / ) Und

    nun mit Optics... employee .lens(_.company.address.street.name) .composeOptional(headOption) .modify(_.toUpper) Aber das ist noch nicht alles! • Traversieren von Strukturen • Rekursion (bzw. Rekursionsschemata) • kein “Bits schieben” mehr
  15. Kein “low level” optimierter Code mehr! ( / ) Aber

    was sind eigentlich “Optics”? • pure funktionale Abstraktionen zur Manipulation von immutable Objekten • Basis ist Iso (Ja, kurz f¨ ur Isomorphismus. ;-)) • darauf aufbauend: • Lens (in Daten “reinzoomen”) • Prism “Teile” von Daten selektieren • des weiteren: Getter, Optional, Traversal, Setter, Fold • in Haskell gibt es Lens https://github.com/ekmett/lens
  16. Keine unkontrollierten Seiteneffekte mehr! ( / ) Die ¨ ublichen

    Trivialbeispiele, aber ihr versteht worum es geht. for { _ <- Future { println("Hi there!") } _ <- Future { println("Hi there!") } } yield () Was macht der Code? val printF = Future { println("Hi there!") } for { _ <- printF _ <- printF } yield () Und was macht dieser?
  17. Keine unkontrollierten Seiteneffekte mehr! ( / ) Es geht auch

    anders, sprich: “IO gibt es nicht nur bei Haskell.” val printF = IO(println("Hi there!")) val effect = for { _ <- printF _ <- printF } yield () Und dieser Code macht tats¨ achlich das, was da steht! :-) @ effect.unsafeRunSync Hi, there! Hi, there!
  18. Wir m¨ ussen weniger Tests schreiben! Nutzen des Typsystems •

    Vermeidung von “stringified programming” • Compiler verhindert Fehler, die wir sonst per Tests finden m¨ ußten! • per Refined Types noch ’ne Schippe drauf • Ergo, der Compiler verhindert noch mehr Fehler! :-) • Reduzierung des Implementierungsraums durch Typisierung und Polymorphismus • D.h. es wird einfacher Funktionen zu implementieren. def f(a: String): String = ??? // versus def f(a: IPv4): Hostname = ???
  19. Wir k¨ onnen bessere Tests schreiben! Es werden nicht nur

    weniger Tests, sondern auch bessere! • leichteres Testen durch Modularisierung und Isolation (ASTs, IO) • robusteres Testen durch “Property Based Testing” forAll("Product ID") { id: ProductId => val uri = Uri(s"/product/$id") val response: IO[Response] = service.orNotFound .run(Request(method = GET, uri = uri)) response.unsafeRunSync.status must be(expectedStatus) }
  20. Die Entwicklungsgeschwindigkeit erh¨ oht sich! Das klingt jetzt erstmal merkw¨

    urdig, aber • Refactorings werden viel leichter • Compiler sagt quasi was zu tun ist (“follow the types”) • Property Based Tests finden viel mehr Fehler • Code ist verst¨ andlicher (h¨ ohere Abstraktionsebene) • h¨ oheres Vertrauen in schnelle ¨ Anderungen
  21. Die Anwendungen laufen schneller! Auch das klingt merkw¨ urdig, aber

    • Das alte Vorurteil “Quick ’n dirty is faster!” ist gefallen! • nat¨ urlich abh¨ angig vom Kontext • Beispiel aus Webservice-Bereich • “pure” L¨ osung bis zu mal(!) schneller als “impure” • dazu weniger Systemlast https://leanpub.com/pfhais
  22. Erfahrungsbericht Datenintegration Die Datenintegrationsl¨ osung wurde in Monaten von Leuten

    konzipiert und entwickelt. Hier einige Eckdaten: • auf “Augenh¨ ohe” mit etablierten L¨ osungen (Talend, Informatica, etc.) • Features, die die anderen nicht hatten (Wahrung referentieller Integrit¨ at, Ber¨ ucksichtigung der Semantik von Daten) • Service basierte Architektur und Aktorensystem (beliebig skalierbar) • ca. . LoC (plus . LoC Tests) • vergleichbare Projekte gehen in die Millionen LoC • und wurden ¨ uber Zeitr¨ aume von - Jahren (und mehr) entwickelt
  23. Erfahrungsbericht Verkaufsplattform Onlineh¨ andler ( / ) Unterst¨ utzung eines

    großen internationalen H¨ andlers ¨ uber Jahre. • große Anzahl an Services und Bibliotheken (Java und Scala) • keine Dokumentation Hier einige Beispiele aus der Zeit... Anmerkung zu LoC Unsere Angabe f¨ ur die Zeilenanzahl (LoC) bezieht eventuelle vorhandene Dokumentation mit ein. Im Regelfall sind - Prozent der LoC in unserem Code Funktionsdokumentation. Im Kundenbereich geht dieser Wert normalerweise gegen Null!
  24. Erfahrungsbericht Verkaufsplattform Onlineh¨ andler ( / ) Beispiel “Datenkonvertierungsservice” •

    Mischung aus Java (mit Spring) und etwas Scala und Akka (Aktoren) • Kurzfassung: “horrible, mutable stuff” • etwa . LoC (ca. LoC Testcode) • selbst erfahrene Entwickler brauchten Tage f¨ ur kleinste ¨ Anderungen • viele Bugs, Abst¨ urze, “h¨ angen bleiben” • hohe Systemlast • “langsam” (mehrere Stunden pro Land)
  25. Erfahrungsbericht Verkaufsplattform Onlineh¨ andler ( / ) Beispiel “Datenkonvertierungsservice” •

    Rewrite komplett in Scala mit funktionaler Programmierung und Refined Types • LoC (davon sind LoC Testcode!) • viel leichter zug¨ anglich ⇒ schnelle ¨ Anderungen • Datenverarbeitung mit Streams • automatische Anpassung an Systemlast (keine ¨ Uberlastung) • “schnell” (mehrere Minuten(!) pro Land) • Fehler in ¨ ubernommener Verarbeitungslogik gefunden durch Property Based Testing
  26. Erfahrungsbericht Verkaufsplattform Onlineh¨ andler ( / ) Beispiel “Backend f¨

    ur Webshop” • komplett in Scala (Nutzung des Play Framework) • viele “Altlasten” (großes Projekt, unerfahrene Entwickler) • ca. . LoC (davon . Testcode) • Code sehr eng verdrahtet (tight coupling) • fragt verschiedenste Services an • aber geht davon aus, daß diese immer erreichbar sind!
  27. Erfahrungsbericht Verkaufsplattform Onlineh¨ andler ( / ) Beispiel “Backend f¨

    ur Webshop” • kein Rewrite, sondern Refactoring • Entkopplung und Modularisierung zur schrittweises Aufl¨ osen der engen Verdrahtung • Team entschied sich f¨ ur den Ansatz “Tagless Final“ (quasi “Interpreter Pattern”) Ziele wurden erreicht • Stabilisierung des Systems • Erh¨ ohung der Entwicklungsgeschwindigkeit • konsistente Fehlerbehandlung (MonadError)
  28. Angst beim Kunden Was h¨ oren wir am h¨ aufigsten?

    “Unsere Leute verstehen das nicht!” Aber das ist Unfug! • lediglich Ber¨ uhrungs¨ angste • alte Vorurteile • “Das ist akademischer Unfug, weltfremd!” • “Daf¨ ur muß man Mathe studiert haben!” • mangelndes Vertrauen in Mitarbeiter Jeder kann das!
  29. H¨ oherer Einarbeitungsaufwand Trotzdem gibt es auch H¨ urden... •

    Denken zumeist stark imperativ gepr¨ agt • oft wenig bis gar keinen Kontakt mit funktionaler Programmierung Es braucht Zeit. • alte Denkmuster abzulegen • m¨ ogliche Abstraktionen zu erkennen
  30. Was bleibt unterm Strich? • Viele Wege f¨ uhren nach

    Rom! • oder auch “Es muß nicht immer < SpracheX > sein!” • Auswahl je nach Aufgabe und Umgebung • Nutzt statisch typisierte funktionale Programmierung! • je gr¨ oßer die Code-Basis, desto mehr hilft der Compiler • findet Fehler nicht erst zur Laufzeit • Nutzt eine “richtige” funktionale Sprache! • imperative Sprachen mit “rangeschraubten” funktionalen Features :-( • ausgebremst durch Grenzen der Sprache
  31. Jeder ist seines Gl¨ uckes Schmied! Habt keine Angst vorm

    Endofunktor! • Es ist gar nicht so schwer! • Es ist nicht umsonst, d.h. “Es gibt Jobs!” • Es erleichtert die Arbeit oder auch: “Softwareentwicklung ist schon schwer genug!” Hebt Euch ab von der Masse oder nach Paul Graham : “Beat the Averages!” http://www.paulgraham.com/avg.html