14.
Error Handling With Functional Programming
Written by Massimo Carli
In the first two sections of this book, you learned everything you need to know about pure functions. You learned that a pure function has a body that’s referentially transparent and, maybe more importantly, doesn’t have side effects. A side effect is a “disturbance” of the world external to the function, making the function difficult to test. Exceptions are a very common example of a side effect. A function containing code that throws an exception isn’t pure and must be fixed if you want to use all the concepts you’ve learned so far.
In this chapter, you’ll learn how to handle exceptions — and errors in general — using a functional programming approach. You’ll start with a very simple example before exploring the solution Kotlin provides using the Result<T> data type. In particular, you’ll learn:
- What exception handling means.
- How to handle exceptions as side effects.
- How to “purify” functions that throw exceptions.
- How to use
Optional<T>in functions with exceptions. - How to use
Either<E, T>to handle exceptions in a functional way. - How to implement a
ResultAp<E, T>monad. - How to compose functions that can throw exceptions.
- What the Kotlin
Result<T>data type is and how to use it in your application.
You’ll learn all this with a practical example that allows you to fetch and parse some content from the internet. This is the same code you’ll use in the RayTV app, which allows you to access data about your favorite TV shows.
Note: You’ll use the RayTV app in the following chapters as well.
You’ll also have the chance for some fun with a few exercises.
Exception handling
Note: Feel free to skip this section if you already have a clear understanding of what exceptions are and why they exist.
Every application is the implementation of a set of use cases in code. A use case is a way you describe how a specific user can interact with a system. Figure 14.1 is an example of a use case describing the scenario when a RayTV app user wants to search for their favorite TV show.
This use case diagram says two main things:
- Who can access the specific feature of the app. In this case, it’s the RayTV app user.
- What the RayTV app can do. In this case, it allows the user to search for a TV show by name.
Usually, you also describe the use case in words by writing a document like this:
Use Case: Search TV shows
Actors: RayTV app
Prerequisites: The user starts the RayTV app.
Steps:
1. The user selects the search option.
2. The app shows the keyboard.
3. The user inputs some text.
4. The user taps the search button.
5. The app accesses the search service and fetches the results.
6. The app displays the results in a list.
Final state: The app displays the results of the search.
It describes what the user can do and how the system reacts. This use case is very specific, though. It describes when everything works fine. You call this the happy path or principal use case. Many things can go wrong. For instance, a network error can happen, or the search simply doesn’t produce any results. In this case, you describe these situations using alternative use cases, like the one in Figure 14.2:
In this case, you describe the scenario as a use case that shows what happens when the happy path isn’t followed. In this case, the search doesn’t produce any result. Usually, you describe this scenario with a document like this:
Use Case: Handle empty results
Actors: RayTV app
Prerequisites: A TV show search produced no results.
Steps:
1. The app displays a message that says the search has produced no results.
Final state: The app waits for action from the user.
This is all good, but what do use cases have to do with errors and exceptions? Well, exceptions aren’t always bad things. They’re just a way programming languages represent alternative scenarios. In general, you have three different types of situations:
-
Errors: These represent cases when something really wrong happens. They aren’t recoverable, and the developer should understand the cause and solve it. Typical examples are
VirtualMachineException,IOErrororAssertionError. - Checked exceptions: This is the type of exception that describes alternative use cases. If you have a connection error, you should somehow handle it. A user entering an invalid credit card number is a common occurrence, so it’s something your code should be prepared to handle. These are checked because in some programming languages, like Java, you have to explicitly declare them in the signature of the method that can throw them.
-
Unchecked exceptions:
RuntimeExceptionis another way to describe this type of exception because it can happen at runtime. A typical one isNullPointerException, which is thrown when you access a member of an object through a null reference. Although you can recover from aRuntimeException, they usually describe bugs you should eventually fix.
Kotlin doesn’t have checked exceptions, which means you don’t need to declare them as part of the function throwing them. This doesn’t mean you shouldn’t handle the exception. Kotlin represents them with classes extending Throwable and still provides you the try-catch-finally expression. So what’s the best way to handle exceptions controlling the flow of your program? Now that you have a solid functional programming background, you know that exceptions are side effects. How can you use what you’ve learned to handle exceptions elegantly and efficiently? Monads, of course! :]
Note: It’s interesting to remember how the type of the following expression is
Nothing:throw IOException("Something bad happens")Remembering the analogy between types and sets, you know that
Nothingis a subtype of any other type. This means the value you return when you throw an exception has a type compatible with the return type of the function itself.
Handling exception strategies
In Chapter 13, “Understanding Monads”, you learned that functions throwing exceptions are impure. An exception changes the world outside the body of a function. In the same chapter, you also learned how to purify a function by changing the side effect to be part of the return type. The same works for exceptions, and you can achieve this using different data types depending on the strategy you want to adopt. Different strategies include:
- Fail fast: Stopping the execution flow of your program and returning the first error that happens. You use this approach when you can’t proceed because you need data that you failed to get in a previous step.
- Collection errors: Proceeding in your program flow, collecting all the errors and making them part of the final result. Validation of some data is a classic example. An address, for instance, can be wrong for many different reasons, and you’d like to collect them all.
For each of these strategies, you can use functional programming:
-
Optional<T>orEither<E, T>for failing fast. - Applicative functors for collection errors.
It’s now helpful to see both of them using some practical examples.
Some preparation
Before describing the different ways of handling exceptions, it’s useful to look at some of the classes you find in the material for this chapter. These are the classes you initially find in the fp_kotlin_cap14 project, and you’ll see them again in the RayTV app in their final state. The RayTV app will allow you to search for a TV show and display some of the show’s information on the screen. This is basically the use case in Figure 14.1.
Note: For the RayTV app, you’ll use a simple API from TVmaze, which doesn’t require a key or registration.
In the tools sub-package, you already have the code for:
- Fetching the TV show data, given some query as input.
- Parsing the JSON to get the information you need encapsulated into a model you find in the model sub-package.
Note: All the sub-packages are related to the main package for the application, which is com.raywenderlich.fp.
In TvShowFetcher.kt, you have the code to send a request to the server, which returns a String.
object TvShowFetcher {
fun fetch(query: String): String {
val encodedUrl = java.net.URLEncoder.encode(query, "utf-8")
val localUrl =
URL("https://api.tvmaze.com/search/shows?q=$encodedUrl")
with(localUrl.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader = inputStream.bufferedReader()
return reader.lines().toArray().asSequence()
.fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
}
}
}
In TvShowParser.kt, you have the code that parses the JSON, returning a list of ScoredShow, which is the class you use to describe each result.
Note:
TvShowParseruses thekotlinx.serializationlibrary.
object TvShowParser {
/** Parses the input json */
fun parse(json: String): List<ScoredShow> = Json {
ignoreUnknownKeys = true
}.decodeFromString<List<ScoredShow>>(
ListSerializer(ScoredShow.serializer()), json
)
}
How TvShowFetcher and TvShowParser accomplish the fetching and parsing of the data from the server isn’t relevant. However, it’s crucial to note that neither fetch nor parse handles exceptions. If something goes wrong, you’ll get an exception that the callers should handle.
To prove that, run some unit tests you find where shown in Figure 14.3:
Of course, TvShowFetcher makes a network request, which can fail for many reasons, like a simple missing connection. The TvShowParser can fail simply by getting an incorrect JSON input. But how can you handle these exceptions?
Handling errors with Optional<T>
The RayTV app sends a request to the server, gets some JSON back, parses it and displays the result to the user. This is the happy path, but many things can go wrong. Both TvShowFetcher::fetch and TvShowParser::parse can fail.
Open ShowSearchService.kt in optional and add the following code:
fun fetchTvShowOptional(
query: String
): Optional<String> = try { // 1
Optional.lift(TvShowFetcher.fetch(query))
} catch (ioe: IOException) {
Optional.empty()
}
/** Invokes the parser returning an Optional */
fun parseTvShowString(
json: String
): Optional<List<ScoredShow>> =
try { // 2
Optional.lift(TvShowParser.parse(json)) // 2
} catch (e: SerializationException) {
Optional.empty()
}
Here, you define:
-
fetchTvShowOptional, which invokesTvShowFetcher::fetchand returns anOptional<String>with the JSON in the case of success. In the case of an error, it simply returnsNone. -
parseTvShowString, which invokesTvShowParser::parseand returns anOptional<List<ScoredShow>>in the case of success andNonein the case of an error.
Both use the try-catch expression and the Optional<T> type you find in lib.
Now, fetchTvShowOptional has type (String) -> Optional<String> and parseTvShowString has type (String) -> Optional<List<ScoredShow>>. What if you want to fetch and then parse?
This is exactly what you learned in Chapter 13, “Understanding Monads”, where you implemented flatten and bind to basically create flatMap.
In Chapter 9, “Data Types”, you implemented flatMap like:
fun <A, B> Optional<A>.flatMap(
fn: Fun<A, Optional<B>>
): Optional<B> = when (this) {
is None -> Optional.empty()
is Some<A> -> {
val res = fn(value)
when (res) {
is None -> Optional.empty()
is Some<B> -> Optional.lift(res.value)
}
}
}
In Chapter 13, “Understanding Monads”, you implemented this too, providing an implementation for flatten. In any case, you compose fetchTvShowOptional and parseTvShowString, adding this code to ShowSearchService.kt in the optional package:
fun fetchAndParseTvShow(query: String) =
fetchTvShowOptional(query)
.flatMap(::parseTvShowString)
fetchAndParseTvShow is now the composition of fetchTvShowOptional and parseTvShowString, and it has type (String) -> Optional<List<ScoredShow>>.
To test how it works, just run this:
fun main() {
fetchAndParseTvShow("Big Bang Theory") // 1
.getOrDefault(emptyList<ScoredShow>()) pipe ::println // 2
}
Here, you:
- Invoke
fetchAndParseTvShow, getting anOptional<List<ScoredShow>>. - Use
getOrDefaultto provide a default value in the case of an error.
Getting something like:
[ScoredShow(score=1.2612172, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> ... </p>, language=English))]
This is quite good because it allows you to test each function in isolation and then trust the composition between two functions in the Kleisli category, as you learned in Chapter 13, “Understanding Monads”. However, you can do much better. Disconnect your computer and run the code again, and you’ll get the following output:
[]
Is this an error, or is this the actual result coming from the server? With the previous code, you’d never know. You need a way to get more information about the error. Regardless, a good point about this solution is that you don’t invoke parseTvShowString at all if the fetchAndParseTvShow fails.
Handling errors with Either<E, T>
The Optional<T> data type is very useful, but it just tells you if you have a response; it doesn’t tell you what the error is if something goes wrong. In this case, a data type like Either<E, T> can help you. In lib, you’ll find Either.kt, which contains all the code you implemented in Chapter 9, “Data Types”. To see how to use it, just add the following code to ShowSearchService.kt, this time in the either sub-package:
fun fetchTvShowEither(
query: String
): Either<IOException, String> = try {
Either.right(TvShowFetcher.fetch(query)) // 1
} catch (ioe: IOException) {
Either.left(ioe)
}
/** Invokes the parser returning an Optional */
fun parseTvShowEither(
json: String
): Either<SerializationException, List<ScoredShow>> = try {
Either.right(TvShowParser.parse(json)) // 2
} catch (e: SerializationException) {
Either.left(e)
}
This case is somewhat similar to Optional<T>, but using Either<E, T>. Here, you define:
-
fetchTvShowEither, which invokesTvShowFetcher::fetchand returns the result asEither.Right<String>in the case of success and the exception in anEither.Left<IOException>in the case of an error. Note how the result type isEither<IOException, String>. -
parseTvShowEither, which invokesTvShowParser::parseand returns the result asEither.Rightin the case of success and the exception in anEither.Leftin the case of an error. In this case, the return type isEither<SerializationException, List<ScoredShow>>.
As you did with Optional<T>, you use flatMap to create the following, which you add to the same file:
fun fetchAndParseTvShowEither(query: String) =
fetchTvShowEither(query)
.flatMap(::parseTvShowEither)
Here, you use flatMap to compose fetchTvShowEither and parseTvShowEither in a function of type (String) -> Either<Exception, List<ScoredShow>>. Note how the type parameter value for Left<T> is Exception and not IOException or SerializationException. This is because Exception is the common ancestor type of the two, and this depends on the specific implementation of flatMap you implemented in Chapter 9, “Data Types”, and you find in Either.kt in lib:
fun <A, B, D> Either<A, B>.flatMap(
fn: (B) -> Either<A, D>
): Either<A, D> = when (this) {
is Left<A> -> Either.left(left)
is Right<B> -> {
val result = fn(right)
when (result) {
is Left<A> -> Either.left(result.left)
is Right<D> -> Either.right(result.right)
}
}
}
To test fetchAndParseTvShowEither, simply add and run the following code:
fun main() {
fetchAndParseTvShowEither("Big Bang Theory")
.leftMap {
println("Error: $it")
}
.rightMap {
println("Result: $it")
}
}
Getting:
Result: [ScoredShow(score=1.2627451, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy...</p>, language=English))]
The interesting part happens when you have some errors. Disconnect your computer from the network and run the code again. You’ll get something like:
Error: java.net.UnknownHostException: api.tvmaze.com
If you restore the network and “sabotage” parseTvShowEither by adding some text to the json parameter like this:
fun parseTvShowEither(json: String): Either<SerializationException, List<ScoredShow>> =
try {
Either.right(TvShowParser.parse(json+"sabotage")) // HERE
} catch (e: SerializationException) {
Either.left(e)
}
You’ll get:
Error: kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 1589: Expected EOF after parsing, but had s instead
JSON input: .....ze.com/episodes/1646220}}}}]sabotage
As you see, the first exception that happens is the one you’ll get as output. This is also true because you can’t parse JSON you don’t have in the case of a fetching error. In some cases, you want to collect all the exceptions that happen, which is a classic use of the applicative functor.
Applicative functor
In the previous section, you learned how to handle exceptions using Optional<T> and Either<E, T> data types following a fail fast approach that stops the execution flow at the first exception. With Either<E, T>, you also get what’s wrong. In Chapter 9, “Data Types”, you learned that Either<A, B> is a bifunctor, which is an algebraic data type representing the addition of two types, A and B. In the previous use case, you used Left<E> to represent the error case and Right<T> for the success case.
In the context of error handling, you usually create a dedicated data type you call Result<E, T>.
Note: As you’ll learn later, Kotlin provides a built-in
Result<T>data type, which is different from the one you’ll create in this section. To avoid conflicts, you’ll call yoursResultAp<E, T>, where theApsuffix represents its applicative behavior.
As a first step, open ResultAp.kt in applicative and add the following code:
sealed class ResultAp<out E : Throwable, out T> { // 1
companion object {
@JvmStatic
fun <E : Throwable> error(
error: E
): ResultAp<E, Nothing> = Error(error) // 2
@JvmStatic
fun <T> success(
value: T
): ResultAp<Nothing, T> = Success(value) // 2
}
}
data class Error<E : Throwable>(
val error: E
) : ResultAp<E, Nothing>() // 3
data class Success<T>(
val value: T
) : ResultAp<Nothing, T>() // 3
fun <E1 : Throwable, E2 : Throwable, T> ResultAp<E1, T>.errorMap(
fl: (E1) -> E2
): ResultAp<E2, T> = when (this) { // 4
is Error<E1> -> ResultAp.error(fl(error))
is Success<T> -> this
}
fun <E : Throwable, T, R> ResultAp<E, T>.successMap(
fr: (T) -> R
): ResultAp<E, R> = when (this) { // 5
is Error<E> -> this
is Success<T> -> ResultAp.success(fr(value))
}
ResultAp<E, T> is very similar to Either<E, T> and, besides the name, it differs in that:
- Parameter type
EhasThrowableas its upper bound. This allows you to just use exceptions for the typeE, but you can also remove that limitation if you prefer. - Factory methods are now called
errorandsuccessto make their meaning explicit. -
ErrorandSuccesstypes replaceLeftandRight. -
mapLeftbecomeserrorMap. -
mapRightbecomessuccessMap.
Exercise 14.1:
ResultAp<E, T>is very similar toEither<E, T>. Can you implementflatMapfor it as well?
Exercise 14.2: In the previous paragraphs, you implemented a simple system to fetch and parse data using both
Optional<T>andEither<E, T>. Can you do the same usingResultAp<E, T>?
Yeah, it’s true. With ResultAp<E, T>, you haven’t done much yet, but now comes the fun!
ResultAp<E, T> as an applicative functor
One of the most important things you’ve learned in this book is that functions are values, which is why you can implement higher-order functions. This also means that T in ResultAp<E, T> can be a function type like (T) -> T. So, what would the meaning be of a function ap with the following signature?
fun <E : Throwable, T, R> ResultAp<E, T>.ap( // 1
fn: ResultAp<E, (T) -> R> // 2
): ResultAp<E, R> { // 3
// ...
}
In this code, ap:
- Is an extension function for the
ResultAp<E, T>type. - Accepts a parameter of type
ResultAp<E, (T) -> R>where the value in the case of success is a function(T) -> R. - Returns
ResultAp<E, R>.
This is basically a way to apply a function to a value only in the case of success, as you see in the implementation you add in ResultAp.kt in applicative:
fun <E : Throwable, T, R> ResultAp<E, T>.ap(
fn: ResultAp<E, (T) -> R>
): ResultAp<E, R> = when (fn) {
is Success<(T) -> R> -> successMap(fn.value)
is Error<E> -> when (this) {
is Success<T> -> Error(fn.error)
is Error<E> -> Error(this.error)
}
}
Cool. Now you understand what it is, but how can ap be useful in the context of exception handling? A typical case involves validation.
Open Validation.kt in validation and add the following User data class.
data class User(
val id: Int,
val name: String,
val email: String
)
Now, imagine you want to create a User starting with some values you enter from a UI, and those values require some sort of validation. To simulate that, add this code to the same file:
class ValidationException(msg: String) : Exception(msg) // 1
/** Name validation */
fun validateName(
name: String
): ResultAp<ValidationException, String> =
if (name.length > 4) {
Success(name)
} else {
Error(ValidationException("Invalid name"))
} // 2
/** Email validation */
fun validateEmail(
email: String
): ResultAp<ValidationException, String> =
if (email.contains("@")) {
Success(email)
} else {
Error(ValidationException("Invalid email"))
} // 3
Note: Of course, you can make the
ValidationExceptionmore informative, explaining what’s wrong and how to fix it.
In this code, you define:
-
ValidationExceptionas a simpleException, representing validation errors. -
validateNameas a function that validates the name property. -
validateEmailas doing the same for emails.
Now comes the magic! Just add this to the same file:
fun main() {
val userBuilder = ::User.curry() // 1
val userApplicative = ResultAp.success(userBuilder) // 2
val idAp = ResultAp.success(1) // 3
validateEmail("max@maxcarli.it") // 6
.ap(
validateName("") // 5
.ap(
idAp.ap(userApplicative) // 4
)
)
.errorMap { // 7
println("Error: $it"); it
}
.successMap { // 7
println("Success $it")
}
}
This code has numerous interesting points:
-
::Useris how you represent a reference to a constructor in Kotlin. As mentioned, this is a function of type(Int, String, String) -> User. Using thecurryimplementations you find in Curry.kt in lib, you get a function of type(Int) -> (String) -> (String) -> User, which you save inuserBuilder. - Using
ResultAp::success, you save the reference of a function of typeResultAp<ValidationException, (Int) -> (String) -> (String) -> User>inuserApplicative. - You don’t validate the value for
id, so you just create aResultAp.Success<Int>from it. - Remember that
userApplicativehas typeResultAp<ValidationException, (Int) -> (String) -> (String) -> User>. InvokingaponidAp, you basically get aResultAp<ValidationException, (String) -> (String) -> User>. Note how this invocation has somehow swallowed theIntparameter. - You now pass the value you got from the previous point to
apof theResult<ValidationException, String>you get fromvalidateName. This swallows another parameter, and you get aResultAp<ValidationException, (String) -> User>. - Finally, you now pass the last value to the
ResultAp<ValidationException, String>you get fromvalidateEmailand get aResult<ValidationException, User>, which is the final result. - You can use
errorMapto handle theValidationExceptionorsuccessMapto handle instances ofUserthat survived validation.
Run the previous code, and you’ll get:
Error: com.raywenderlich.fp.validation.ValidationException: Invalid Name
This is because the name you’re passing is empty.
Just apply this change to add a name and run the code again:
fun main() {
// ...
validateEmail("max@maxcarli.it")
.ap(
validateName("Massimo") // HERE
.ap(idAp.ap(userApplicative))
)
// ...
}
You’ll get:
Success User(id=1, name=Massimo, email=max@maxcarli.it)
This looks good, but you still have two problems to solve:
- As an expert functional programming engineer, you probably don’t like all those parentheses. It would be nice to make the syntax simpler.
- If you enter an invalid email and an invalid name, you only get the error about the former. It would be nice to know about both.
You can solve the first problem by adding the following code that creates appl as an infix version of ap:
infix fun <E : Throwable, A, B> ResultAp<E, (A) -> B>.appl(
a: ResultAp<E, A>
) = a.ap(this)
Now, you can write the main like this:
fun main() {
val userBuilder = ::User.curry()
val userApplicative = ResultAp.success(userBuilder)
val idAp = ResultAp.success(1)
(userApplicative appl
idAp appl
validateName("Massimo") appl
validateEmail("max@maxcarli.it"))
.errorMap {
println("Error: $it"); it
}
.successMap {
println("Success $it")
}
}
Now, appl allows you to follow the same order for validation of the parameters in ::User. The only problem is a limitation of Kotlin that doesn’t allow you to set the precedence between the operators, forcing you to use parentheses before errorMap and successMap.
Run the previous code, and you’ll get:
Success User(id=1, name=Massimo, email=max@maxcarli.it)
The second problem gives you another opportunity to use a concept you learned about in previous chapters: semigroups.
Applicative functors and semigroups
As mentioned, the previous code doesn’t allow you to get all the validation errors, only the first. Open ValidationSemigroup.kt and add the following code:
fun main() {
val userBuilder = ::User.curry()
val userApplicative = ResultAp.success(userBuilder)
val idAp = ResultAp.success(1)
(userApplicative appl
idAp appl
validateName("") appl // HERE
validateEmail("")) // HERE
.errorMap {
println("Error: $it"); it
}
.successMap {
println("Success $it")
}
}
Run this code, and you get:
Error: com.raywenderlich.fp.validation.ValidationException: Invalid email
This is correct, but you can do better. Both name and email are invalid, but the error message has no mention of the former. You need to find a way to somehow accumulate the errors into one. In Chapter 12, “Monoids & Semigroups”, you learned that a monoid describes a way to combine two values into a single value of the same type. You can represent the properties of a monoid in different ways. For example, in ValidationSemigroup.kt, add the following definition:
interface Semigroup<T> {
operator fun plus(rh: T): T
}
The Semigroup<T> interface here defines types with the plus operator. Now, you can create a different type of ValidationException, which is also a Semigroup, like this:
data class ValidationExceptionComposite( // 1
private val errors: List<ValidationException> // 2
) : Exception(), Semigroup<ValidationExceptionComposite> {
override fun plus(
rh: ValidationExceptionComposite
): ValidationExceptionComposite =
ValidationExceptionComposite(this.errors + rh.errors) // 3
override fun getLocalizedMessage(): String {
return errors.joinToString { it.localizedMessage } // 4
}
}
In this code, you:
- Create
ValidationExceptionCompositeas a data class extendingExceptionand implementingSemigroup<ValidationExceptionComposite>. - Define
errorsas a variable containing all theValidationExceptions you want to combine. - Implement
plus, creating a newValidationExceptionCompositewhose errors are the union of the errors of the two operands. - Override
getLocalizedMessage, composing all thelocalizedMessages.
The next step now is to implement a version of ap, you call apsg, that handles Semigroups. In ValidationSemigroup.kt add the following code:
fun <E, T, R> ResultAp<E, T>.apsg(
fn: ResultAp<E, (T) -> R>
): ResultAp<E, R> where E : Throwable, E : Semigroup<E> = // 1
when (fn) {
is Success<(T) -> R> -> successMap(fn.value)
is Error<E> -> when (this) {
is Success<T> -> Error(fn.error)
is Error<E> -> Error(this.error + fn.error) // 2
}
}
The main things to note here are:
- The type parameter
Ehas two upper bounds. It must be aThrowableand aSemigroup<E>. - This is where the magic happens. Because
Eis aSemigroup<E>, you can use the+operator to combine them. Here, you have the case when you already have an error and you find a new one.
As you did previously for ap, you can provide an infix version by adding this code:
infix fun <E, T, R> ResultAp<E, (T) -> R>.applsg(
a: ResultAp<E, T>
) where E : Throwable, E : Semigroup<E> = a.apsg(this)
To use the new ValidationExceptionComposite, you need to provide new validation functions. Add the following:
fun validateNameSg(
name: String
): ResultAp<ValidationExceptionComposite, String> =
if (name.length > 4) {
Success(name)
} else {
Error(ValidationExceptionComposite(
listOf(ValidationException("Invalid name"))
))
}
fun validateEmailSg(
email: String
): ResultAp<ValidationExceptionComposite, String> =
if (email.contains("@")) {
Success(email)
} else {
Error(ValidationExceptionComposite(
listOf(ValidationException("Invalid email"))
))
}
Note how you now use ValidationExceptionComposite, which contains a list of ValidationException.
Finally, you can replace the main and use the new operators like this:
fun main() {
val userBuilder = ::User.curry()
val userApplicative = ResultAp.success(userBuilder)
val idAp = ResultAp.success(1)
(userApplicative applsg
idAp applsg
validateNameSg("") applsg
validateEmailSg(""))
.errorMap {
println(it.localizedMessage); it
}.successMap {
println("Success $it")
}
}
Run this code, and you’ll get:
Invalid email, Invalid name
Fix the email and run the code, getting:
Invalid name
Finally, fix the name and run the code, getting:
Success User(id=1, name=Massimo, email=max@maxcarli.it)
This is how, using ResultAp<E, T> as a specialized version of Either<E, T> and Semigroups, you implemented a typeclass called applicative functor to handle validation in a good, functional way.
The Kotlin Result<T> data type
As mentioned earlier, the Kotlin standard library has a Result<T> type that is similar to ResultAp<E, T>, which you implemented earlier.
Note: If you want to learn all about the
Result<T>API, the Kotlin Apprentice book is the right place to go.
Looking at the source code for Result<T>, you’ll notice that it’s not implemented as a sealed class. Result<T> has a single parameter type T, but the actual internal value has type Any?. This is because Result<T> handles the Failure case, assigning an instance of the internal class Result.Failure to value.
The goal here is to see if Result<T> is a functor first and then a monad.
Result<T> as a functor
To prove that Result<T> is a functor, you should verify the functor laws, and in particular, that:
map id == idmap (f compose g) == (map f compose map g)
First, you see that Result<T> APIs have map with the following implementation:
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R, T> Result<T>.map(
transform: (value: T) -> R
): Result<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when {
isSuccess -> Result.success(transform(value as T))
else -> Result(value)
}
}
Removing the contract part and replacing transform with id, you get:
public inline fun <R, T> Result<T>.map(): Result<R> {
return when {
isSuccess -> Result.success(id(value))
else -> Result(value)
}
}
In the case of success, you get:
public inline fun <R, T> Result<T>.map(): Result<R> =
Result.success(id(value))
Because id(value) = value, you have:
public inline fun <R, T> Result<T>.map(): Result<R> =
Result.success(value)
In the case of failure, you get:
public inline fun <R, T> Result<T>.map(): Result<R> =
Result(value)
Considering that the value in the case of failure is the Result.Failure instance itself, this completes the proof of the first functor law.
Proving the second law can be more verbose, but you can actually make it shorter by noting that the function transform you pass as a parameter is only used in the case of success. If you have a success, Result<T>, and map a function f first and then a function g, you’ll get the same value you’d get mapping f compose g.
Another, more pragmatic and quick proof that Result<T> is a functor is the presence of the map in the APIs. Looking at the same APIs, you can’t find a flatMap function, which makes you wonder if the Result<T> data type is a monad or not.
Result<T> as a monad
As mentioned at the end of the previous section, the Kotlin Result<T> data type doesn’t have a flatMap. What happens, then, if you need to compose a function of type (A) -> Result<B> with a function of type (B) -> Result<C>? Well, in Chapter 13, “Understanding Monads”, you learned how to handle this case with a generic data type M<A>. It’s time to do the same for Result<T>.
Open ResultMonad.kt in result and add the following code:
infix fun <A, B, C> Fun<A, Result<B>>.fish( // 1
g: Fun<B, Result<C>>
): (A) -> Result<C> =
{ a: A ->
this(a).bind(g)
}
infix fun <B, C> Result<B>.bind( // 2
g: Fun<B, Result<C>>
): Result<C> =
map(g).flatten()
In this code, you simply replaced M<T> with Result<T> in the definition of the operators:
fishbind
To do this, you need to implement flatten like the following:
fun <A> Result<Result<A>>.flatten(): Result<A> = // 1
if (isSuccess) {
getOrNull()!! // 2
} else {
Result.failure(exceptionOrNull()!!) // 3
}
Here, you:
- Define
flattenas an external function ofResult<Result<A>>withResult<A>as the return type. - Return its value in the case of success. Note that the value of
Result<Result<A>>has typeResult<A>. - Return what you get from
Result::failure, passing the exception in the case of failure.
Now, you can add the following code to the same file:
fun <A> Result<A>.lift(value: A): Result<A> = // 1
Result.success(value)
fun <A, B> Result<A>.flatMap(fn: Fun<A, Result<B>>): Result<B> =
map(::lift fish fn).flatten() // 2
Here, you define:
-
lift, which is basicallyResult::successwith a different name. -
flatMapusingfish,liftandflatten.
Now, you have all you need to implement ShowSearchService with Result<T>, as you did with Optional<T>, Either<E, T> and ResultAp<E, T>.
Using Result<T> as a monad
Open ShowSearchService.kt in result and add the following code:
fun fetchTvShowResult(query: String): Result<String> = try {
Result.success(TvShowFetcher.fetch(query)) // 1
} catch (ioe: IOException) {
Result.failure(ioe) // 2
}
fun parseTvShowResult(json: String): Result<List<ScoredShow>> =
try {
Result.success(TvShowParser.parse(json /* +"sabotage" */)) // 1
} catch (e: SerializationException) {
Result.failure(e) // 2
}
This code should be very familiar to you now. Here, you:
- Use
Result::successto create theResult<T>to return in the case of success, encapsulating the result. Note that infetchTvShowResult, there is a commented “sabotage”Stringyou can uncomment to simulate a failure in the parsing of the JSON in input. - Use
Result::failureto create theResult<T>to return in the case of failure. In this case, you encapsulate the exception as aThrowable.
As the next step, add the following code:
fun fetchAndParseTvShowResult(query: String) =
fetchTvShowResult(query) // 1
.flatMap(::parseTvShowResult) // 2
Here, you:
- Invoke
fetchTvShowResult, getting aResult<String>as a result. - Use
flatMappassingparseTvShowResultas parameter.
Finally, add the following code:
fun main() {
fetchAndParseTvShowResult("Big Bang Theory")
.fold(onFailure = {
println("Error: $it")
}, onSuccess = {
println("Result: $it")
})
}
Run main, and you’ll get something like the following:
Result: [ScoredShow(score=1.2637222, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>...</p>, language=English))]
As you did before, run again with your computer disconnected from the network, getting:
Error: java.net.UnknownHostException: api.tvmaze.com
Now, reconnect your computer and uncomment the “sabotage” String in parseTvShowResult that’s making the function fail. This time, you’ll get:
Error: kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 1589: Expected EOF after parsing, but had s instead
JSON input: .....ze.com/episodes/1646220}}}}]sabotage
Now, the Kotlin Result<T> data type is also a monad. :]
Meet the RayTV app
In the first part of the chapter, you learned how to handle exceptions in a functional way. You implemented two functions for fetching and parsing some data about TV shows using APIs provided by TVmaze.
Using Android Studio, open the RayTV project in the material for this chapter. When you run the project, after the splash screen, you’ll get what’s in Figure 14.4:
Enter some text in the TextField at the top of the screen, and you’ll get some results like the following:
When you select a TV show from the list, you see its details, as in Figure 14.6:
You’ll play with the RayTV app more in the following chapters. In this case, it’s interesting to look at how it handles errors.
Open ShowSearchService.kt in tools.api and look at the following code:
fun fetchTvShowResult(query: String): Result<String> =
try {
Result.success(TvShowFetcher.fetch(query))
} catch (ioe: IOException) {
Result.failure(ioe)
}
fun parseTvShowResult(json: String): Result<List<ScoredShow>> =
try {
Result.success(TvShowParser.parse(json /* +"sabotage" */))
} catch (e: SerializationException) {
Result.failure(e)
}
fun fetchAndParseTvShowResult(query: String) =
fetchTvShowResult(query)
.flatMap(::parseTvShowResult)
These are the same functions you implemented in the first part of the chapter. What’s more interesting is how the app uses them.
Open SearchViewModel.kt in ui.screen.search and look at the following code:
@HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() {
var searchState = mutableStateOf<SearchState>(NoSearchDone) // 1
private set
private var currentJob: Job? = null
fun findShow(showName: String) {
currentJob?.cancel()
currentJob = viewModelScope.launch(Dispatchers.IO) {
searchState.value = SearchRunning // 1
fetchAndParseTvShowResult(showName) // 2
.fold(onFailure = { // 3
searchState.value = FailureSearchResult(it)
}, onSuccess = { // 4
if (!it.isEmpty()) {
searchState.value = SuccessSearchResult(it)
} else {
searchState.value = NoSearchResult // 5
}
})
}
}
}
Besides some code related to the use of Jetpack Compose, Coroutines and Hilt, you can note the following:
- The initial state is
NoSearchDoneand becomesSearchRunningevery time you start a new search. - Every time you call
findShow, you invokefetchAndParseTvShowResult, passing theStringto search. - You use
foldand change the state toFailureSearchResultin the case of failure, encapsulating theThrowable. - In the case of success, you set the state to
SuccessSearchResult, encapsulating the result. - Here, you also use a special case if the query is successful but you don’t get any result.
In SearchViewModel, you basically bind every specific Result to a different UI state.
Note: If you want to learn everything you need to know about coroutines, the Kotlin Coroutines by Tutorials book is the right place to go. Jetpack Compose by Tutorials is the best resource for learning how to create UI using Compose. With Dagger by Tutorials, you’ll learn everything you need to know about Dagger and Hilt. Finally, Real World Android by Tutorials will help you understand the best way to put all these technologies together.
Now, open SearchComposable.kt in ui.screens.search and find the following code:
// ...
ErrorAlert(errorMessage = {
stringResource(R.string.error_message)
}) {
result is FailureSearchResult
}
// ...
ErrorAlert is a Composable function you find in Util.kt in ui.screens. It displays itself only if the current state is a FailureSearchResult.
To verify how it works, just enable the “sabotage” in parseTvShowResult in ShowSearchService.kt or disconnect your machine and run the app. When you try to search for a TV show, you’ll get what’s in Figure 14.7:
Key points
- Error handling is a fundamental part of any software application.
- Many programming languages model errors using exceptions.
- Exceptions are a classic example of side effects.
- For exceptions, you can use the same process you used for other impure functions: Make the side effect a part of the result type.
- You can use
Optional<T>andEither<E, T>to model functions throwing exceptions. - Applicative functors and semigroups are useful in the case of multiple validations.
- The Kotlin standard library provides the
Result<T>data type. -
Result<T>is a functor but not a monad. - You can make
Result<T>a monad by following the same process you used in Chapter 13, “Understanding Monads”.
Where to go from here?
Congratulations! In this chapter, you had the chance to apply all the principles and concepts you learned in the first two parts of the book in a real example. In the next chapter, you’ll learn everything you need to know about state.