如何使用 Scala Cats Validated 的正确方法?

How to use Scala Cats Validated the correct way?

以下是我的用例

  1. 我正在使用 Cats 来验证我的配置。我的配置文件在 json.
  2. 我使用 lift-json and then validate it using Cats. I am using this 作为指南将我的配置文件反序列化为我的案例 class Config
  3. 我使用 Cats 的动机是收集验证时存在的所有错误。

我的问题是指南中给出的示例属于

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

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

但在我的案例中,我已经将我的配置反序列化到我的案例中 class(下面是一个示例),然后我将其传递给验证

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 : 上面的配置只是一个例子,我的真实配置可以有其他 case classes 作为其成员,它们本身可以依赖其他 case classes.

像 Cats 这样的图书馆提倡的那种编程的中心目标之一是使无效状态无法表示。在一个完美的世界中,根据这种哲学,不可能用无效的成员数据创建 Config 的实例(通过使用像 Refined 这样的库,其中复杂的约束可以用和表示由类型系统跟踪,或者只是通过隐藏不安全的构造函数)。在一个不太完美的世界中,仍然可以构造 Config 的无效实例,但不鼓励,例如通过使用安全构造函数(比如 PersonvalidatePerson 方法)。

听起来您处在一个更不完美的世界中,您的 Config 实例可能包含也可能不包含无效数据,并且您想验证它们以获得 "new" 个实例您知道的 Config 是有效的。这是完全可能的,并且在某些情况下是合理的,如果您被困在那个不完美的世界中,您的 validateConfig 方法是解决此问题的完全合法的方法。

不过,缺点是编译器无法跟踪已经验证的 Config 实例和尚未验证的实例之间的差异。您的程序中将有 Config 个实例,如果您想知道它们是否已经过验证,则必须追踪它们可能来自的所有地方。在某些情况下,这可能很好,但对于大型或复杂的程序来说,这并不理想。

总结一下:理想情况下,您会在创建 Config 个实例时对其进行验证(甚至可能无法创建无效实例),这样您就不必记住任何给定的 Config 好不好——类型系统可以帮你记住。如果那是不可能的,因为例如您无法控制的 API 或定义,或者如果它对于一个简单的用例来说似乎过于繁琐,那么您使用 validateConfig 所做的事情是完全合理的。


作为脚注,既然您在上面说过您有兴趣在 Refined 中查看更多细节,那么在这种情况下它为您提供的是一种避免更多形状函数的方法 A => ValidationResult[A].例如,现在您的 validateName 方法可能需要一个 String 和 returns 一个 ValidationResult[String]。你可以对这个签名提出与我上面反对 Config => ValidationResult[Config] 完全相同的论点——一旦你处理了结果(通过在 Validated 或其他什么上映射一个函数),你只有一个字符串,并且该类型不会告诉您它已经过验证。

Refined 允许您编写如下方法:

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

…其中 SomeProperty 可能指定最小长度,或者字符串与特定正则表达式匹配的事实等。重要的一点是您没有验证 String 并返回一个只有 知道的 String——你正在验证一个 String 并返回一个 String 编译器 知道一些事情(通过 Refined[A, Prop] 包装器)。

同样,对于您的用例来说,这可能(好吧,可能是)矫枉过正——您可能会发现很高兴知道您可以在程序中进一步推动这一原则(类型中的跟踪验证)。