What is the difference between Try and Either?

2020-02-10 04:20发布

问题:

According to the documentation:

The Try type represents a computation that may either result in an exception, or return a successfully computed value. It's similar to, but semantically different from the scala.util.Either type.

The docs do not go into further detail as to what the semantic difference is. Both seem to be able to communicate successes and failures. Why would you use one over the other?

回答1:

I covered the relationship between Try, Either, and Option in this answer. The highlights from there regarding the relationship between Try and Either are summarized below:

Try[A] is isomorphic to Either[Throwable, A]. In other words you can treat a Try as an Either with a left type of Throwable, and you can treat any Either that has a left type of Throwable as a Try. It is conventional to use Left for failures and Right for successes.

Of course, you can also use Either more broadly, not only in situations with missing or exceptional values. There are other situations where Either can help express the semantics of a simple union type (where value is one of two types).

Semantically, you might use Try to indicate that the operation might fail. You might similarly use Either in such a situation, especially if your "error" type is something other than Throwable (e.g. Either[ErrorType, SuccessType]). And then you might also use Either when you are operating over a union type (e.g. Either[PossibleType1, PossibleType2]).

The standard library does not include the conversions from Either to Try or from Try to Either, but it is pretty simple to enrich Try, and Either as needed:

object TryEitherConversions {
    implicit class EitherToTry[L <: Throwable, R](val e: Either[L, R]) extends AnyVal {
        def toTry: Try[R] = e.fold(Failure(_), Success(_))
    }

    implicit class TryToEither[T](val t: Try[T]) extends AnyVal {
        def toEither: Either[Throwable, T] = t.map(Right(_)).recover(PartialFunction(Left(_))).get
    }
}

This would allow you to do:

import TryEitherConversions._

//Try to Either
Try(1).toEither //Either[Throwable, Int] = Right(1)
Try("foo".toInt).toEither //Either[Throwable, Int] = Left(java.lang.NumberFormatException)

//Either to Try
Right[Throwable, Int](1).toTry //Success(1)
Left[Throwable, Int](new Exception).toTry //Failure(java.lang.Exception)


回答2:

To narrowly answer your question: "What's the semantic difference":

This probably refers to flatMap and map, which are non-existent in Either and either propagate failure or map the success value in Try. This allows, for instance, chaining like

for { 
   a <- Try {something} 
   b <- Try {somethingElse(a)}
   c <- Try {theOtherThing(b)}
} yield c

which does just what you'd hope - returns a Try containing either the first exception, or the result.

Try has lots of other useful methods, and of course its companion apply method, that make it very convenient for its intended use - exception handling.

If you really want to be overwhelmed, there are two other classes out there which may be of interest for this kind of application. Scalaz has a class called "\/" (formerly known as Prince), pronounced "Either", which is mostly like Either, but flatMap and map work on the Right value. Similarly, and not, Scalactic has an "Or" which is also similar to Either, but flatMap and map work on the Left value.

I don't recommend Scalaz for beginners.



回答3:

Either does not imply success and failure, it is just a container for either an A or a B. It is common to use it to represent successes and failures, the convention being to put the failure on the left side, and the success on the right.

A Try can be seen as an Either with the left-side type set to Throwable. Try[A] would be equivalent to Either[Throwable, A].

Use Try to clearly identify a potential failure in the computation, the failure being represented by an exception. If you want to represent the failure with a different type (like a String, or a set of case classes extending a sealed trait for example) use Either.



回答4:

Either is more general, since it simply represents disjoint unions of types. In particular, it can represent a union of valid return values of some type X and Exception. However, it does not attempt to catch any exceptions on its own. You have to add try-catch blocks around dangerous code, and then make sure that each branch returns an appropriate subclass of Either (usually: Left for errors, Right for successful computations).

Try[X] can be thought of as Either[Exception, X], but it also catches Exceptions on its own.



回答5:

Either[X, Y] usage is more general. As its name say it can represent either an object of X type or of Y.

Try[X] has only one type and it might be either a Success[X] or a Failure (which contains a Throwable).

At some point you might see Try[X] as an Either[Throwable,X]

What is nice about Try[X] is that you can chain futher operations to it, if it is really a Success they will execute, if it was a Failure they won't

val connection = Try(factory.open())
val data = connection.flatMap(conn => Try(conn.readData()))
//At some point you can do 
data matches {
  Success(data) => print data
  Failure(throwable) => log error
}

Of course, you can always oneline this like

Try(factory.open()).flatMap(conn => Try(conn.readData()) matches {
      Success(data) => print data
      Failure(throwable) => log error
}


回答6:

As already have been mentioned, Either is more general, so it might not only wrap error/successful result, but also can be used as an alternative to Option, for branching the code path.

For abstracting the effect of an error, only for this purpose, I identified the following differences:

  1. Either can be used to specify a description of the error, which can be shown to the client. Try - wraps an exception with a stack trace, less descriptive, less client oriented, more for internal usage.
  2. Either allows us to specify error type, with existing monoid for this type. As a result, it allows us to combine errors (usually via applicative effects). Try abstraction with its exception, has no monoid defined. With Try we must spent more effort to extract error and handle it.

    Based on it, here is my best practices: When I want to abstract effect of error, I always use Either as the first choice, with List/Vector/NonEmptyList as error type.

    Try is used only, when you invoke code, written in OOP. Good candidates for Try are methods, that might throw an exception, or methods, that sends request to external systems (rest/soap/database requests in case the methods return a raw result, not wrapped into FP abstractions, like Future, for instance.



标签: scala