我有一个包含命令行配置信息的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
,或者其他一些类来做到这一点?
答案 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
气味。
然而,虽然案例类对于短列表非常有效,但对于大量配置选项而言,这变得乏味且错误的风险增加。在这种情况下,我会考虑这样的事情:
None
所以假设某处已定义
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
}
类似于特拉维斯·布朗的回答。