validation der Methodenparameter in Scala, mit Verständnis und Monaden

Ich versuche, die Parameter einer Methode für Nichtigkeit zu validieren, aber ich finde die Lösung nicht …

Kann mir jemand sagen, wie es geht?

Ich versuche so etwas:

def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[Error,Category] = { val errors: Option[String] = for { _ <- Option(user).toRight("User is mandatory for a normal category").right _ <- Option(parent).toRight("Parent category is mandatory for a normal category").right _ <- Option(name).toRight("Name is mandatory for a normal category").right errors : Option[String]  Left( Error(Error.FORBIDDEN,errorString) ) case None => Right( buildTrashCategory(user) ) } } 

   

Wenn Sie Scalaz verwenden möchten , gibt es eine Handvoll Werkzeuge, die diese Art von Aufgabe komfortabler machen, einschließlich einer neuen Validation und einiger nützlicher classninstanzen mit richtiger scala.Either für einfache alte scala.Either . Ich werde ein Beispiel von jedem hier geben.

Accumulating errors with Validation

Zuerst für unsere Scalaz-Importe (beachten Sie, dass wir scalaz.Category verstecken scalaz.Category , um den scalaz.Category zu vermeiden):

 import scalaz.{ Category => _, _ } import syntax.apply._, syntax.std.option._, syntax.validation._ 

Ich benutze Scalaz 7 für dieses Beispiel. Sie müssen einige kleinere Änderungen vornehmen, um 6 zu verwenden.

Ich nehme an, wir haben dieses vereinfachte Modell:

 case class User(name: String) case class Category(user: User, parent: Category, name: String, desc: String) 

Als Nächstes definiere ich die folgende validationsmethode, die Sie leicht anpassen können, wenn Sie zu einem Ansatz übergehen, der nicht nach Nullwerten sucht:

 def nonNull[A](a: A, msg: String): ValidationNel[String, A] = Option(a).toSuccess(msg).toValidationNel 

Der Nel Teil steht für “nicht leere Liste”, und eine ValidationNel[String, A] ist im Wesentlichen die gleiche wie eine Either[List[String], A] .

Jetzt verwenden wir diese Methode, um unsere Argumente zu überprüfen:

 def buildCategory(user: User, parent: Category, name: String, desc: String) = ( nonNull(user, "User is mandatory for a normal category") |@| nonNull(parent, "Parent category is mandatory for a normal category") |@| nonNull(name, "Name is mandatory for a normal category") |@| nonNull(desc, "Description is mandatory for a normal category") )(Category.apply) 

Beachten Sie, dass Validation[Whatever, _] keine Monade ist (aus hier diskutierten Gründen), aber ValidationNel[String, _] ist ein anwendungsspezifischer Funktor, und wir verwenden diese Tatsache hier, wenn wir Category.apply “heben” Category.apply Sie sich darauf an. Weitere Informationen zu anwendungsspezifischen Funktoren finden Sie im Anhang.

Wenn wir jetzt so etwas schreiben:

 val result: ValidationNel[String, Category] = buildCategory(User("mary"), null, null, "Some category.") 

Wir werden einen Fehler mit den aufgelaufenen Fehlern bekommen:

 Failure( NonEmptyList( Parent category is mandatory for a normal category, Name is mandatory for a normal category ) ) 

Wenn alle Argumente ausgecheckt wären, hätten wir stattdessen einen Success mit einem Category .

Failing schnell mit Either

Einer der praktischen Aspekte bei der Verwendung von anwendungsspezifischen Funktoren zur validation ist die Leichtigkeit, mit der Sie Ihre Herangehensweise an die Fehlerbehandlung auslagern können. Wenn Sie beim ersten Mal scheitern wollen, anstatt sie zu akkumulieren, können Sie im Wesentlichen nur Ihre nonNull Methode ändern.

Wir brauchen eine etwas andere Menge von Importen:

 import scalaz.{ Category => _, _ } import syntax.apply._, std.either._ 

Aber Sie müssen die oben genannten Fallklassen nicht ändern.

Hier ist unsere neue validationsmethode:

 def nonNull[A](a: A, msg: String): Either[String, A] = Option(a).toRight(msg) 

Fast identisch mit dem obigen, außer dass wir Either ValidationNEL anstelle von ” ValidationNEL und die standardmäßige “applicative funktor instance”, die Scalaz für “Even” bereitstellt, keine Fehler akkumuliert.

Das ist alles, was wir tun müssen, um das gewünschte Fail-Fast-Verhalten zu erreichen – für unsere buildCategory Methode sind keine Änderungen erforderlich. Wenn wir das jetzt schreiben:

 val result: Either[String, Category] = buildCategory(User("mary"), null, null, "Some category.") 

Das Ergebnis enthält nur den ersten Fehler:

 Left(Parent category is mandatory for a normal category) 

Genau wie wir es wollten.

Anhang: Schnelle Einführung in anwendungsbezogene Funktoren

Angenommen, wir haben eine Methode mit einem einzigen Argument:

 def incremented(i: Int): Int = i + 1 

Und nehmen wir auch an, dass wir diese Methode auf einige x: Option[Int] anwenden wollen x: Option[Int] und eine Option[Int] zurückbekommen. Die Tatsache, dass Option ein Funktor ist und daher eine Kartenmethode bereitstellt, macht dies einfach:

 val xi = x map incremented 

Wir haben “gehoben” in den Option Funktor incremented ; Das heißt, wir haben im Wesentlichen eine function geändert, die Int in Int in eine Zuordnungsoption Option[Int] in Option[Int] umwandelt (obwohl die Syntax dies etwas durcheinanderbringt – die “anhebende” Metapher ist in einer Sprache wie Haskell viel klarer) .

Nun nehmen wir an, wir wollen die folgende add Methode auf ähnliche Weise auf x und y anwenden.

 def add(i: Int, j: Int): Int = i + j val x: Option[Int] = users.find(_.name == "John").map(_.age) val y: Option[Int] = users.find(_.name == "Mary").map(_.age) // Or whatever. 

Die Tatsache, dass Option ein Funktor ist, ist nicht genug. Die Tatsache, dass es eine Monade ist, ist jedoch, und wir können flatMap , um zu bekommen, was wir wollen:

 val xy: Option[Int] = x.flatMap(xv => y.map(add(xv, _))) 

Oder gleichwertig:

 val xy: Option[Int] = for { xv < - x; yv <- y } yield add(xv, yv) 

In gewisser Hinsicht ist die Monadness von Option jedoch für diese Operation übertrieben. Es gibt eine einfachere Abstraktion - einen anwendungsorientierten Funktor genannt -, der zwischen einem Funktor und einer Monade liegt und all die Maschinerie bereitstellt, die wir brauchen.

Beachten Sie, dass es in einem formalen Sinn dazwischen liegt : Jede Monade ist ein anwendungsbezogener Funktor, jeder anwendungsbezogene Funktor ist ein Funktor, aber nicht jeder anwendbare Funktor ist eine Monade usw.

Scalaz gibt uns eine anwendungsspezifische Funktorinstanz für Option , damit wir folgendes schreiben können:

 import scalaz._, std.option._, syntax.apply._ val xy = (x |@| y)(add) 

Die Syntax ist ein wenig merkwürdig, aber das Konzept ist nicht komplizierter als die obigen funktor- oder monad-Beispiele - wir heben nur das add zum anwendungsspezifischen Funktor auf. Wenn wir eine Methode f mit drei Argumenten hätten, könnten wir folgendes schreiben:

 val xyz = (x |@| y |@| z)(f) 

Und so weiter.

Warum also mit anwendungsbezogenen Funktoren überhaupt umgehen, wenn wir Monaden haben? Zunächst einmal ist es einfach nicht möglich, für einige der Abstraktionen, mit denen wir arbeiten wollen, Monaden-Instanzen zur Verfügung zu stellen - Validation ist das perfekte Beispiel.

Zweitens (und damit zusammenhängend) ist es nur eine solide Entwicklungspraxis, die am wenigsten starke Abstraktion zu verwenden, die den Job erledigt. Im Prinzip kann dies Optimierungen ermöglichen, die sonst nicht möglich wären, aber was noch wichtiger ist, macht den Code, den wir schreiben, wiederverwendbar.

Ich unterstütze den Vorschlag von Ben James, einen Wrapper für die Null-produzierende API zu erstellen. Aber Sie haben immer noch das gleiche Problem beim Schreiben dieses Wrappers. Also hier sind meine Vorschläge.

Warum Monaden warum zum Verständnis? Ein Überkompilierungs-IMO. Hier ist, wie Sie das tun könnten:

 def buildNormalCategory ( user: User, parent: Category, name: String, description: String ) : Either[ Error, Category ] = Either.cond( !Seq(user, parent, name, description).contains(null), buildTrashCategory(user), Error(Error.FORBIDDEN, "null detected") ) 

Oder wenn Sie darauf bestehen, dass die Fehlernachricht den Namen des Parameters speichert, könnten Sie Folgendes tun, was ein wenig mehr Vortex erfordern würde:

 def buildNormalCategory ( user: User, parent: Category, name: String, description: String ) : Either[ Error, Category ] = { val nullParams = Seq("user" -> user, "parent" -> parent, "name" -> name, "description" -> description) .collect{ case (n, null) => n } Either.cond( nullParams.isEmpty, buildTrashCategory(user), Error( Error.FORBIDDEN, "Null provided for the following parameters: " + nullParams.mkString(", ") ) ) } 

Wenn Sie den Ansatz des anwendungsorientierten Funktors von @Travis Browns Antwort mögen, aber die Scalaz-Syntax nicht mögen oder Scalaz einfach nicht verwenden möchten, ist hier eine einfache Bibliothek, die die Standardbibliothek bereichert. Jede class als Anwendung Funktorvalidierung: https://github.com/youdevise/eithervalidation

Beispielsweise:

 import com.youdevise.eithervalidation.EitherValidation.Implicits._ def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[List[Error], Category] = { val validUser = Option(user).toRight(List("User is mandatory for a normal category")) val validParent = Option(parent).toRight(List("Parent category is mandatory for a normal category")) val validName = Option(name).toRight(List("Name is mandatory for a normal category")) Right(Category)(validUser, validParent, validName). left.map(_.map(errorString => Error(Error.FORBIDDEN, errorString))) } 

Mit anderen Worten, diese function gibt ein Right zurück, das Ihre Kategorie enthält, wenn alle Eithers Rechte waren, oder es gibt eine Linken zurück, die eine Liste aller Fehler enthält, wenn eine oder mehrere Lefts waren.

Beachten Sie die wohl mehr Scala-ish und weniger Haskell-ish-Syntax und eine kleinere Bibliothek;)

Nehmen wir an, Sie haben entweder mit den folgenden schnellen und schmutzigen Sachen abgeschlossen:

 object Validation { var errors = List[String]() implicit class Either2[X] (x: Either[String,X]){ def fmap[Y](f: X => Y) = { errors = List[String]() //println(s"errors are $errors") x match { case Left(s) => {errors = s :: errors ; Left(errors)} case Right(x) => Right(f(x)) } } def fapply[Y](f: Either[List[String],X=>Y]) = { x match { case Left(s) => {errors = s :: errors ; Left(errors)} case Right(v) => { if (f.isLeft) Left(errors) else Right(f.right.get(v)) } } } }} 

Betrachten Sie eine validationsfunktion, die ein Entweder zurückgibt:

  def whenNone (value: Option[String],msg:String): Either[String,String] = if (value isEmpty) Left(msg) else Right(value.get) 

ein curryfied-Konstruktor, der ein Tupel zurückgibt:

  val me = ((user:String,parent:String,name:String)=> (user,parent,name)) curried 

Sie können es validieren mit:

  whenNone(None,"bad user") .fapply( whenNone(Some("parent"), "bad parent") .fapply( whenNone(None,"bad name") .fmap(me ) )) 

Keine große Sache.