Question

I have a Scala case class containing command-line configuration information:

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

I am writing a validation function that checks that each of the values is a 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)
  }
}

but if I understand monads from Haskell, it seems that I should be able to chain the validations together (pseudo syntax):

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"))
}

If any of the config.XXX expressions returns Failure, the whole thing (validateConfig) should fail, otherwise Success(config) should be returned.

Is there some way to do this with Try, or maybe some other class?

Was it helpful?

Solution 3

Here's a solution that I came up with after some searching and scaladocs reading:

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
}

Similar to Travis Brown's answer.

OTHER TIPS

It's pretty straightforward to convert each Option to an instance of the right projection of 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 isn't a monad in the standard library's terms, but its right projection is, and will provide the behavior you want in the case of failure.

If you'd prefer to end up with a Try, you could just convert the resulting Either:

import scala.util._

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

I wish that the standard library provided a nicer way to make this conversion, but it doesn't.

It's a case class, so why aren't you doing this with pattern matching?

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)
}

In your simple example, my priority would be to forget monads and chaining, just get rid of that nasty if...else smell.

However, while a case class works perfectly well for a short list, for a large number of configuration options, this becomes tedious and the risk of error increases. In this case, I would consider something like this:

  1. Add a method that returns a key->value map of the configuration options, using the option names as the keys.
  2. Have the Validate method check if any value in the map is None
  3. If no such value, return success.
  4. If at least one value matches, return that value name with the error.

So assuming that somewhere is defined

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

and the Config class has a method like this:

def optionMap: OptionMap = ...

then I would write Config.validate like this:

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

So now Config.validate returns either a Left containing the name of all the bad options or a Right containing the full map of options and their values. Frankly, it probably doesn't matter what you put in the Right.

Now, anything that wants to validate a Config just calls Config.validate and examines the result. If it's a Left, it can throw an IllegalArgumentException containing one or more of the names of bad options. If it's a Right, it can do whatever it wanted to do, knowing the Config is valid.

So we could rewrite your validateConfig function as

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

Can you see how much more functional and OO this is getting?

  • No imperative chain of if...else
  • The Config object validates itself
  • The consequences of a Config object being invalid are left to the larger program.

I think a real example would be more complex yet, though. You are validating options by saying "Does it contain Option[String] or None?" but not checking the validity of the string itself. Really, I think your Config class should contain a map of options where the name maps to the value and to an anonymous function that validates the string. I could describe how to extend the above logic to work with that model, but I think I'll leave that as an exercise for you. I will give you a hint: you might want to return not just the list of failed options, but also the reason for failure in each case.

Oh, by the way... I hope none of the above implies that I think you should actually store the options and their values as an optionMap inside the object. I think it's useful to be able to retrieve them like that, but I wouldn't ever encourage such exposure of the actual internal representation ;)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top