在Scala中链接验证

时间:2013-09-12 16:23:25

标签: scala monads either

我有一个包含命令行配置信息的Scala case class

case class Config(emailAddress: Option[String],
                  firstName: Option[String]
                  lastName: Option[String]
                  password: Option[String])

我正在编写一个验证函数,用于检查每个值是否为Some

def validateConfig(config: Config): Try[Config] = {
  if (config.emailAddress.isEmpty) {
    Failure(new IllegalArgumentException("Email Address")
  } else if (config.firstName.isEmpty) {
    Failure(new IllegalArgumentException("First Name")
  } else if (config.lastName.isEmpty) {
    Failure(new IllegalArgumentException("Last Name")
  } else if (config.password.isEmpty) {
    Failure(new IllegalArgumentException("Password")
  } else {
    Success(config)
  }
}

但如果我理解来自Haskell的monad,似乎我应该能够将验证链接在一起(伪语法):

def validateConfig(config: Config): Try[Config] = {
  config.emailAddress.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Email Address")) >>
  config.firstName.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("First Name")) >>
  config.lastName.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Last Name")) >>
  config.password.map(Success(config)).
    getOrElse(Failure(new IllegalArgumentException("Password"))
}

如果任何config.XXX表达式返回Failure,则整个事件(validateConfig)应该失败,否则应返回Success(config)

有没有办法用Try,或者其他一些类来做到这一点?

3 个答案:

答案 0 :(得分:8)

将每个Option转换为Either正确投影的实例非常简单:

def validateConfig(config: Config): Either[String, Config] = for {
  _ <- config.emailAddress.toRight("Email Address").right
  _ <- config.firstName.toRight("First Name").right
  _ <- config.lastName.toRight("Last Name").right
  _ <- config.password.toRight("Password").right
} yield config

Either在标准库的术语中不是monad,但它的正确投影是,并且将在失败的情况下提供您想要的行为。

如果您希望最终得到Try,则可以转换生成的Either

import scala.util._

val validate: Config => Try[Config] = (validateConfig _) andThen (
  _.fold(msg => Failure(new IllegalArgumentException(msg)), Success(_))
)

我希望标准库提供更好的方式来进行此转换,但事实并非如此。

答案 1 :(得分:1)

这是一个案例类,那你为什么不用模式匹配呢?

def validateConfig(config: Config): Try[Config] = config match {
  case Config(None, _, _, _) => Failure(new IllegalArgumentException("Email Address")
  case Config(_, None, _, _) => Failure(new IllegalArgumentException("First Name")
  case Config(_, _, None, _) => Failure(new IllegalArgumentException("Last Name")
  case Config(_, _, _, None) => Failure(new IllegalArgumentException("Password")
  case _ => Success(config)
}

在你的简单例子中,我的首要任务是忘记monad和chaining,只是摆脱那种令人讨厌的if...else气味。

然而,虽然案例类对于短列表非常有效,但对于大量配置选项而言,这变得乏味且错误的风险增加。在这种情况下,我会考虑这样的事情:

  1. 使用选项名称作为键,添加一个返回配置选项的key-&gt;值映射的方法。
  2. 让Validate方法检查地图中的任何值是否为None
  3. 如果没有这样的值,则返回成功。
  4. 如果至少有一个值匹配,则返回带有错误的值名称。
  5. 所以假设某处已定义

    type OptionMap = scala.collection.immutable.Map[String, Option[Any]]
    

    并且Config类有这样的方法:

    def optionMap: OptionMap = ...
    

    然后我会像这样写Config.validate

    def validate: Either[List[String], OptionMap] = {
      val badOptions = optionMap collect { case (s, None) => s }
      if (badOptions.size > 0)
        Left(badOptions)
      else
        Right(optionMap)
    }
    

    所以现在Config.validate会返回包含所有错误选项名称的Left或包含选项及其值的完整地图的Right。坦率地说,你放在Right中的内容可能无关紧要。

    现在,任何想要验证Config的内容只需调用Config.validate并检查结果。如果它是Left,它可以抛出包含一个或多个错误选项名称的IllegalArgumentException。如果它是Right,它可以做任何想做的事情,知道Config有效。

    因此我们可以将您的validateConfig函数重写为

    def validateConfig(config: Config): Try[Config] = config.validate match {
      case Left(l) => Failure(new IllegalArgumentException(l.toString))
      case _ => Success(config)
    }
    

    你能看到它有多少功能 OO?

    • 没有必要的if...else
    • Config对象自行验证
    • Config对象无效的后果留给更大的程序。
    但是,我认为一个真实的例子会更复杂。您正在通过说“它包含Option[String]None来验证选项吗?”但不检查字符串本身的有效性。实际上,我认为你的Config类应该包含一个选项映射,其中名称映射到值,以及一个验证字符串的匿名函数。我可以描述如何扩展上述逻辑以使用该模型,但我想我会把它作为练习给你。我会给你一个提示:你可能不仅要返回失败选项列表,还要返回每种情况下失败的原因。

    哦,顺便说一句......我希望上述内容都没有暗示我认为你应该将选项及其值作为optionMap存储在对象中。我认为能够像那样检索它们是有用的,但我不会鼓励这种实际内部表示的曝光;)

答案 2 :(得分:0)

这是我在一些搜索和scaladocs阅读后提出的解决方案:

def validateConfig(config: Config): Try[Config] = {
  for {
    _ <- Try(config.emailAddress.
             getOrElse(throw new IllegalArgumentException("Email address missing")))
    _ <- Try(config.firstName.
             getOrElse(throw new IllegalArgumentException("First name missing")))
    _ <- Try(config.lastName.
             getOrElse(throw new IllegalArgumentException("Last name missing")))
    _ <- Try(config.password.
             getOrElse(throw new IllegalArgumentException("Password missing")))
  } yield config
}

类似于特拉维斯·布朗的回答。