- Type: Standard Library API proposal
- Author: Roman Elizarov
- Contributors: Andrey Breslav, Ilya Gorbunov
- Status: Public review
- Prototype: Implemented in 1.3-M1
- Related issues: KT-18608
- Discussion: KEEP-127
Kotlin language provides exceptions that are used to represent an arbitrary failure of a function and include ability to attach additional information pertaining to this failure. Exceptions are sequential in nature and work great in any kind of sequential code, including code for a single coroutine or in other case where one piece of work in being sequentially decomposed. Exceptions ensure that the first failure in a sequentially performed work stops further progress and is propagated up to the caller. However, sequential nature of exceptions complicates their use in cases where some kind of parallel decomposition of work is needed or multiple failures need to be retained for later processing.
We'd like to introduce a type in the Kotlin standard library that is effectively a discriminated union between successful
and failed outcome of execution of Kotlin function — Success T | Failure Throwable
,
where Success T
represents a successful result of some type T
and Failure Throwable
represents a failure with any Throwable
exception.
For the purpose of efficiency, we would model it as a generic inline class SuccessOrFailure<T>
in the standard library.
NOTE: This SuccessOrFailure
class SHOULD NOT be used directly as a return type of Kotlin functions with a few
exceptions that are explained in more detail in the section on
style and exceptions.
See use cases below on how SuccessOrFailure
is designed be used.
This section lists motivating use-cases.
The primary driver for inclusion of this class into the Standard Library is Continuation<T>
callback interface
that should get invoked on the successful or failed execution of an asynchronous operation.
We'd like to be able to have only a single function with "success or failure" union type as its parameter:
interface Continuation<in T> {
fun resumeWith(result: SuccessOrFailure<T>)
}
Another example here is parallel execution of multiple asynchronous operations that must capture successful or failed execution of each individual piece to analyze and reach decision on the outcome of a larger piece of work:
val deferreds: List<Deferred<T>> = List(n) {
async {
/* Do something that produces T or fails */
}
}
val outcomes1: List<T> = deferreds.map { it.await() } // BAD -- crash on the first (by index) failure
val outcomes2: List<T> = deferreds.awaitAll() // BAD -- crash on the earliest (by time) failure
val outcomes3: List<SuccessOrFailure<T>> = deferreds.map { it.awaitCatching() } // !!! <= THIS IS THE ONE WE WANT
Where awaitCatching
could be defined like this:
suspend fun <T> Deferred<T>.awaitCatching(): SuccessOrFailure<T> =
runCatching { await() }
Kotlin encourages writing code in a functional style. It works well as long as business-specific failures are represented with nullable types or sealed class hierarchies, while other kinds of failures (that are represented by exceptions) do not require any special local handling. However, when interfacing with Java-style APIs that rely heavily on exceptions or otherwise having a need to somehow process exceptions locally (as opposed to propagating them up the call stack), we see a clear lack of primitives in the Kotlin standard library.
Consider writing a function readFiles
that receives a list of files, reads all of them, and returns a
list of results. We are given the following function to read single file contents:
fun readFileData(file: File): Data
This reading function throws exception if file is not found or parsing of a file had somehow failed. Normally that would
be fine and the first failure of this kind would terminate the whole program with a stacktrace and explanatory message.
However, for readFiles
we'd explicitly like to be able to continue after the failure to collect and report all failures.
Moreover, we'd like to be able to have a functional implementation of readFiles
like this:
fun readFilesCatching(files: List<File>): List<SuccessOrFailure<Data>> =
files.map {
runCatching {
readFileData(it)
}
}
This function is named
readFileCatching
to make it explicit to the caller that all encountered failures were caught and encapsulated inSuccessOrFailure
and it is caller responsibility to process these failures.
Now, consider making some transformation of readFilesCatching
results that we'd like to express functionally,
while preserving accumulated failures:
readFilesCatching(files).map { result: SuccessOrFailure<Data> -> // type explicitly written here for clarity
result.map { it.doSomething() } // Operates on Success case, while preserving Failure
}
If doSomething
, in turn, can potentially fail and we are interested in keeping this failure per each individual
file, then we can write it using mapCatching
instead of map
:
readFilesCatching(files).map { result: SuccessOrFailure<Data> ->
result.mapCatching { it.doSomething() }
}
In mostly functional code try { ... } catch(e: Throwable) { ... }
construct looks
out of style. For example, consider this piece of code that uses RxKotlin
for asynchronous processing.
It invokes doSomethingAsync
that returns
Single
and processes potential error in a functional style:
doSomethingAsync()
.subscribe(
{ processData(it) },
{ showErrorDialog(it) }
)
Note, that the above code is written in a style that is very different from direct programming style.
doSomethingAsync()
that returnsSingle
does not actually do anything untilsubscribe
is invoked (its result is typically cold). This distinction is not important for the purposes of this section. We are interested here in a visual fact that error and result handling are chained to the initial invocation.
Working with function that returns Java's CompletableFuture
is visually similar:
doSomethingAsync()
.whenComplete { data, exception ->
if (exception != null)
showErrorDialog(exception)
else
processData(data)
}
It is closer to direct style, since this
doSomethingAsync
invocation actually starts performing operation, but we also see that ultimate processing of success or failure is performed via chaining.
Now, if doSomethingSync
is a synchronous function, then handling its success or failure looks quite visually different,
which is problematic for the code that mixes both approaches:
try {
val data = doSomethingSync()
processData(data)
} catch(e: Throwable) {
showErrorDialog(e)
}
Also note, that the code with
try/catch
has different semantics, since it also catches exceptions that could have been thrown byprocessData
. Preserving functional-style error-handling semantics usingtry/catch
is quite non-trivial (see Error handling alternative section).
Instead, we'd like to be able to write the same code in a more functional way:
runCatching { doSomethingSync() }
.onFailure { showErrorDialog(it) }
.onSuccess { processData(it) }
There is a number of community-supported libraries that provide this kind of success or failure union type,
but we cannot use any of them for the Continuation
callback interface that is defined in the Standard Library.
Alternative signatures for the Continuation
interface are listed below.
Two methods as in current experimental version of coroutines:
interface Continuation<in T> {
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
This solution was tried in experimental version of coroutines and the following problems were identified:
- All implementations have to implement both methods and there is no easy shortcut to provide a builder with
a lambda like
Continuation { ... body ... }
. - Some implementations need to capture "success or failure" in their state and pass on captured success or failure to another delegate continuation at a later time.
- Some implementations have a common piece of logic that should be executed on both success and failure
with minor differences for successful and failed cases. These implementations have to immediately forward both
resume
andresumeWithException
to some internal function likedoResume
, thus increasing stack size and still forcing implementor to figure out a way to represent both success and failure in one method.
One method with two parameters:
interface Continuation<in T> {
fun resume(value: T?, exception: Throwable?)
}
The downside here is that both parameters here are nullable and there is no larger type-safety nor a clear indication of intent to have only one of them set.
One method with Any? parameter:
interface Continuation<in T> {
fun resume(result: Any?) // result: T | Failure(Throwable)
}
This solution completely lacks any type-safety on Kotlin side.
Let's see what it takes to rewrite the code with functional-style error handling without resorting to 3rd party libraries.
Non-nullable value type:
If the result of doSomethingSync
is non-nullable, then we can write somewhat concise code:
val data: Data? = try {
doSomethingSync()
} catch(e: Throwable) {
showErrorDialog(e)
null
}
if (data != null)
processData(data)
Nullable value type:
If the result of doSomethingSync
is nullable, then one possible alternative is shown below:
var data: Data? = null
val success = try {
data = doSomethingSync()
true
} catch(e: Throwable) {
showErrorDialog(e)
false
}
if (success)
processData(data)
The following snippet gives summary of all the public APIs:
class SuccessOrFailure<out T> /* internal constructor */ {
val isSuccess: Boolean
val isFailure: Boolean
fun getOrThrow(): T
fun getOrNull(): T?
fun exceptionOrNull(): Throwable?
companion object {
fun <T> success(value: T): SuccessOrFailure<T>
fun <T> failure(exception: Throwable): SuccessOrFailure<T>
}
}
inline fun <R> runCatching(block: () -> R): SuccessOrFailure<R>
inline fun <T, R> T.runCatching(block: T.() -> R): SuccessOrFailure<R>
fun <R, T : R> SuccessOrFailure<T>.getOrDefault(defaultValue: R): R
inline fun <R, T : R> SuccessOrFailure<T>.getOrElse(onFailure: (exception: Throwable) -> R): R
inline fun <R, T> SuccessOrFailure<T>.fold(onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R): R
inline fun <R, T> SuccessOrFailure<T>.map(transform: (value: T) -> R): SuccessOrFailure<R>
inline fun <R, T: R> SuccessOrFailure<T>.recover(transform: (exception: Throwable) -> R): SuccessOrFailure<R>
inline fun <R, T> SuccessOrFailure<T>.mapCatching(transform: (value: T) -> R): SuccessOrFailure<R>
inline fun <R, T: R> SuccessOrFailure<T>.recoverCatching(transform: (exception: Throwable) -> R): SuccessOrFailure<R>
inline fun <T> SuccessOrFailure<T>.onSuccess(action: (value: T) -> Unit): SuccessOrFailure<T>
inline fun <T> SuccessOrFailure<T>.onFailure(action: (exception: Throwable) -> Unit): SuccessOrFailure<T>
All of the functions have self-explanatory consistent names that follow established tradition in Kotlin Standard library and establish the following additional conventions:
- Functions that can throw previously suppressed (captured) exception are named
with explicit
OrThrow
suffix likegetOrThrow
. - Functions that capture thrown exception and encapsulate it into
SuccessOrFailure
instance are named with explicitCatching
suffix likerunCatching
andmapCatching
. - A traditional
map
transformation function that works on successful cases is augmented with arecover
function that similarly transforms exceptional cases. A failure inside eithermap
orrecover
transform aborts operation like a traditional function, butmapCatching
andrecoverCatching
encapsulate failure in transform into the resultingSuccessOrFailure
. - Functions to query the case are naturally named
isSuccess
andisFailure
. - Functions that act on the success or failure cases are named
onSuccess
andonFailure
and return their receiver unchanged for further chaining according to tradition established byonEach
extension from the Standard Library.
This library depends on
inline class
language feature for its efficient implementation.
SuccessOrFailure
is implemented by an inline class
and is optimized for a successful case. Success is stored as
a value of SuccessOrFailure
directly, without additional boxing, while failure exception is wrapped into an internal
SuccessOrFailure.Failure
class. SuccessOrFailure
class has the following internal published APIs that
represent its binary interface on JVM in addition to its public API:
inline class SuccessOrFailure<out T> @PublishedApi internal constructor(
@PublishedApi internal val value: Any? // internal value -- either T or Failure
) : Serializable {
@PublishedApi internal class Failure(
@JvmField val exception: Throwable
) : Serializable
}
The SuccessOrFailure
class is not designed to be used directly as the result type of general functions and we'd like
to explicitly discourage using it like this:
fun findUserByName(name: String): SuccessOrFailure<User> // !!! DON'T DO THIS !!!
In general, if some API requires its callers to handle failures locally (immediately around or next to the invocation), then it should use nullable types, when these failures do not carry additional business meaning, or domain-specific data types to represent its successful results and failures with any additional business-related data that is needed to process these failures.
In the above example, if the only kind of failure we might be interested in handling is the failure to find the user with the given name, then the following signature shall be used:
fun findUserByName(name: String): User? // Much better
If there is a business need to distinguish different failures and process these different failures in distinct ways on each invocation site, then the following kind of signature shall be considered:
sealed class FindUserResult {
data class Found(val user: User) : FindUserResult()
data class NotFound(val name: String) : FindUserResult()
data class MalformedName(val name: String) : FindUserResult()
// other cases that need different business-specific handling code
}
fun findUserByName(name: String): UserResult
This is one of the reasons that
SuccessOrFailure
was not namedResult
. That is, to prevent its abuse as a "universal result class" and to encourage declaration of more domain-specific and more explanatory result classes.
Exceptions in Kotlin are designed for the failures that usually do not require local handling at each call site.
This includes several broad areas — logic and programming errors like index bounds problems and various checks
for internal invariants and preconditions, environment problems, out of memory conditions, etc.
These failures are usually non-recoverable (or are not supposed to be recovered from) and are handled in some
centralized way by logging or otherwise reporting them for troubleshooting, typically terminating application
or, sometimes, attempting to restart or to reinitialize an application as a whole or just its failing subsystem.
This is where default exceptions behaviour to abort current operation and propagate it up the call stack comes in handy.
External environment problems like network or file input/output errors represent a corner case here.
It is cumbersome to require their local handling by the caller as it complicates sequential business
logic by obscuring it with code to handle IO errors, so it is idiomatic in Kotlin to use exceptions (like IOException
)
for these. However, they are often handled at a more granular level than some global error-handling code.
These errors often require some specific user-interaction and can require domain-specific retry or recovery code.
Exceptions are also very expensive to create, but relatively cheap to throw, because they carry a lot of additional metadata, like stack trace and message to aid in debugging. They are extremely valuable when this metadata is written to the log for developers to aid in troubleshooting, but all that metadata is useless if exception is to be consumed by some business-logic to make some business-decision based simple on the presence of exception. Use nullable types or domain-specific classes to represent failures that need specific handling.
So, in case when findUserByName
failure does not require local handling by the caller, then its failure
should be represented by exception and its signature should look like this:
fun findUserByName(name: String): User
This signature is fine if we always sure that user shall be found, unless we have bugs, environment or IO issues.
If invoker of this function wants to perform multiple operations and process their failures afterwards
(without aborting on the first failure), it can always use runCatching { findUserByName(name) }
to make it explicit that a failure is being caught and
encapsulated into SuccessOrFailure
instance. If the need to do so happen often, then it is fine to define
a function named findUserByNameCatching
that returns SuccessOrFailure<User>
in addition to findUserByName
, following the convention of Catching
suffix in the name of the function
that encapsulates failures.
That is the only case when having a function that directly returns
SuccessOrFailure<T>
is appropriate. To recap: it shall haveCatching
suffix in its name and it shall be written in addition to the function withoutCatching
suffix.
A function that returns
List<SuccessOrFailure<T>>
should also haveCatching
suffix in its name, but it is not required to have a kin without this suffix, since this bulk error-handling use-case cannot be represented otherwise.
Kotlin Standard Library provides rich collection of transformations for nullable types that are idiomatic in Kotlin to indicate failure when no additional information about the failure is needed. However, there is no build-in support for non-standard exception handling in the Standard Library -- exceptions always terminate operation and propagate to the caller.
Other programming languages include a similar facility to represent a union of success and failure in their standard library with the following names:
Try[T]
in Scala is similar to the proposedSuccessOrFailure<T>
.Result<T, E>
in Rust (also parametrized by the type of error).Exceptional e t
in Haskell (also parametrized by the type of error).expected<E, T>
in C++ (proposed, also parametrized by the type of error).
Existing Kotlin libraries that provide similar functionality:
Try<T>
from Arrow library.Result<T, E>
from @kittinunf.
Note, that both of the libraries above promote "Railway Oriented Programming" style with monads and their transformations, which heavily relies on functions returning
Try
,Result
, etc. This programming style can be implemented and used in Kotlin via libraries as the above examples demonstrate. However, core Kotlin language and its Standard Library are designed around a direct programming style in mind and so returningSuccessOrFailure
as function result is discouraged. The general approach in Kotlin is that alternative programming styles should be provided as 3rd party libraries and DSLs.
For a more detailed comparison of Scala's Try
and its Kotlin analogue in Arrow library with this SuccessOrFailure
class
see Appendix.
This API shall be placed into the Kotlin Standard Library. Since the proposed API is fairly small and does not
clearly belong to any larger group of APIs, it should be placed directly into kotlin
package.
This section lists unresolved (open) or contentious questions about this design.
The proposed name is SuccessOrFailure
was chosen because it clearly express that this class is a union of
successful and failed outcomes. Some alternative names like Result
and Try
were discussed and the
following objections were raised:
- A shorter name does not clearly indicate what this wrapper class actually represents.
Neither
Result
norTry
have the corresponding connotations.Exception
orExpected
were not at the table and the time of naming discussion. - A shorter name would encourage abuse of this class as a return type of a function in cases where simply throwing an exception and letting a caller decide works better (see Error-handling style and exceptions).
Parameterizing this class by the type of exception like SuccessOrFailure<T, E>
is possible, but raises the
following problems:
- It increases verboseness without providing improvement for any of the outlined Use cases.
- Kotlin currently lacks facility to specify default values for generic type parameters.
- It leads to abuse in cases where a user-provided API-specific sealed class would work better.
It is possible to define a separate class like SuccessOrFailureEx<T, E>
that is parametrized by
both successful type T
and failed type E
(that must extend Throwable
)
and then define SuccessOrFailure<T>
and a typealias
to SuccessOrFailureEx<T, Throwable>
.
However, this creates its own problems:
- Typealiases are quite verbosely rendered by IDE in signatures and there is no clear way on making them better.
- We cannot succinctly define
runCatching
function and otherCatching
functions to make them usable both with and without explicit caught type specification. We'll have to have two different names for such a function: one for a function with an additionalE: Throwable
type parameter that must be specified and another one without it. Moreover, specifyingE
on call site requires specifying return type, too, since partial type parameter specification is not currently possible in Kotlin.
All in all, it does not seem that the costs outweigh whatever benefits it might bring.
Defining an even more general
Either<L, R>
type as a discriminated union between between two arbitrary typesL
andR
and then usingtypealias SuccessOrFailure<T> = Either<Throwable, T>
raises similar problems with an additional burden of designing functions forEither
that would not needlessly pollute the namespace of functions applicable toSuccessOrFailure
. We don't have sufficient motivating use-cases for havingEither
in the Kotlin Standard Library beyond theoretical desire to baseSuccessOrFailure
upon it.
We need to clearly mark functions that return SuccessOrFailure
and this KEEP suggests Catching
suffix
like in runCatching
. Alternatives:
OrCatch
suffix as inrunOrCatch
(inspired byOrNull
suffix).OrFailure
suffix as inrunOrFailure
(same inspiration).
Using SuccessOrFailure
as the return type poses a problem that it might accidentally get lost, thus loosing
unhandled exception. The problem is somewhat alleviated by naming
all such functions with Catching
suffix, but finding potential lost exception in the code that uses SuccessOrFailure
is still non trivial challenge.
Consider this code from Functional bulk manipulation of failures:
readFilesCatching(files).map { result ->
result.map { it.doSomething() }
}
If doSomething
here throws an exception, then all exceptions that were returned in a list by readFilesCatching
are lost.
Some IDE inspections can be designed to detect these kinds of problems. It is an open question how exactly they should work and and whether it is really a big problem after all.
API for SuccessOrFailure
class is designed to be quite bare-bones, because we'd like to discourage its
wide-spread use in the return types of the functions. In general, functions should not use SuccessOrFailure
as their
result. However, according to Functional bulk manipulation of failures use-case,
one might occasionally encounter List<SuccessOrFailure<T>>
or another collection of SuccessOrFailure
instances.
It is open question whether we should provide additional extensions in the Standard Library to represent common
operations on such collections and what those operations might be.
This section lists potential directions for future enhancement. None of them are worked out at the moment and all of them are purely tentative.
Kotlin inline
classes cannot be currently used with sealed class
construct.
If that is supported in the future, then we could change implementation of
SuccessOrFailure
without affecting its public APIs and binary interfaces in the following way:
sealed inline class SuccessOrFailure<T> {
inline class Success<T>(val value: T) : SuccessOrFailure<T>()
class Failure<T>(val exception: Throwable) : SuccessOrFailure<T>()
}
Notice, that only
Success
case is marked withinline
modifier here. That is the case that should be represented without boxing. In general, ifinline sealed
classes are allowed in the future, then Kotlin compiler could only supportinline
modifier on a set of subclasses with pairwise non-intersecting types of their primary constructor properties. In particular, bothSuccess
andFailure
cannot beinline
at the same time, since we would not be able to distinguishSuccess(Exception(...))
fromFailure(Exception(...))
at run time.
These changes would make it possible to use result is Success
and result is Failure
expressions and get advantage of
smart casts instead of result.isSuccess
and result.isFailure
that are currently provided and which do not work
with smart casts.
If Kotlin
adds some form of support for type parameter default values and partial type inference,
then we can consider extending SuccessOrFailure
class with an additional type parameter E: Throwable
that represents
the base class for caught exceptions. For example, in input/output code there may be a desire to catch only
IOException
and its subclasses, while aborting on any other exception using something like
runCatching<_, IOException> { code }
assuming that return type can be still inferred
(potential partial type inference syntax here is used for illustration only).
Kotlin nullable types have extensive support in Kotlin via operators ?.
, ?:
, !!
, and T?
type constructor
syntax. We can envision better integration of SuccessOrFailure
into the Kotlin language in the future.
However, unlike nullable types, that are often used to represent non signalling failure that does not cary
additional information, SuccessOrFailure
instances also carry additional information and, in general, shall be
always handled in some way. Making SuccessOrFailure
an integral part of the language also requires a
considerable effort on improving Kotlin type system to ensure proper handling of encapsulated exceptions.
One potential direction is to design a special syntax for SuccessOrFailure<T>
. For example, it might be
represented as T throws Throwable
(note, all of the following is a pure speculation),
with natural support for a more specific list of exceptions that can
be encapsulated inside the given SuccessOrFailure<T>
instance,
so one can write T throws IOException
type, for example.
Using this hypothetical syntax we can declare the function from Error-handling style section in the following way:
fun findUserByName(name: String): User throws NotFoundException, MalformedNameException
However, unlike throws
annotation in Java, User throws NotFoundException, MalformedNameException
is going to
be considered a return type of this function that explicitly lists exceptions that must be handled locally.
There will be no silent propagation of these listed exception types up to the caller. The caller will be
required to handle them just like with nullable types. When one writes:
val result = findUserByName(name)
Then inferred type of result
will be User throws NotFoundException, MalformedNameException
and it will be
stored in SuccessOrFailure<User>
behind the scenes. Direct access to the User
methods and extensions on
User throws
type will be forbidden, but all the ?.
, ?:
, and !!
operators can be extended to work with those throws
types in a similar way as it happens with nullable types today. Some additional operators might be required, too.
Unlike checked exception in Java, these are going to be full-blown types, so they play nicely with collections
(List<Data throws IOException>
is going to be a valid type represented as List<SuccessOrFailure<Data>>
behind
the scenes) and all the higher-order functions in Kotlin will work properly with those types without all the problems
that made it impossible to properly integrate checked exceptions with Java generics.
Moreover, it can be very efficiently implemented on JVM in the return type position by actually throwing the corresponding
exceptions inside and catching them outside, on the caller side, so no boxing will be required even for primitive
results. All in all, it could provide a safe replacement for checked exceptions on JVM and open a path to a better
integration with JVM APIs that rely on checked exceptions. However, details of this interoperability will have to
be worked out as there are lots of problems down this path. We cannot just lift all Java functions with throws
into
Kotlin functions with such throws
type not only because of backwards compatibility, but also due to the way checked
exceptions are (ab)used in the JVM ecosystem, so are more fine-grained control for interoperability will have to
be designed.
It is all beyond the scope of this KEEP.
You can skip this appendix is you are not familiar with Scala's or Arrow's Try
monad that provides very
similar functionality to this SuccessOrFailure
class.
If you are familiar with Try
monad, then you might ask why there is no flatMap
function on the SuccessOrFailure
class. This function could have been defined with the following signature:
inline fun <R, T> SuccessOrFailure<T>.flatMap(transform: (T) -> SuccessOrFailure<R>): SuccessOrFailure<R>
The usual reason to have flatMap
is to avoid "nesting" of monadic types when combining multiple
functions that return them, like in the following example:
d.awaitCatching().map { it.doSomethingCatching() } // : SuccessOrFailure<SuccessOrFailure<Data>> -- oops!
Functional code that uses Try
monad gets quickly polluted
with flatMap
invocations. To make such code manageable, a functional programming language is usually extended
with monad comprehension syntax to hide those flatMap
invocations.
However, we discourage writing functions that return SuccessOrFailure
as
a matter of style. If those functions are written, they should
only be present in addition to regular (throwing) ones.
Take a look at the following example code that
uses monad comprehension over Try
monad
(which is adapted from a guide
here):
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url) // here parseURL returns Try[URL], encapsulates failure
connection <- Try(url.openConnection())
input <- Try(connection.getInputStream)
source = Source.fromInputStream(input)
} yield source.getLines()
Adapting functions used here to Kotlin style, one can write this code in Kotlin, with the same semantics of aborting further progress on the first failure, in the following way:
fun getURLContent(url: String): List<String> {
val url = parseURL(url) // here parseURL returns URL, throws on failure
val connection = url.openConnection()
val input = connection.getInputStream()
val source = Source.fromInputStream(input)
return source.getLines()
}
Notice, that monad comprehension over Try
monad is basically built into the Kotlin language.
That is how imperative control flow works in Kotlin out of the box and there is no need to emulate it
via monad comprehensions. If callers of this function need an encapsulated failure,
they can always use runCatching { getURLContent(url) }
expression.
However, the Kotlin is not exactly equivalent to the initial code with Try
.
Let us see what are the differences. The original parseURL
have been returning an encapsulated exception
and it could be making a fine grained decision on which kinds of exceptions shall be encapsulated into the result
and which kinds of exceptions shall be thrown. Rewritten code propagates any failure in parseURL
up to the caller
without this fine grained distinction between different kinds of failures. There is also a subtle difference on
the fromInputStream
invocation. Original code would fail with exception if this invocation fails, while any failure
in openConnection
and getInputStream
is encapsulated into the result of the function via Try
. Rewritten code
does not make distinctions between different kinds of failures anymore.
All in all, the differences can be summarized as follows. SuccessOrFailure
is a blunt tool designed to catch
any failure in the function invocation for the processing later on. That is why, as a matter of
style, we don't recommend writing functions that return SuccessOrFailure
.
On the other hand, libraries like Arrow provide utility classes like Try
and the
corresponding extension functions that enable more fine-grained control. When a function is declared with Try<T>
as its result type, it means that this function can make a fine-grained decision on which failures are encapsulated
and which failures are thrown up the call stack.
If your code needs fine-grained exception handling policy, we'd recommend to design your code in such a way, that
exceptions are not used at all for any kinds of locally-handled failures
(see section on style for example code
with nullable types and sealed data classes). In the context of this appendix, parseURL
could return a nullable
result (of type URL?
) to indicate parsing failure or return its own purpose-designed sealed class that would provide
all the additional details about failure (like the exact failure position in input string)
if that is needed for some business function
(like setting cursor to the place of failure in the user interface).
In cases when you need to distinguish between different kinds of failures and these approaches do not work for you,
you are welcome to write your own utility libraries or use libraries like Arrow
that provide the corresponding utilities.