Search

Either

์ƒ์„ฑ์ผ
2021/07/30 09:11
ํƒœ๊ทธ

Either

sealed class Either<out A, out B>
In day-to-day programming, it is fairly common to find ourselves writing functions that can fail. For instance, querying a service may result in a connection issue, or some unexpected JSON response.
To communicate these errors, it has become common practice to throw exceptions; however, exceptions are not tracked in any way, shape, or form by the compiler. To see what kind of exceptions (in any) a function may throw, we have to dig through the source code. Then, to handle these exceptions, we have to make sure we catch them at the call site. This all becomes even more unwieldy when we try to compose exception-throwing procedures.
Assume we happily throw exceptions in our code. Looking at the types of the functions above, any could throw a number of exceptions โ€” we do not know. When we compose, exceptions from any of the constituent functions can be thrown. Moreover, they may throw the same kind of exception (e.g., IllegalArgumentException) and, thus, it gets tricky tracking exactly where an exception came from.
How then do we communicate an error? By making it explicit in the data type we return.

Either vs Validated

In general, Validated is used to accumulate errors, while Either is used to short-circuit a computation upon the first error. For more information, see the Validated vs Either section the Validated documentation.
By convention, the right side of an Either is used to hold successful values.
Because Either is right-biased, it is possible to define a Monad instance for it.
Since we only ever want the computation to continue in the case of Right (as captured by the right-bias nature), we fix the left type parameter and leave the right one free.
So, the map and flatMap methods are right-biased:

Using Either instead of exceptions

As a running example, we will have a series of functions that will:
โ€ข
Parse a string into an integer
โ€ข
Calculate the reciprocal
โ€ข
Convert the reciprocal into a string
Using exception-throwing code, we could write something like this:
instead, let's make the fact that some of our functions can fail explicit in the return type.
These calls to parse return a Left and Right value
Now, using combinators like flatMap and map, we can compose our functions together.
In the following exercise, we pattern-match on every case in which the Either returned by magic can be in. Note the when clause in the Left - the compiler will complain if we leave that out because it knows that, given the type Either[Exception, String], there can be inhabitants of Left that are not NumberFormatException or IllegalArgumentException. You should also notice that we are using SmartCast for accessing Left and Right values.
Instead of using exceptions as our error value, let's instead enumerate explicitly the things that can go wrong in our program.
For our little module, we enumerate any and all errors that can occur. Then, instead of using exception classes as error values, we use one of the enumerated cases. Now, when we pattern match, we are able to comphrensively handle failure without resulting in an else branch; moreover, since Error is sealed, no outside code can add additional subtypes that we might fail to handle.

Either.catch exceptions

Sometimes you do need to interact with code that can potentially throw exceptions. In such cases, you should migrate the possibility that an exception can be thrown. You can do so by using the catch function.
Example:

Resolve Either into one type of value.

In some cases you can not use Either as a value. For instance, when you need to respond to an HTTP request. To resolve Either into one type of value, you can use the resolve function. In the case of an HTTP endpoint you most often need to return some (framework specific) response object which holds the result of the request. Tre result can be expected and positive, this is the success flow. Or the result can be expected but negative, this is the error flow. Or the result can be unexpected and negative, in this case an unhandled exception was thrown. In all three cases, you want to use the same kind of response object. But probably you want to respond slightly different in each case. This can be achieved by providing specific functions for the success, error and throwable cases.
Example:
There are far more use cases for the resolve function, the HTTP endpoint example is just one of them.

Syntax

Either cal also map over the left value with mapLeft, which is similar to map, but applies on left instances.
Either<A, B> can be transformed to Either<B, A> using the swap() method.
For using Either's syntax on arbitrary data types. This will make possible to use the left(), right(), contains(), getOrElse() and getOrHandle() methods:
For creating Either instance based on a predicate, use Either.conditionally() method. It will evaluate an expression passed as first parameter, in case the expression evaluates to false it will give an Either.Left<L> build from the second parameter. If the expression evaluates to a true it will take the third parameter and give an Either.Right<R>:
ExistsByUsername() ํ•จ์ˆ˜์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„๋“ฏ
Another operation is fold. This operation will extract the value from the Either, or provide a default if the value is Left.
The getOrHandle() operation allows the transformation of an Either.Left value to a Either.Right using the value of Left. This can be useful when mapping to a single result type is required like fold(), but without the need to handle Either.Right case.
As an example, we want to map an Either<Throwable, Int> to a proper HTTP status code:
The leftIfNull operation transforms a null Either.Right value to the specified Either.Left value. If the value is non-null, the value wrapped into a non-nullable Either.Right is returned (very useful to skip null-check further down the call chain). If the operation is called on an Either.Left, the same Either.Left is returned.
See the examples below:
Another useful operation when working with null is rightIfNotNull. If the value is null, it will be transformed to the specified Either.Left and, if it's not null, the type will be wrapped to Either.Right.
Example: