【问题标题】:How to use Scala Cats Validated the correct way?如何以正确的方式使用 Scala Cats Validated?
【发布时间】:2019-01-24 15:45:14
【问题描述】:

以下是我的用例

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

【问题讨论】:

  • 您对这些列表进行了哪些验证?如果您想要额外的类型级别强制执行,请查看refined
  • @YuvalItzchakov 肯定会调查的。但我描述的方式是否违反了良好做法?
  • 绝对不是。这是验证无法使用类型级别 shananigans 强制执行的解析输入的一种完全有效的方法
  • 感谢 Yuval Itzchakov 提供宝贵的 cmets。

标签: scala validation functor scala-cats


【解决方案1】:

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

听起来您处于一个更不完美的世界中,您拥有可能包含或不包含无效数据的 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]包装器)。 p>

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

【讨论】:

  • 非常感谢。确实是一个详尽的答案。必须学习新事物。 Wish 可以给予更多的支持。
  • 我认为'newtypes'对于这里的配置用例很有用。将您的配置拆分为两个单独的案例类,例如 UnvalidatedConfigConfig。您还可以隐藏Config 的构造函数,以确保它的每个实例都必须由validateConfig 方法创建。
  • @AndrzejSołtysik 同意,尽管我认为这样的类型激增有一个缺点。如果由于某种原因您需要打包输入并在一段时间内未经验证携带它们,是的,一个新的UnvalidatedConfig 是完全合理的。
猜你喜欢
  • 2019-06-04
  • 2017-05-03
  • 2019-03-09
  • 2019-12-24
  • 2017-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多