2
\$\begingroup\$

I have a series of four method calls that all return Try[Something], with the last returning Try[Unit]. Something like this:

def getFields(): Try[List[A]] = { ... }
def getValues(): Try[List[B]] = { ... }
def createTmpFile(as: List[A], bs: List[B]): Try[C] { ... }
def runProcessWithFile(c: C): Try[Unit] { ... }

def run(): Try[Unit] = {
  for (
    fields <- getFields();
    values <- getValues();
    file <- createTmpFile(fields, values);
    _ <- runProcessWithFile(file)
  ) yield ()
}

This seems OK to me. I guess my first question is about Try[Unit]. That seems a little awkward. Option[Exception] seems equally awkward. Is there a "best practice" return type in Scala for a possible Exception?

Also, things got complicated when I wanted to add branching based on whether getValues() returns an empty list. This is what I have right now:

def run(): Try[Unit] = {
  for (
    fields <- getFields();
    values <- getValues();
    _ <- if (values.nonEmpty) {
      for (
        file <- createTmpFile(fields, values);
        _ <- runProcessWithFile(file)
      ) yield ()
    } else {
      Success(())
    }
  ) yield ()
}

That seems pretty hairy. Is there a better way to do this? I suppose I could abandon Try altogether and just let everything throw Exceptions, but that seems like throwing the baby out with the bathwater.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Having a Try[Unit] is completely ok - it models the fact that there may be exceptions on a side effecting operation in a type. That your code looks ugly is not because your monad is wrong but because you used it wrong. You already started in modeling your application with monads, why just stopping in the middle?

You can easily restructure your monad a little bit:

def run(): Try[Unit] = {
  val res = for {
    fields <- getFields()
    values <- getValues()
  } yield {
    if (values.isEmpty)
      Success(())
    else for (file <- createTmpFile(fields, values))
      yield runProcessWithFile(file)
  }
  res.flatten
}

What we see here is a monad.map(f).flatten which is the same as monad.flatMap(f) which is the same as for (m <- monad) yield f(m). Applying this fact to your code snippet results in:

def processTmpFile(as: List[A], bs: List[B]): Try[Unit] =
  if (values.isEmpty)
    Success(())
  else for (file <- createTmpFile(fields, values))
    yield runProcessWithFile(file)

def run(): Try[Unit] = {
  for {
    fields <- getFields()
    values <- getValues()
  } yield processTmpFile(fields, values)
}

Operating on a deeper level of the monad (what you did with your condition) means that you can either nest the monad and unpack it later at the end or that you can unpack it immediately after the nesting happened (that is what a for comprehension does).

\$\endgroup\$
1
\$\begingroup\$

About your first question, isn't the code below equivalent?

def run(): Try[Unit] = 
  for (
    fields <- getFields();
    values <- getValues();
    file <- createTmpFile(fields, values);
  ) yield runProcessWithFile(file)

For your second question, I would create a function that combines createTmpFile and runProcessWithFile. This combined function could be useful elsewhere too. (sschaef has a much longer argument, but came up with the same answer.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.