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

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

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

Я пытаюсь что-то вроде этого:

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

person Sebastien Lorber    schedule 06.09.2012    source источник
comment
Было бы более идиоматично написать оболочку вокруг любой библиотеки, возвращающей возможные нулевые ссылки. В противном случае можно представить себе дублирование такого рода проверки почти в каждом методе.   -  person Ben James    schedule 07.09.2012
comment
Я согласен с @BenJames. Очень полезно полностью избегать работы с null в Scala и вместо этого использовать Option[..], где это уместно.   -  person Petr    schedule 07.09.2012
comment
@BenJames мои значения взяты из парсера Play2 json: JsValue. Правильно, кажется, я мог бы сделать более элегантное решение, но я изучаю Scala, и меня все еще интересует ответ;)   -  person Sebastien Lorber    schedule 07.09.2012
comment
@PetrPudlák, я полностью согласен, но если я работаю с параметрами, это означает, что моя подпись метода должна будет отображать параметры ... разве это не проблема, поскольку все эти значения не являются необязательными?   -  person Sebastien Lorber    schedule 07.09.2012
comment
Функции, работающие с простыми значениями, могут быть подняты для работы в течение Optionс. Посмотрите, как это сделать с библиотекой Scalaz, если вам интересно. В противном случае просто примените метод внутри for-comprehension.   -  person Ben James    schedule 07.09.2012


Ответы (4)


Если вы хотите использовать 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 означает «непустой список», а 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)

И так далее.

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

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

person Travis Brown    schedule 06.09.2012
comment
Спасибо. А что, если я хочу вернуть только первую обнаруженную ошибку и не выполнять другие операторы? - person Sebastien Lorber; 07.09.2012
comment
Спасибо. Кстати, что такое |@| присутствует в вашем коде? это функция, добавленная Scalaz к Либо? - person Sebastien Lorber; 07.09.2012
comment
Это синтаксис аппликативного конструктора. Короче говоря, если у нас есть функция f, которая принимает n аргументов, (x_1 |@| x_2 |@| ... |@| x_n)(f) — это функция, поднятая до некоторого аппликативного функтора. Я знаю, что это, вероятно, ужасно неясно - я постараюсь обновить ответ, когда у меня будет пара минут. - person Travis Brown; 08.09.2012
comment
@SebastienLorber: см. приложение. - person Travis Brown; 09.09.2012
comment
@Travis Brown Есть ли простой способ запретить клиенту напрямую использовать класс case? Например, я хочу запретить использование: Category(myUser, myParent, myName, myDesc). Действительно, я хочу принудительно выполнить проверку перед созданием объекта, чтобы заставить его всегда использовать метод buildCategory. - person Mik378; 21.02.2013
comment
@Mik378: В этой ситуации я бы сделал Category запечатанным свойством с классом частного случая, который его расширяет (и с buildCategory, явно типизированным как ValidationNEL[Error, Category]). Таким образом, вы получаете удобство класса case внутри вашего объекта проверки, но у вас есть контроль над тем, как люди могут создавать новые экземпляры Category. - person Travis Brown; 21.02.2013
comment
@Travis Brown, расширенный класс черты и частного случая, он должен называться как Category с вашим решением? Я бы переименовал запечатанную черту в CategoryBuilder, не так ли? В противном случае в подписи: ValidationNEL[Error, Category], Category будут относиться к признаку, а не к классу случая. - person Mik378; 22.02.2013
comment
См. этот пример — идея состоит в том, что посторонние видят только черту, возвращаемый тип необходим, чтобы предотвратить утечку класса частного случая. - person Travis Brown; 22.02.2013
comment
Что делать, если я не хочу подтверждать parent? В приведенном выше коде я не смогу поднять Category.apply, если я удалю родителя из цепочки проверки. Как я могу обойти это? Спасибо - person Mutaz; 21.05.2016

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

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

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(", ")
      )
    )
  }
person Nikita Volkov    schedule 06.09.2012
comment
Нет ничего необычного в желании накапливать ошибки, когда вы выполняете такую ​​проверку (чтобы вы могли, например, сообщить пользователю все, что он сделал неправильно), и этот упрощенный подход на самом деле не поддерживает это чисто. - person Travis Brown; 07.09.2012
comment
@TravisBrown Имеет смысл. Я обновил ответ, чтобы покрыть это. - person Nikita Volkov; 07.09.2012
comment
На самом деле я хотел бы избежать накопления ошибок и возвращать только первую обнаруженную ошибку, не выполняя проверку других параметров. - person Sebastien Lorber; 07.09.2012
comment
@SebastienLorber Тогда простое изменение collect на collectFirst поможет вам - person Nikita Volkov; 07.09.2012

Если вам нравится подход аппликативного функтора в ответе @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)))
}

Другими словами, эта функция вернет Право, содержащее вашу Категорию, если все Либо были Правами, или вернет Лево, содержащее Список всех Ошибок, если одна или несколько были Левыми.

Обратите внимание на синтаксис, возможно, более похожий на Scala и менее на Haskell, а также на меньшую библиотеку;)

person ms-tg    schedule 27.02.2013

Давайте предположим, что вы завершили «Либо» со следующими быстрыми и грязными вещами:

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)

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

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

Не ахти какое дело.

person wiki1000    schedule 12.11.2014
comment
Извините за ужасную изменяемую переменную, явно установленную и переинициированную при необходимости, но дело в том, что есть побочный эффект, который нужно реализовать, и, безусловно, есть лучшие способы его кодирования. - person wiki1000; 19.11.2014