Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert try to option without losing error information in Scala

Introduction

I had created a lovely one-liner:

Option("something").map(_ => Try("something else")).flatten.getOrElse("default")

which actually does not compile, with error:

Error:(15, 31) Cannot prove that scala.util.Try[String] <:< Option[B].
Option("").map(_ => Try("")).flatten.getOrElse("");}
                             ^

so I had found a way around:

Option("something").flatMap(_ => Try("something else").toOption).getOrElse("default")

But, the problem

My colleague warned me, that my construction is actually loosing the error information. This is true, and in real-life application - not acceptable.

After getting rid of all the repetition I had ended up with:

implicit class CoolTry[T](t: Try[T]) extends StrictLogging {
  def toOptionSE: Option[T] = t match {
    case Success(s) => Some(s)
    case Failure(ex) =>
      logger.error(ex.getMessage, ex)
      None
  }
}

using:

Option("something").flatMap(_ => Try(new Exception("error")).toOptionSE).getOrElse("default")

Question

I believe there are many similar cases in every application and I simply do not know if either my approach is bad or Try().toOption is simply done wrong?

I understand that logging is a side effect, but while using Try I guess everyone does expect it if something goes wrong?

  • Can the implicit class be improved?
  • Is there any utility library handling Try().toOption my way?
  • Or what (other) approach should I take here?

Thanks!

like image 986
Atais Avatar asked Feb 08 '17 10:02

Atais


People also ask

What is getOrElse in Scala?

As we know getOrElse method is the member function of Option class in scala. This method is used to return an optional value. This option can contain two objects first is Some and another one is None in scala. Some class represent some value and None is represent a not defined value.

How do I use options in Scala?

The Option in Scala is referred to a carrier of single or no element for a stated type. When a method returns a value which can even be null then Option is utilized i.e, the method defined returns an instance of an Option, in place of returning a single object or a null.

What is some function in Scala?

Scala some class returns some value if the object is not null, it is the child class of option. Basically, the option is a data structure which means it can return some value or None. The option has two cases with it, None and Some. We can use this with the collection.


2 Answers

Forcing logging every time you change a Try[T] to an Option[T] is an undesired effect IMO. When you do such a transformation you're explicitly admitting that you don't really care about the internals of the failure, if it happened. All you want is to access the result, if it exists. Most of the time you say "well, this is undesirable, I always want to log exceptions", but sometimes you simply only care about the end result, so dealing with an Option[T] can be good enough.

Or what (other) approach should I take here?

Using Either[A, B] is an option here, especially in Scala 2.12 when it became right biased. You can simply map over it and only at the end check if there was an error and then log (created with Scala 2.12.1):

val res: Either[Throwable, String] = Option("something")
  .map(_ => Try("something else").toEither)
  .getOrElse(Right("default"))
  .map(str => s"I got my awesome $str")

Ideally (IMO) deferring the logging side effect to the last possible point would serve you better.

like image 147
Yuval Itzchakov Avatar answered Oct 23 '22 14:10

Yuval Itzchakov


One possible improvement would be to make it crystal clear to the user of what is going on; between the implicit being injected somewhere by the compiler and the apparently "pure" name (toOptionSE) it may not be evident of what is going on for a second developer reading and/or modifying your code. Furthermore, you're fixing how you treat the error case and don't leave the opportunity to handle it differently from logging it.

You can treat errors by leveraging projection, like the failed projection defined over Try. If you really want to do this fluently and on one line, you can leverage implicit classes like this.

implicit class TryErrorHandlingForwarding[A](t: Try[A]) {
  def onError(handler: Throwable => Unit): Try[A] = {
    t.failed.foreach(handler)
    t
  }
}

// maybe here you want to have an actual logger
def printStackTrace: Throwable => Unit =
  _.printStackTrace

Option("something").
  flatMap(_ => Try(???).onError(printStackTrace).toOption).
  getOrElse("default")

Also, here I'm assuming that for whatever reason you cannot use Try right from the start (as it's been suggested in a comment).

like image 34
stefanobaghino Avatar answered Oct 23 '22 12:10

stefanobaghino