Scalaz introduction for Java programmers

Preview:

DESCRIPTION

Scalaz introduct

Citation preview

Scalaz Einführungfür Java Programmierer

Bernhard Huemer | IRIANbernhard.huemer@irian.at

@bhuemer

Agenda

Keine Einführung in ScalaKeine Einführung in Scalaz("Haskell für Scala")

MotivationTransaktionen mit MonadsType classes bzw. Ad-hoc polymorphismusev. Arbeiten mit der Datenbank ohne* Seiteneffekteev. Dependency Injection

Funktionale APIs gestalten

* Fast ohne

Referential transparency"An expression is said to be referentially transparent if it can be replaced with its value without

changing the behavior of a program." (d.h. keine Seiteneffekte)

def square(i: Int) = i * i// Bad! Referenziert auch etwas anderes als Parameterdef now() = System.currentTimeMillis()

Keine Reassignment von VariablenKeine Setter, keine MutationsKeine ExceptionsKeine Konsolenein-/ausgabeKeine Netzwerkkommunikation...

Exceptions als Beispiel

def f(): Int = { val y = g() try { y + y } catch { case e: Exception => 1337 }}

def f(): Int = {

try { g() + g() } catch { case e: Exception => 1337 }}

def g(): Int = { throw new Exception("No!"); }

Checked vs. unchecked exceptions (etwas) irrelevantCompiler versichert, dass Seiteneffekte behandelt werdenBesser: Sie treten gar nicht erst auf

Viel zu triviales Beispiel, ich weiß!

Exceptions als Beispiel (2)

class Cache (cacheLoader: CacheLoader) { def get(key: Key): Value = { // ... }}

trait CacheLoader { def load(key: Key): Value // throws Exception!}

class Cache (cacheLoader: CacheLoader) { def preload() { // ... } def get(key: Key): Value = { // ... }}

Refactoring mit oder ohne Seiteneffekte?Exceptions bei preload: Falsch/teilw. initialisierter Cache, fehlendesException-Handling*, etc.?Exceptions bei get: Mühsam* z.B: Applikation startet nicht mehr, weil unbenützter Cache Key nicht geladen werden kann, andere Initialisierungen

werden nicht eingeleitet, etc. - Autor von Cache hat keinen Einfluss darauf!

Call Stack AbhängigkeitEs treten immer dann Probleme auf,

wenn wir den Call Stack irgendwie ändern.

Multi-threaded UmgebungenWeb ServicesRMI oder sonstiges Remotinggenerell jegl. Netzwerkkommunikation...

"Do or do not. There is no scala.util.Try[A]." - Yoda

scala.util.Try[A]import scala.util.{Try, Success, Failure}

def divide: Try[Int] = for { dividend <- Try(Console.readLine("Dividend:\n").toInt) divisor <- Try(Console.readLine("Divisor:\n").toInt)} yield (dividend / divisor)

val result: Try[Int] = /* .. */

val resultInt = if (result.isSuccess) { result.get } else { 0 } val resultInt = result.getOrElse(0) val resultInt: Try[Int] = result.recover { case e: Exception => 0}

Nicht einfach nur ein Wrapper um ExceptionsMacht aus einer partiellen Funktion eine totale (d.h. gut!)

"... the meaning of RT expressions does not depend on context and may be reasoned about locally, while themeaning of non-RT expressions is context dependent and requires more global reasoning." (FPiS)

Typsystem = light-weight proof-system

I was eventually persuaded of the need to design programming notations so as to

1. maximise the number of errors which cannot be made,

2. or if made, can be reliably detected at compile time.

- Tony Hoare (ACM Turing Award Lecture)

Typsystem kann viel mehr beweisen als "x has method f"Compile time = Build time, d.h. Unit Tests sind gut, aber nicht gutgenug

Transaktionen - CMT/Annotations/XML@Transactionalpublic void updateSomething() { /* ... */ }

@Transactionalpublic List<SomeType> querySomething() { /* ... */ }

Offene Transaktion? @Transactional/Interceptor vergessen?Richtige Transaktion? Implizite Beziehung zwischen Transaktion undEntityManagerKopplung mit Transaktionsattributen (Propagation, Isolation, etc.), retry?

Transaktionen - BMT/Templates

public void updateSomething(EntityManagerFactory emf) { transactionTemplate.execute(emf, new TransactionCallbackWithoutResult() { protected void doInTransaction(EntityManager em) { updateOperation1(em); updateOperation2(em); } } );}

Wiederverwendung in anderer Transaktion?Offene/richtige Transaktion? (teilweise ..)EntityManager (o.ä.) müssen durchgereicht werden

Alternative: Transaktions Monad

Monad = Abstraktion zur sequentiellen Ausführung von einzelnenSchritten in einem bestimmten Kontext (informell)

trait Tx[+A] { // Um die Transaktion "auszuführen" und den Inhalt zu bekommen def run(em: EntityManager): A}

object Tx { // DSL für Transaktionen (konvertiert Funktion in Tx[A]) def transactional[A](body: EntityManager => A) = new Tx[A] { override def run(em: EntityManager) = body(em) }}

Das ist noch kein Monad, nicht einmal informell. Ich habe unser Reiseziel nur vorweggenommen.

Tx[A]-"Hello World"def findAllProductIds() /* : Tx[List[Int]] */ = transactional { em /* : EntityManager */ => // ...}

def findEmployee(employeeId: String) = transactional { em => /* ... */}

So einfach wie mit Annotations - type inference :)Erstmal vollkommen ohne Abhängigkeiten/Seiteneffekte*Unmöglich falschen EntityManager zu verwenden**

* Für Skeptiker unter uns: Wie etwas sinnvolles programmieren ohne Seiteneffekte?

** Tony Hoar wäre stolz auf uns!

Tx<A> mit Java 8

Tx<A> = Keine Proxies, keine Konfiguration, keine sonstige "Magie",eigentlich nur ein TransactionCallback<T> mit Lambdas?

@FunctionalInterfacepublic interface Tx<T> { public T run(EntityManager em); // Nicht wirklich hilfreich, Return Type sparen wir uns nicht public static <T> Tx<T> transactional( Function<EntityManager, T> body) { return body::apply; }}

public Tx<List<Integer>> findProductIds(String searchCrit) { return transactional(em -> /* ... */);}public Tx<Employee> findEmployee(String employeeId) { return em -> /* ... */;}

Tx[A] Ausführungdef run[A](emf: EntityManagerFactory, tx: Tx[A]): A = { val em = emf.createEntityManager try { em.getTransaction.begin() val result = tx.run(em) em.getTransaction.commit() result } finally { // Rollback (eventuell) und clean-up } }

Fazit bisher: Lediglich BMTs aufgesplittet?Interpretieren des Monads verursacht SeiteneffekteAndere Form der Abstrahierung - eine Beschreibung, vieleMöglichkeiten diese zu interpretieren

Compiler kennt jetztTransaktionsklammern

// Oh! Wir haben @Transactional vergessen, sozusagendef findEmployeeName(employeeId: String) = findEmployee(employeeId).name // Wäre zwar nicht so schlimm ..

// ERROR: // value name is not a member of ...Tx[...Employee]

// Wir sind aber auch vergesslich!def findManagerForEmployee(employeeId: String) = // Lazy loading kann hier aber schon zu Problemen führen! findEmployee(employeeId).department.manager // findEmployee(employeeId).run(em).department.manager

// ERROR: // value department is not a member of ...Tx[...Employee]

Transaktionskontext benötigt, um Inhalt zu bearbeiten (bzw. eine EntityManager Referenz ..)

Tx[A] Verarbeitung (noch) unschön

def findManagerForEmployee(employeeId: String) = transactional { em => findEmployee(employeeId).run(em).department.manager }

public Tx<Employee> findManagerForEmployee(String employeeId) { return em -> findEmployee(employeeId).run(em).getDepartment().getManager();}

Lesbarkeit eher suboptimal, etwas umständlichEntityManager sollten wir nicht durchreichen müssenGesucht: Funktion mit Signatur Tx[A] => (A => B) => Tx[B]

Higher-order function: maptrait Tx[+A] { self => // Statt "Tx.this" // .. def map[B](f: A => B) = transactional { em => f(self.body(em)) }}

public interface Tx<T> { // .. default public <U> Tx<U> map(Function<T, U> f) { return em -> f.apply(run(em)); }}

Nicht nur bei Collections nützlichStrukturerhaltende Verarbeitung von Elementen

Verwendung von map() (1)def findManagerForEmployee(empId: String)/*: Tx[Employee] */ = findEmployee(empId) map { employee => employee.department.manager }

public Tx<Employee> findManagerForEmployee(String empId) { return findEmployee(empId).map( employee -> employee.getDepartment().getManager());}

Keine EntityManager, kein @Transactional - alles wird sozusageninferredErstmals wirklicher Unterschied zu der BMT Lösung

Verwendung von map() (2)public Tx<Employee> findManagerForEmployee(String empId) { return findEmployee(empId) .map(Employee::getDepartment) .map(Department::getManager);}

def findDepartmentForEmployee(empId: String) = for { employee <- findEmployee(empId) } yield employee.department.manager

For comprehensions lediglich syntactic sugar

Verwendung von map (2)def findProductIds(searchCrit: String) = transactional { em => /* ... */}

def findProductNames(productIds: List[Int]) = transactional { em => /* ... */}

def findProductNamesFor(searchCrit: String) = for { productIds <- findProductIds(searchCriteria) } yield findProductNames(productIds)

// od. einfacher:

def findProductNamesFor(searchCrit: String) = findProductIds(searchCrit) map findProductNames

Kompiliert zwar, liefert aber Tx[Tx[List[String]Wir suchen: T[x] => (A => Tx[B]) => Tx[B]

Higher-order function: flatMaptrait Tx[+A] { self => // .. def flatMap[B](f: A => Tx[B]) = transactional { em => f(self.run(em)).run(em) }}

public interface Tx<T> { // .. default public <U> Tx<U> flatMap(Function<T, Tx<U>> f) { return em -> f.apply(run(em)).run(em); }}

Ergebnis von Tx[A] als Eingabe für Tx[B]Tx[B] ersetzt sozusagen Tx[A], führt es aber auch ausGarantiert die selbe TransaktionGarantiert der selbe EntityManager

Vergleich mit Java 7public Tx<List<String>>> findProductNamesFor(final String crit) { return new Tx<List<String>>>() { public List<String> run(EntityManager em) { List<Integer> productIds = findProductIds(crit).run(em); // self.run(em) return findProductNames(productIds).run(em); // f(..).run(em) } };}

Schwer zu lesen, aber nach wie vor nachvollziehbar(?)Vergleich mit BMT: Suboperationen für sich verwendbar

Verwendung von flatMapdef findProductNamesFor(searchCrit: String) = findProductIds(searchCrit) flatMap { productIds => findProductNames(productIds) }

def findProductNamesFor(searchCrit: String) = findProductIds(searchCrit) flatMap findProductNames

def findProductNamesFor(searchCrit: String) = for { productIds <- findProductIds(searchCrit) productNames <- findProductNames(productIds) } yield productNames

Zwei Datenbankqueries in der selben TransaktionWiederum weder EntityManager noch @Transactional (o.ä.) - Kontext(hier Tx) wird wiederverwendet

Monad Definition für Scalazimplicit val txMonad = new scalaz.Monad[Tx] { def point[A](a: => A): Tx[A] = transactional { // EntityManger wird nicht benötigt, kann ignoriert werden. _ => a }

def bind[A, B](fa: Tx[A])(f: A => Tx[B]): Tx[B] = fa flatMap f }

Essenz: Wir hatten die Implementierung praktisch schonGibt Zugriff zu nützlichen Monadic FunctionsMonad Laws beweisen sparen wir uns momentan ..

Monadic Functions (1)// Um die Signatur zu verdeutlichen:def sequenceM[M[_] : Monad, T](ms: List[M[T]]): M[List[T]] = ms.sequencedef zipM[M[_] : Monad, T, U](mt: M[T], mu: M[U]): M[(T, U)] = for { t <- mt u <- mu } yield (t, u)

// In Bezug auf Transaktionen bedeutet das:def joinTransactions[T](transactions: List[Tx[T]]): Tx[List[T]] = sequenceM(transactions)def joinTransactions[T, U](txT: Tx[T], txU: Tx[U]): Tx[(T, U)] = zipM(txT, txU)

Kontext gibt dabei die jeweilige Semantik an

Monadic Functions (2)def replicateM[M[_] : Monad, A](n: Int, ma: M[A]): M[List[A]] = sequenceM(List.fill(n)(ma))

Referential transparency ist ja wieder gegeben

Unterschiedliche Datenbankentrait Tx[+A, Tag] { def run: A def map[B](A => B): Tx[B, Tag] = // ... def flatMap[B, ArgModule >: Module] (f: A => Tx[B, ArgModule]): Tx[B, Module] = // ...}

object Tx { def transactional[A, Tag](body: EntityManager => A) = // ...}

sealed trait Unit1sealed trait Unit2

def txUnit1[A](body: EntityManager => A) = transactional[A, Unit1](body)

def txUnit2[A](body: EntityManager => A) = transactional[A, Unit2](body)

Tx bezieht sich nun auf bestimmte Datenbank

Compiler kennt nun auchDatenbankgrenzen (1)

def findEmployee(employeeId: String) = txUnit1 { /* .. */ }def saveEmployee(employee: Employee) = txUnit2 { /* .. */ }

// Wir wollen die zwei Operationen kombinierendef findAndSaveEmployee(employeeId: String) = findEmployee(employeeId) flatMap saveEmployee

// Error:(..) type mismatch;// found : ..Tx[Unit,..Unit2]// required: ..Tx[?],..Unit1]// findEmployee(employeeId) flatMap saveEmployee// ̂

Modellierung ist einem selbst überlassen - z.B. common Tx für alle?

Compiler kennt nun auchDatenbankgrenzen (2)

// Müssen wir über verschachtelte Transaktionen machendef findAndSaveEmployee(employeeId: String) /*: Tx[Tx[Unit, Unit2], Unit1] */ = findEmployee(employeeId) map saveEmployee

Publikumsfrage: Funktioniert das?@PersistenceContext(unitName = "db1Unit") private EntityManager em1;@PersistenceContext(unitName = "db2Unit") private EntityManager em2;

@Transactional("unit1Manager")@Transactional("unit2Manager") // Employee redundant in zwei Datenbanken speichernpublic void saveEmployee(String employeeName) { em1.persist(new Employee(employeeName)); em2.persist(new Employee(employeeName));}

def saveEmployee(employeeName: String) = ( txUnit1 { em1 => em1.persist(new Employee(employeeName)) }, txUnit2 { em2 => em2.persist(new Employee(employeeName)) })

Wie sicher ist man bei der Antwort dieser Frage?

Demo

Fazit

Ziel einer jeden API: LEGO(tm) BaukastenSeiteneffekte kann man sehr gut einschränkenDeferred Execution / Interpretation, wenn man Seiteneffekte benötigtDer (Scala!) Compiler ist dein Freund!

Q&A

Buchempfehlungen

Buchempfehlungen

Danke

Bernhard Huemer | IRIANbernhard.huemer@irian.at

@bhuemer

Recommended