Подтвердить что ты не робот

Проверка параметров метода в Scala, для понимания и монад

Я пытаюсь проверить параметры метода для nullity, но я не нашел решение...

Может ли кто-нибудь сказать мне, как это сделать?

Я пробую что-то вроде этого:

  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] <- Option(description).toRight("Description is mandatory for a normal category").left.toOption
    } yield errors
    errors match {
      case Some(errorString) => Left( Error(Error.FORBIDDEN,errorString) )
      case None =>  Right( buildTrashCategory(user) )
    }
  }
4b9b3361

Ответ 1

Если вы хотите использовать Scalaz, у него есть несколько инструментов, которые делают эту задачу более удобной, в том числе новый класс Validation и некоторые полезные экземпляры класса класса с прямым смещением для простого старого scala.Either. Я приведу пример каждого из них.

Накопление ошибок с помощью Validation

Сначала для нашего импорта Scalaz (обратите внимание, что нам нужно скрыть scalaz.Category, чтобы избежать конфликта имен):

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

Я использую Scalaz 7 для этого примера. Вам нужно будет внести некоторые незначительные изменения в использование 6.

Я предполагаю, что у нас есть эта упрощенная модель:

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

Далее я определяю следующий метод проверки, который вы можете легко адаптировать, если перейти к подходу, который не включает проверку нулевых значений:

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

Часть Nel означает "непустой список", a ValidationNel[String, A] по существу совпадает с Either[List[String], A].

Теперь мы используем этот метод для проверки наших аргументов:

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)

Обратите внимание, что Validation[Whatever, _] не является монадой (например, по обсуждаемым причинам здесь), но ValidationNel[String, _] - прикладной функтор, и мы используя этот факт здесь, когда мы "поднимем" Category.apply на него. Дополнительную информацию об аппликативных функторах см. В приложении ниже.

Теперь, если мы напишем что-то вроде этого:

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

Мы получим сбой с накопленными ошибками:

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

Если все аргументы были проверены, вместо этого у нас будет Success с Category.

Не удалось быстро с Either

Одним из удобных способов использования аппликативных функторов для проверки является легкость, с которой вы можете поменять свой подход к обработке ошибок. Если вы хотите свалить первый, а не накапливать их, вы можете просто изменить свой метод nonNull.

Нам нужен немного другой набор импорта:

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

Но нет необходимости менять классы case выше.

Здесь наш новый метод проверки:

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

Почти идентичен приведенному выше, за исключением того, что мы используем Either вместо ValidationNEL, а пример прикладного функционала по умолчанию, который Scalaz предоставляет для Either, не накапливает ошибки.

Это все, что нам нужно сделать, чтобы получить желаемое быстрое выполнение - никакие изменения не нужны нашему методу buildCategory. Теперь, если мы напишем это:

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

Результат будет содержать только первую ошибку:

Left(Parent category is mandatory for a normal category)

Точно так, как мы хотели.

Приложение: Краткое введение в аппликативные функторы

Предположим, что у нас есть метод с единственным аргументом:

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

Предположим также, что мы хотим применить этот метод к некоторому x: Option[Int] и получить обратно Option[Int]. Тот факт, что Option является функтором и поэтому обеспечивает метод map, делает это простым:

val xi = x map incremented

Мы "подняли" incremented в функтор Option; то есть мы по существу изменили отображение функции Int на Int на одно отображение Option[Int] на Option[Int] (хотя синтаксис мутирует немного вверх - метафору "лифтинга" гораздо яснее на языке, таком как Haskell).

Теперь предположим, что мы хотим применить следующий метод add к x и y аналогичным образом.

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.

Тот факт, что Option является функтором, недостаточно. Тот факт, что это монада, однако, мы можем использовать flatMap, чтобы получить то, что хотим:

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

Или, что эквивалентно:

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

В некотором смысле, монадность Option является излишней для этой операции. Там более простая абстракция, называемая прикладным функтором, - это промежуточный функтор и монада, который обеспечивает все необходимое оборудование.

Обратите внимание, что это промежуточный в формальном смысле: каждая монада является прикладным функтором, каждый прикладной функтор является функтором, но не каждый прикладной функтор является монадой и т.д.

Scalaz дает нам экземпляр прикладного функтора для Option, поэтому мы можем написать следующее:

import scalaz._, std.option._, syntax.apply._

val xy = (x |@| y)(add)

Синтаксис немного странный, но концепция не сложнее, чем примеры функтора или монады выше - мы просто поднимаем add в аппликативный функтор. Если бы у нас был метод f с тремя аргументами, мы могли бы написать следующее:

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

И так далее.

Так зачем вообще заниматься аппликативными функторами, когда у нас есть монады? Во-первых, просто невозможно предоставить экземпляры monad для некоторых абстракций, с которыми мы хотим работать - Validation - прекрасный пример.

Второй (и связанный с этим), это просто твердая практика разработки, чтобы использовать наименее мощную абстракцию, которая выполнит свою работу. В принципе это может позволить оптимизацию, которая в противном случае была бы невозможна, но что более важно, это делает код, который мы пишем, более многократно используется.

Ответ 2

Я полностью поддерживаю предложение Бена Джеймса сделать обертку для нулевого производящего api. Но вы будете иметь такую ​​же проблему при написании этой обертки. Итак, вот мои предложения.

Почему монады почему для понимания? Внедрение ИМО. Вот как вы могли это сделать:

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")
    )

Или, если вы настаиваете на том, чтобы сообщение об ошибке сохраняло имя параметра, вы можете сделать следующее, что потребует немного большего шаблона:

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(", ")
      )
    )
  }

Ответ 3

Если вам нравится аппликативный функторный подход ответа @Travis Brown, но вам не нравится синтаксис Scalaz или иначе просто не хотите использовать Scalaz, вот простая библиотека, которая обогащает стандартную библиотеку. Любой класс должен действовать как подтверждение аппликативного функтора: https://github.com/youdevise/eithervalidation

Например:

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)))
}

Другими словами, эта функция вернет право, содержащее вашу категорию, если все Eithers были правами, или он вернет Left, содержащий список всех ошибок, если один или несколько были левыми.

Обратите внимание на более сильное синтаксис Scala -ish и менее Haskell-ish и меньшую библиотеку;)

Ответ 4

Предположим, что вы закончили Либо со следующими быстрыми и грязными вещами:

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))
    }
  }
}
}}

рассмотрим функцию проверки, возвращающую Либо:

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

конструктор curryfied, возвращающий кортеж:

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

Вы можете проверить его с помощью:

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

Неважно.