为何Validation会违背monad定律?解析Scalaz等框架中Validation非monad的原因
为什么Scalaz/Cats的Validation(或Kotlin Arrow的Validation)无法成为Monad?
这是个非常戳中要点的问题!核心原因其实很直白:Validation的设计目标和Monad的契约完全冲突——一个要「收集所有验证错误」,一个要「遇到第一个失败就终止执行」,两者根本没法兼容。
具体拆解下两者的行为差异:
作为Applicative Functor的Validation:它的核心价值就是批量执行所有验证逻辑,把所有失败的错误汇总返回。比如在Scala Cats里用
mapN组合多个校验时,不管前面的校验有没有失败,后面的都会跑完,最后把所有错误打包给你:import cats.data.Validated import cats.implicits._ case class User(name: String, age: Int) val validateName = (name: String) => if (name.nonEmpty) Validated.valid(name) else Validated.invalid(List("姓名不能为空")) val validateAge = (age: Int) => if (age >= 18) Validated.valid(age) else Validated.invalid(List("年龄必须满18岁")) // 两个校验都会执行,错误会合并 val userValidation = (validateName(""), validateAge(15)).mapN(User) // 结果是 Invalid(List("姓名不能为空", "年龄必须满18岁"))如果Validation是Monad:那它必须遵守Monad的
flatMap语义——「用前一步的结果决定下一步是否执行」。要是前一步失败了,后续的逻辑直接跳过,只能拿到第一个错误。这就完全违背了Validation「收集所有错误」的设计初衷,等于废掉了它最核心的功能。
什么时候需要把Validation转成Either?
当你不需要全量收集错误,而是想要快速失败(遇到第一个错误就停止后续校验)的时候,就可以把Validation转成Either(Either是标准的Monad)。比如Cats里直接用toEither方法:
val nameEither = validateName("").toEither val ageEither = validateAge(15).toEither // Either的flatMap会快速终止,年龄校验根本不会执行 val userEither = for { name <- nameEither age <- ageEither } yield User(name, age) // 结果是 Left(List("姓名不能为空"))
反过来,如果手里是Either但想收集所有错误,也可以用toValidated转成Validation,灵活切换适配不同场景。
简单说就是:Validation和Monad的核心诉求完全相反,一个追求「全量错误」,一个追求「快速失败」,所以Validation注定没法成为Monad——它们是为不同验证场景量身打造的工具。
内容的提问来源于stack exchange,提问作者Jan Bols
相关产品推荐
相关产品推荐

