Как правильно использовать Scala Cats Validated? - PullRequest
0 голосов
/ 24 января 2019

Ниже приведен мой вариант использования

  1. Я использую Кошки для проверки моей конфигурации. Мой конфигурационный файл в формате json.
  2. Я десериализирую свой конфигурационный файл в свой класс дел Config, используя lift-json , а затем проверяю его, используя Cats. Я использую этот в качестве руководства.
  3. Мой мотив использования Cats - собирать все ошибки, если они присутствуют на момент проверки.

Моя проблема в приведенных в руководстве примерах типа

case class Person(name: String, age: Int)

def validatePerson(name: String, age: Int): ValidationResult[Person] = {
   (validateName(name),validate(age)).mapN(Person)
}

Но в моем случае я уже десериализовал свою конфигурацию в свой класс case (ниже приведен пример), а затем я передаю ее для проверки

case class Config(source: List[String], dest: List[String], extra: List[String])

def vaildateConfig(config: Config): ValidationResult[Config] = {
  (validateSource(config.source), validateDestination(config.dest))
   .mapN { case _ => config }
}

Разница здесь mapN { case _ => config }. Поскольку у меня уже есть конфиг, если все верно, я не хочу создавать конфиг заново из его членов. Это возникает, когда я передаю конфигурацию для проверки функции, а не ее членов.

Человек на моем рабочем месте сказал мне, что это неправильный способ, поскольку Cats Validated предоставляет способ создания объекта, если его члены действительны. Объект не должен существовать или не должен быть конструктивным, если его члены недопустимы. Что имеет для меня полный смысл.

Так я должен внести какие-либо изменения? Вышеизложенное я делаю приемлемо?

PS: Приведенный выше конфиг является лишь примером, мой реальный конфиг может иметь другие классы дел в качестве своих членов, которые сами могут зависеть от других классов дел.

1 Ответ

0 голосов
/ 24 января 2019

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

Похоже, что вы находитесь в еще менее совершенном мире, где у вас есть экземпляры Config, которые могут содержать или не содержать недействительные данные, и вы хотите проверить их, чтобы получить "новые" экземпляры Config, которые вы знать действительны. Это вполне возможно, а в некоторых случаях разумно, и ваш метод validateConfig является совершенно законным способом решения этой проблемы, если вы застряли в этом несовершенном мире.

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

Подводя итог: в идеале вы должны проверять Config экземпляры всякий раз, когда они создаются (возможно, даже делая невозможным создание недействительных), так что вам не нужно помнить, хорош ли какой-либо данный Config или нет - система типов может запомнить для вас. Если это невозможно, например, из-за API или определения, которые вы не контролируете, или если это просто кажется слишком обременительным для простого варианта использования, то то, что вы делаете с validateConfig, совершенно разумно.


В качестве сноски, так как вы говорите выше, что вам интересно более детально взглянуть на Refined, то, что она предоставляет вам в подобной ситуации, - это способ избежать еще большего числа функций формы A => ValidationResult[A]. Прямо сейчас ваш validateName метод, например, вероятно, принимает String и возвращает ValidationResult[String]. Вы можете сделать точно такой же аргумент против этой подписи, как у меня против Config => ValidationResult[Config] выше - как только вы работаете с результатом (сопоставляя функцию с Validated или чем-то еще), у вас просто есть строка и тип не говорит вам, что это уже было подтверждено.

То, что Refined позволяет вам сделать, это написать такой метод:

def validateName(in: String): ValidationResult[Refined[String, SomeProperty]] = ...

… где SomeProperty может указывать минимальную длину или тот факт, что строка соответствует определенному регулярному выражению и т. Д. Важным моментом является то, что вы не проверяете String и возвращаете String, который только вы знаете что-то о - вы проверяете String и возвращаете String, о котором компилятор знает что-то (через оболочку Refined[A, Prop]).

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

...