如何使用经验证的Scala Cats正确方法?

时间:2019-01-24 15:45:14

标签: scala validation functor scala-cats

以下是我的用例

  1. 我正在使用Cats来验证我的配置。我的配置文件在json中。
  2. 我使用lift-json将配置文件反序列化为案例类Config,然后使用Cats对其进行验证。我正在使用this作为指导。
  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 }。因为我已经有了一个配置文件,如果一切都有效,所以我不想从其成员那里重新创建配置文件。这是由于我通过config验证功能而不是其成员而引起的。

我工作场所的一个人告诉我这不是正确的方法,因为Cats Validated提供了一种在对象有效时构造对象的方法。如果对象的成员无效,则该对象不应存在或不可构造。这对我来说完全有意义。

那我应该做些改变吗?以上我正在接受吗?

PS:上面的Config只是一个示例,我的实际配置中可以包含其他case类作为其成员,而这些case类本身可以依赖于其他case类。

1 个答案:

答案 0 :(得分:17)

像Cats这样的库提倡的那种编程的主要目标之一是使无效状态无法表示。根据这种理念,在理想情况下,不可能使用无效的成员数据创建Config的实例(通过使用类似Refined的库,可以在其中表达复杂的约束,由类型系统跟踪,或仅通过隐藏不安全的构造函数进行跟踪)。在稍微不太完美的世界中,仍然有可能构造Config的无效实例,但建议不要这样做,例如通过使用安全的构造函数(例如validatePerson的{​​{1}}方法)。

这听起来像是您处在一个更加不完美的世界中,其中有Person实例可能包含或可能不包含无效数据,并且您想验证它们以获取{{1 }},您知道这是有效的。这是完全有可能的,并且在某些情况下是合理的,并且如果您陷在这个不完美的世界中,则Config方法是解决此问题的一种完全合法的方法。

但是,不利之处在于,编译器无法跟踪已经验证的Config实例和尚未验证的实例之间的差异。您的程序中将有validateConfig个实例,如果您想知道它们是否已经通过验证,则必须跟踪它们可能来自的所有位置。在某些情况下,这可能还不错,但是对于大型或复杂程序而言,这并不理想。

总结:理想情况下,每创建一个Config实例时,您都将对其进行验证(甚至可能无法创建无效的实例),这样您就不必记住是否有给定的{{1} }是好还是不好-类型系统可以为您记住。如果不可能,例如您无法控制的API或定义,或者对于一个简单的用例而言,如果它似乎太繁琐,那么您使用Config所做的事情是完全合理的。


作为脚注,由于您在上面已经说过,您有兴趣在Refined上进行详细介绍,因此在这种情况下,它为您提供了避免出现Config形状的更多功能的方法。例如,现在您的Config方法可能需要一个validateConfig并返回一个A => ValidationResult[A]。您可以对此签名进行与我上面针对validateName相同的参数-一旦使用结果(通过将函数映射到String或其他方式),您只有一个字符串,并且类型不会告诉您它已经被验证。

Refined允许您执行的操作是编写这样的方法:

ValidationResult[String]

…其中Config => ValidationResult[Config]可能指定最小长度,或者字符串匹配特定的正则表达式,等等。重要的是,您不是在验证Validated并返回{ {1}},只有知道一些有关信息,您正在验证def validateName(in: String): ValidationResult[Refined[String, SomeProperty]] = ... 并返回编译器知道的SomeProperty信息(通过String包装器。

同样,对于您的用例来说,这可能是(好吧,可能是)过大的杀伤力,您可能会发现,可以在程序中进一步推动这一原理(对类型进行跟踪验证),您会发现这很高兴。