Статьи

Простая проверка в Scala с использованием Scalaz, Readers и ValidationNel

Я работаю над новой книгой для Packt, в которой показано, как вы можете использовать различные фреймворки Scala для создания сервисов REST. Для этой книги я смотрю на следующий список фреймворков: Finch, Unfiltered, Scalatra, Spray и Akka-HTTP (реактивные потоки).

При написании глав о Finch и Unfiltered мне очень понравилось, как эти две платформы обрабатывали валидацию. Они оба используют составные валидаторы, которые позволяют вам создавать простые в использовании цепочки валидации, которые собирают любые ошибки валидации.

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

Начиная

Полный пример можно найти в следующем Gist. Все фрагменты кода, представленные в этой статье, показаны непосредственно из этого списка. Во-первых, однако, для этого проекта мы, конечно, используем SBT. Поскольку мы будем использовать только Scalaz, у нас есть очень простой файл сборки SBT:

1
2
3
4
5
6
7
name := "scalaz-readers"
 
version := "1.0"
 
scalaVersion := "2.11.7"
 
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.3"

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

1
2
3
4
5
import scalaz._
import Scalaz._
 
object ValidationSample extends App {
}

Прежде чем мы рассмотрим самих читателей, давайте сначала определим наш case-класс и псевдонимы некоторых типов, чтобы упростить понимание:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// lets pretend this is the http request (or any other type of request)
// that can be mapped to a map of values.
type Request = Map[String, String]
val Request = Map[String, String] _
 
// we also define what our reader looks like. In this case
// we define that we want the result to be of type T, and our
// reader expects a Request to process. The result of this reader
// won't be a T, but will be a ValidationNel, which contains
// a list of error messages (as String) or the actual T.
type RequestReader[T] =  Reader[Request, ValidationNel[String, T]]
 
// To experiment, lets say we want to parse the incoming request into a
// Person.
case class Person(firstName: String, lastName: String, age: Int)

Читатель Монада

Прежде чем мы начнем создавать свои собственные читатели, давайте немного подробнее рассмотрим, что позволяет вам делать монада читателей. Мы рассмотрим пример отсюда (отличное введение в Scalaz): http://eed3si9n.com/learning-scalaz-day10

01
02
03
04
05
06
07
08
09
10
11
12
scala> def myName(step: String): Reader[String, String] = Reader {step + ", I am " + _}
myName: (step: String)scalaz.Reader[String,String]
  
scala> def localExample: Reader[String, (String, String, String)] = for {
         a <- myName("First")
         b <- myName("Second") >=> Reader { _ + "dy"}
         c <- myName("Third"
       } yield (a, b, c)
localExample: scalaz.Reader[String,(String, String, String)]
  
scala> localExample("Fred")
res0: (String, String, String) = (First, I am Fred,Second, I am Freddy,Third, I am Fred)

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

Наши читатели

Мы определили наших читателей в качестве вспомогательного объекта, чтобы сделать их использование немного проще. Сначала давайте посмотрим на полный код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * Object which contains our readers (or functions that create readers), just simple readers
 * that check based on type.
 */
object Readers {
 
  def keyFromMap(request: Request, key: String) = request.get(key).map(Success(_)).getOrElse(Failure(s"Key: $key Not found"))
 
  // convert the provided validation containing a throwable, to a validation
  // containing a string.
  def toMessage[S](v: Validation[Throwable, S]): Validation[String, S] = {
    // Returns new Functor of type self, and set value to result of provided function
    ((f: Throwable) => s"Exception: ${f.getClass} msg: ${f.getMessage}")  <-: v
  }
 
  def as[T](key: String)(implicit to: (String) => RequestReader[T]): RequestReader[T] = {
    to(key)
  }
 
  // Create a requestreader for booleanValues.
  implicit def asBool(key: String): RequestReader[Boolean] = {
    Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseBoolean |> toMessage[Boolean] }).toValidationNel)
  }
 
  // Create a requestreader for intvalues.
  implicit def asInt(key: String): RequestReader[Int] = {
    Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseInt |> toMessage[Int] }).toValidationNel)
  }
 
 
  // Create a requestreader for string values.
  implicit def asString(key: String): RequestReader[String] = {
    Reader((request: Request) => keyFromMap(request, key).toValidationNel)
  }
}

Здесь мы определяем один метод как [T] с неявным методом, который возвращает RequestReader указанного типа. Используя имплициты, scala будет использовать один из методов asString, AsInt и т. Д., Чтобы определить, как преобразовать переданный ключ в правильное средство чтения. Давайте посмотрим поближе на функции asInt и keyFromMap.

01
02
03
04
05
06
07
08
09
10
11
12
def keyFromMap(request: Request, key: String) = request.get(key).map(Success(_)).getOrElse(Failure(s"Key: $key Not found"))
 
// convert the provided validation containing a throwable, to a validation
// containing a string.
def toMessage[S](v: Validation[Throwable, S]): Validation[String, S] = {
  // Returns new Functor of type self, and set value to result of provided function
  ((f: Throwable) => s"Exception: ${f.getClass} msg: ${f.getMessage}")  <-: v
}
 
implicit def asInt(key: String): RequestReader[Int] = {
  Reader((request: Request) => keyFromMap(request, key).flatMap({_.parseInt |> toMessage[Int] }).toValidationNel)
}

Функция asInt создает новый Reader и использует переданный запрос для вызова функции keyFromMap. Эта функция пытается получить значение из карты, и, если она успешна, она возвращает Успех, если не Сбой. Затем мы сопоставляем этот результат (применяется только тогда, когда результатом является Успех) и пытаемся преобразовать найденное значение в целое число, используя скалярную функцию parseInt, которая, в свою очередь, также возвращает Validation. Результат этой функции передается в функцию toMessage, которая преобразует Validation [Throwable, S] в Validation [String, s]. Наконец, перед возвратом мы используем функцию toValidationNel для преобразования Validation [String, Int] в ValidationNel [String, Int]. Мы делаем это так, чтобы нам было легче собирать все ошибки вместе.

Создание новой проверки, просто означает создание новой программы чтения, которая возвращает ValidationNel [String, T].

Составление проверок

Теперь давайте посмотрим, как мы можем составить проверки вместе. Для этого мы можем использовать ApplicativeBuilder от Scalaz следующим образом:

1
2
3
4
// This reader doesn't accumulate the validations yet, just returns them as a tuple of Validations
val toTupleReader = (as[String]("first") |@|
  as[String]("last") |@|
  as[Int]("age")).tupled // automatically convert to tuple

Используя | @ | Символ мы объединяем наших читателей, используя скаляр ApplicativeBuilder. Используя функцию tupled, мы возвращаем список кортежей, содержащих индивидуальные результаты наших читателей, после их запуска. Чтобы запустить этот ридер, нам нужно предоставить ему запрос:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// our sample requests. This first one is invalid,
val invalidRequest = Request(Seq(
  "first" -> "Amber",
  "las" -> "Dirksen",
  "age" -> "20 Months"
))
 
// another sample request. This request is valid
val validRequest = Request(Seq(
  "first" -> "Sophie",
  "last" -> "Dirksen",
  "age" -> "5"
))
 
// now we can run our readers by supplying a request.
val tuple3Invalid = toTupleReader.run(invalidRequest) 
val tuple3Valid = toTupleReader.run(validRequest)
println(s"tuple3Invalid:\n $tuple3Invalid ")
println(s"tuple3valid:\n $tuple3Valid ")

Результат этих вызовов выглядит следующим образом:

1
2
3
4
tuple3Invalid:
 (Success(Amber),Failure(NonEmptyList(Key: last Not found)),Failure(NonEmptyList(Exception: class java.lang.NumberFormatException msg: For input string: "20 Months")))
tuple3valid:
 (Success(Sophie),Success(Dirksen),Success(5))

Даже несмотря на то, что этот подход уже позволяет нам создавать и составлять проверки и возвращать отдельные успехи и неудачи, все равно потребуется некоторая работа, чтобы получить либо ошибки, либо преобразовать значения в наш класс дел. К счастью, мы также можем легко собрать успехи и неудачи, так как мы используем объект ValidationNel:

1
2
3
4
5
// This reader converts to a Success(person) or a failure Nel
val toPersonReader = (as[String]("first") |@|
  as[String]("last") |@|
  as[Int]("age"))
  .apply((a, b, c) => (a |@| b |@| c ).apply(Person) ) // manually convert to case class

Когда этот читатель запущен, каждая отдельная проверка будет применена и передана в предоставленную функцию применения. В этой функции мы собираем проверки, используя | @ | конструктор. Это вернет Сбой, содержащий собранные ошибки, или созданное лицо, если все проверки пройдены успешно:

1
2
3
4
val personInvalid = toPersonReader.run(invalidRequest)
val personValid = toPersonReader.run(validRequest)
println(s"personInvalid:\n $personInvalid ")
println(s"personValid:\n $personValid ")

Что приводит к этому:

1
2
3
4
personInvalid:
 Failure(NonEmptyList(Key: last Not found, Exception: class java.lang.NumberFormatException msg: For input string: "20 Months"))
personValid:
 Success(Person(Sophie,Dirksen,5))

Круто верно! Таким образом, мы либо получим успех, содержащий наш доменный объект, либо один объект Failure содержит список ошибок.

Заключительная часть, которую я хотел бы показать, — это альтернативный способ сбора проверок. В предыдущем примере мы использовали | @ | Синтаксис, вы также можете напрямую создать Applicative и использовать его для сбора проверок:

01
02
03
04
05
06
07
08
09
10
// we can further process the tuple using an applicative builder |@|, or
// we can use the Applicative.apply function like this:
 
// we need to use a type lambda, since we use a higher order function
val V = Applicative[({type λ[α]=ValidationNel[String, α]})#λ]
val appValid: ValidationNel[String, Person] = V.apply3(tuple3Valid._1, tuple3Valid._2, tuple3Valid._3)(Person)
val appInvalid: ValidationNel[String, Person] = V.apply3(tuple3Invalid._1, tuple3Invalid._2, tuple3Invalid._3)(Person)
   
println(s"applicativeInvalid:\n $appInvalid")
println(s"applicativeValid:\n $appValid")

И результат этой функции:

1
2
3
4
applicativeInvalid:
 Failure(NonEmptyList(Key: last Not found, Exception: class java.lang.NumberFormatException msg: For input string: "20 Months"))
applicativeValid:
 Success(Person(Sophie,Dirksen,5))

Вот и все. Подведем итог основным моментам:

  • Используя шаблон читателя, очень легко передать некоторый контекст в функции для обработки.
  • С аппликативным строителем или | @ | Символ, вы можете легко составить читателей, которые продолжат, даже если первый из них потерпит неудачу.
  • Используя validationNel, мы можем легко собирать различные результаты проверки и либо возвращать собранные ошибки, либо возвращать один успех.