首页 > 解决方案 > 如何以正确的方式使用 Scala Cats Validated?

问题描述

以下是我的用例

  1. 我正在使用Cats来验证我的配置。我的配置文件在 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 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:上面的配置只是一个例子,我的真实配置可以有其他案例类作为它的成员,它们本身可以依赖于其他案例类。

标签: scalavalidationfunctorscala-cats

解决方案


像 Cats 这样的库所提倡的那种编程的中心目标之一是使无效状态无法表示。在一个完美的世界中,根据这种哲学,不可能创建一个Config带有无效成员数据的实例(通过使用像Refined这样的库,其中复杂的约束可以在类型系统中表达和跟踪,或者简单地通过隐藏不安全的构造函数)。在一个稍微不那么完美的世界中,仍然可以构造 的无效实例Config,但不鼓励,例如通过使用安全构造函数(如您的validatePerson方法 for Person)。

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

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

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


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

Refined 允许您做的是编写这样的方法:

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

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

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


推荐阅读