1

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

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或者其他类来做到这一点?

4

3 回答 3

9

将每个转换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不是标准库术语中的单子,但它的正确投影是,并且将在失败的情况下提供您想要的行为。

如果您希望以 a 结尾Try,则可以转换结果Either

import scala.util._

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

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

于 2013-09-12T19:12:51.107 回答
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)
}

在您的简单示例中,我的首要任务是忘记单子和链接,只是摆脱那种讨厌的if...else气味。

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

  1. 添加一个返回配置选项的键->值映射的方法,使用选项名称作为键。
  2. 让 Validate 方法检查地图中是否有任何值None
  3. 如果没有这样的值,则返回成功。
  4. 如果至少有一个值匹配,则返回带有错误的值名称。

所以假设某处被定义

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包含所有错误选项名称的 a 或Right包含选项及其值的完整映射的 a。坦率地说,你在Right.

现在,任何想要验证 a 的东西都会Config调用Config.validate并检查结果。如果它是 a Left,它可以抛出IllegalArgumentException包含一个或多个错误选项名称的 a 。如果它是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对象内部。我认为能够像这样检索它们很有用,但我永远不会鼓励这种实际内部表示的暴露;)

于 2013-09-12T17:44:15.643 回答
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
}

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

于 2013-09-12T19:17:53.713 回答