2016-11-11 1 views
2

Ich versuche, ein DSL für Schreiben Systemtests in Scala zu schreiben. In dieser DSL möchte ich nicht die Tatsache offenlegen, dass einige Operationen asynchron stattfinden können (weil sie zum Beispiel mit dem zu testenden Web-Service implementiert werden), oder dass Fehler auftreten können (weil der Web-Service möglicherweise nicht verfügbar ist) und wir möchten, dass der Test fehlschlägt. In this answer Dieser Ansatz wird abgeraten, aber ich stimme dem im Zusammenhang mit einem DSL zum Schreiben von Tests nicht völlig zu. Ich denke, das DSL wird durch die Einführung dieser Aspekte unnötig verschmutzt.Praktische freie Monaden für System-Tests DSL: Nebenläufigkeit und Fehlerbehandlung

Um die Frage umrahmen, betrachten Sie das folgende DSL:

type Elem = String 

sealed trait TestF[A] 
// Put an element into the bag. 
case class Put[A](e: Elem, next: A) extends TestF[A] 
// Count the number of elements equal to "e" in the bag. 
case class Count[A](e: Elem, withCount: Int => A) extends TestF[A] 

def put(e: Elem): Free[TestF, Unit] = 
    Free.liftF(Put(e,())) 

def count(e: Elem): Free[TestF, Int] = 
    Free.liftF(Count(e, identity)) 

def test0 = for { 
    _ <- put("Apple") 
    _ <- put("Orange") 
    _ <- put("Pinneaple") 
    nApples <- count("Apple") 
    nPears <- count("Pear") 
    nBananas <- count("Banana") 
} yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 

Jetzt nehmen wir einen Dolmetscher implementieren wollen, die Nutzung unseres Dienstes im Test macht zu setzen und die Elemente in den Laden zu verlassen. Da wir das Netzwerk nutzen, möchte ich, dass die put Operationen asynchron ablaufen. Angesichts der Tatsache, dass Netzwerkfehler oder Serverfehler auftreten können, möchte ich außerdem, dass das Programm stoppt, sobald ein Fehler auftritt. Um eine Vorstellung davon zu geben, was ich erreichen möchte, ist here ein Beispiel für das Mischen der verschiedenen Aspekte in Haskell mittels Monade-Transformatoren (die ich nicht in Scala übersetzen kann).

Also meine Frage ist, die Monade M würden Sie für einen Dolmetscher an, die die oben genannten Anforderungen erfüllt:

def interp[A](cmd: TestF[A]): M[A] 

Und falls M ist eine Monade tranformer, wie Sie sie mit der FP-Bibliothek zusammenstellen würde Ihre Wahl (Katzen, Scalaz).

+1

'Task' (scalaz oder besser fs2) alle Anforderungen erfüllen sollte, es Monade-Transformator nicht braucht, da es bereits Entweder innen (entweder für fs2, \/für Scalaz). Es hat auch Fast-Fail-Verhalten, das Sie benötigen, genau wie rechtsgerichtete Disjunktion/Xor. – dk14

+0

Ich wusste nicht über 'Task', nett. Dieser Ansatz scheint auch darauf hinzuweisen, wie Menschen in der scala-Welt Monaden bilden, die 'lift'-Operatoren von Haskell vergessen, eigene Klassen mit allen Aspekten, die Sie benötigen (z. B. Parallelität und Fehlerbehandlung) definieren und eine Monade definieren Beispiel dafür. –

+0

Sie müssen noch etwas heben, wenn Sie 'Task' verwenden, von Wert zu 'Aufgabe' oder von 'Entweder' zu' Aufgabe' heben. Aber ja, es scheint einfacher zu sein als Monadetransformatoren, insbesondere in Bezug auf die Tatsache, dass Monaden kaum zusammensetzbar sind (um Monadtransformatoren zu definieren, müssen Sie einige andere Details über Ihren Typ wissen, abgesehen davon, dass sie eine Monade sind - normalerweise erfordert sie etwas wie comonad um den Wert zu extrahieren). Nur für Werbezwecke würde ich hinzufügen, dass "Task" eine stapel-sichere, trampoline Berechnung darstellt. Es gibt jedoch einige Projekte, die sich auf monadische Kompositionen wie "Emm-monad" konzentrieren – dk14

Antwort

1

Task (scalaz oder besser fs2) sollten alle Anforderungen erfüllen, ist es nicht Monade-Transformator benötigen, wie es schon ist hat Either innen (Either für fs2, \/ für scalaz). Es hat auch ein Fast-Fail-Verhalten, das Sie benötigen, genau wie rechtsbündige Disjunktion/Xor.

Hier sind mehrere Implementierungen sind, die mir bekannt sind:

Unabhängig von Monade-Transformator Abwesenheit, haben Sie noch ein bisschen Heben benötigen, wenn Task mit:

  • von Wert zu Task oder
  • von Either zu Task

Aber ja, es scheint einfacher zu sein als Monadetransformatoren, vor allem im Hinblick darauf, dass Monaden kaum zusammensetzbar sind - um m zu definieren Onad Transformator müssen Sie einige andere Details über Ihren Typ neben einer Monade wissen (in der Regel erfordert es etwas wie comonad, um Wert zu extrahieren).

Nur für Werbezwecke, würde ich auch hinzufügen, dass Task Stack-sichere Trampolin-Berechnung darstellt.

Allerdings gibt es einige Projekte konzentrierten sich auf erweiterte monadischen Zusammensetzung, wie Emm-Monade: https://github.com/djspiewak/emm, so können Sie Monade Transformatoren mit Future/Task, Either, Option, List und so weiter und so fort komponieren. Aber, IMO, es ist immer noch im Vergleich zu Applicative Zusammensetzung begrenzt - cats bietet universellen Nested Datentyp, mit dem leicht zu jeder Applicative komponieren können, finden Sie einige Beispiele - der einzige Nachteil hier ist, dass es schwer ist, eine lesbare DSL mit Applicative zu bauen. Eine andere Alternative ist die sogenannte "Freer-Monade": https://github.com/m50d/paperdoll, die im Grunde eine bessere Zusammensetzung bereitstellt und es ermöglicht, verschiedene Effektschichten in verschiedene Interpreter zu trennen.

Zum Beispiel, wie es kein FutureT/ Transformator nicht Effekte wie type E = Option |: Task |: Base (Option von Task) als solche flatMap würde bauen können von den Future/Task Extraktion von Wert erfordern.

Als Fazit kann ich sagen, dass aus meiner Erfahrung Task wirklich kommt für do-notation-basierte DSLs: Ich hatte eine komplexe externe Regel-DSL für Async-Berechnungen und als ich beschloss, alles auf Scala migrieren Embedded-Version Task wirklich geholfen - ich konvertierte buchstäblich externen DSL zu Scala for-comprehension. Eine andere Sache, die wir in Betracht gezogen haben, ist ein benutzerdefinierter Typ, wie ComputationRule mit einer Reihe von definierten Typklassen und Konvertierungen zu Task/Future oder was auch immer wir brauchen, aber das war, weil wir Free -monad nicht explizit verwendet haben.


Sie könnten sogar brauchen hier nicht Free -monad vorausgesetzt, Sie Dolmetscher keine Fähigkeit benötigen zu wechseln (die für nur Systemtests wahr sein könnte). In diesem Fall Task vielleicht das einzige, was Sie brauchen - es ist faul (im Vergleich mit Zukunft), wirklich funktionell und Stack-safe:

trait DSL { 
    def put[E](e: E): Task[Unit] 
    def count[E](e: E): Task[Int] 
} 

object Implementation1 extends DSL { 

    ...implementation 
} 

object Implementation2 extends DSL { 

    ...implementation 
} 


//System-test script: 

def test0(dsl: DSL) = { 
    import dsl._ 
    for { 
    _ <- put("Apple") 
    _ <- put("Orange") 
    _ <- put("Pinneaple") 
    nApples <- count("Apple") 
    nPears <- count("Pear") 
    nBananas <- count("Banana") 
    } yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 
} 

So können Sie Implementierung wechseln, indem verschiedene „Dolmetscher“ hier:

test0(Implementation1).unsafeRun 
test0(Implementation2).unsafeRun 

Unterschiede/Nachteile (im Vergleich zu http://typelevel.org/cats/datatypes/freemonad.html):

  • Sie mit Task Art stecken, so dass Sie es zu einigen nicht kollabieren o Ther Monade leicht.
  • Implementierung wird in der Laufzeit aufgelöst, wenn Sie eine Instanz von DSL-Eigenschaft übergeben (anstelle von natürlichen Transformation), können Sie es einfach abstrahieren mit Eta-Erweiterung: test0 _. Polymorphe Methoden (put, count) werden natürlich von Java/Scala unterstützt, aber Poly-Funktionen sind nicht so einfach zu übergeben Instanz DSL enthält T => Task[Unit] (für put Betrieb) als die synthetische polymorphe Funktion DSLEntry[T] => Task[Unit] mit Natural-Transformation DSLEntry ~> Task.

  • keine explizite AST als statt Muster innerhalb natürliche Transformation passend - wir verwenden statische Dispatch (explizit eine Methode aufrufen, die faul Berechnung zurück) innerhalb DSL Merkmal

Eigentlich kann man sogar loswerden hier von Task:

trait DSL[F[_]] { 
    def put[E](e: E): F[Unit] 
    def count[E](e: E): F[Int] 
} 

def test0[M[_]: Monad](dsl: DSL[M]) = {...} 

hier So könnte es auch eine Frage der Präferenz besonders, wenn Sie nicht eine Open-Source-Bibliothek zu schreiben.

es Putting alles zusammen:

import cats._ 
import cats.implicits._ 

trait DSL[F[_]] { 
    def put[E](e: E): F[Unit] 
    def count[E](e: E): F[Int] 
} 

def test0[M[_]: Monad](dsl: DSL[M]) = { 
    import dsl._ 
    for { 
     _ <- put("Apple") 
     _ <- put("Orange") 
     _ <- put("Pinneaple") 
     nApples <- count("Apple") 
     nPears <- count("Pear") 
     nBananas <- count("Banana") 
    } yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 
} 

object IdDsl extends DSL[Id] { 
    def put[E](e: E) =() 
    def count[E](e: E) = 5 
} 

Beachten Sie, dass Katzen für Id eine Monad definiert haben, so:

scala> test0(IdDsl) 
res2: cats.Id[List[(String, Int)]] = List((Apple,5), (Pears,5), (Bananas,5)) 

einfach funktioniert. Natürlich können Sie Task/Future/Option oder jede Kombination wählen, wenn Sie bevorzugen. Wie in der Tat, können Sie Applicative statt Monad verwenden:

def test0[F[_]: Applicative](dsl: DSL[F]) = 
    dsl.count("Apple") |@| dsl.count("Pinapple apple pen") map {_ + _ } 

scala> test0(IdDsl) 
res8: cats.Id[Int] = 10 

|@| ein paralleler Operator ist, so dass Sie cats.Validated statt Xor verwenden können, bewusst sein, dass |@| für Aufgabe nicht ausgeführt wird (zumindest in ältere Scalaz-Version) parallel (Paralleloperator nicht gleich Parallelberechnung). Sie können auch eine Kombination aus beidem:

import cats.syntax._ 

def test0[M[_]:Monad](d: DSL[M]) = { 
    for { 
     _ <- d.put("Apple") 
     _ <- d.put("Orange") 
     _ <- d.put("Pinneaple") 
     sum <- d.count("Apple") |@| d.count("Pear") |@| d.count("Banana") map {_ + _ + _} 
    } yield sum 
} 

scala> test0(IdDsl) 
res18: cats.Id[Int] = 15