Functional programming languages like Scala often have a type called "Try" to hold the result of a computation if successful or to capture an exception on failure.
This is an incredibly useful type, allowing a caller to pointedly control how to handle an exceptional scenario. In this post I will try and create such a type from scratch.
As an example, I will be using the scenario from Daniel Westheide's excellent introduction to the Try type in Scala
So my objective is to call a remote URL and return the content as a string. A few things can go wrong -
- The url can be badly formed
- The url may be wrong and may not have anything to retrieve content from
Let's start with the first one, the URL being badly formed, an API call using the "Try" type would look something like this:
fun parseUrl(url: String): Try<URL> { return Try.of { URL(url) } }Here a URL is being parsed and the result is a valid url or an exception. So a Try type that can implement this much, would look something like this:
sealed class Try<out T> { class Success<T>(private val result: T) : Try<T>() class Failure<T>(private val throwable: Throwable) : Try<T>() companion object { fun <T> of(block: () -> T) = try { Success(block()) } catch (e: Throwable) { Failure(e) } } }
"Try" type has two sub types - A "Success" wrapping a successful result and a "Failure" capturing an exception from the call.
With the two subtypes in place, let's extend the use of the Try type:
val urlResult: Try<URL> = parseUrl("htt://somewrongurl") assertThat(urlResult.isFailure()).isTrue()
assertThat(urlResult.isSuccess()).isFalse()
sealed class Try<out T> { abstract fun isSuccess(): Boolean fun isFailure(): Boolean = !isSuccess() class Success<T>(private val result: T) : Try<T>() { override fun isSuccess(): Boolean = true } class Failure<T>(private val throwable: Throwable) : Try<T>() { override fun isSuccess(): Boolean = false } ... }That works nicely, so now that a url is available, hopefully valid, lets get some content from the URL:
val uriResult: Try<URL> = parseUrl("http://someurl") val getResult: Try<String> = getFromARemoteUrl(uriResult.get()) assertThat(getResult.get()).isEqualTo("a result")which means that our "Try" type should have a "get()" method to retrieve the result if successful and can be implemented like this:
sealed class Try<out T> { ... abstract fun get(): T class Success<T>(private val result: T) : Try<T>() { ... override fun get(): T = result } class Failure<T>(private val throwable: Throwable) : Try<T>() { ... override fun get(): T = throw throwable } }The Success path simply returns the result and the Failure path propagates the wrapped exception.
map Operation
Let's take it a small step forward. Given a url say you want to return the host of the url
val uriResult: Try<URL> = parseUrl("http://myhost") assertThat(uriResult.get().host).isEqualTo("myhost")While this works, the problem with the approach is that the "get()" call for an invalid url would result in an exception if the url is not valid to start with, so a better approach is to retrieve the host name only if the url is valid. Traditionally this is done using a "map" operator and a usage looks like this:
val urlResult: Try<URL> = parseUrl("http://myhost") val hostResult: Try<String> = urlResult.map { url -> url.host } assertThat(hostResult).isEqualTo(Try.success("myhost"))So let's add in a "map" operator to the "Try" type:
sealed class Try<out T> { ... abstract fun <R> map(block: (T) -> R): Try<R> abstract fun get(): T data class Success<T>(private val result: T) : Try<T>() { ... override fun <R> map(block: (T) -> R): Try<R> { return of { block(result) } } } data class Failure<T>(private val throwable: Throwable) : Try<T>() { ... override fun <R> map(block: (T) -> R): Try<R> { return this as Failure<R> } } }and it behaves as expected.
flatMap Operation
Along the lines of "map" operation, now lets get back to the original scenario of validating the url and then attempting to get the content. Now the call to get content can also fail, so you would want that to be wrapped with a Try type also.
val urlResult: Try<URL> = parseUrl("http://someurl") val getResult: Try<String> = getFromARemoteUrl(urlResult.get())
val urlResult: Try<URL> = parseUrl("http://someurl") val getResult: Try<Try<String>> = urlResult.map { url -> getFromARemoteUrl(url) }
A test using it would look like this:
val urlResult: Try<URL> = parseUrl("http://someurl") val getResult: Try<String> = urlResult.flatMap { url -> getFromARemoteUrl(url) } assertThat(getResult).isEqualTo(Try.success("a result"))So how can "flatMap" be implemented, with a fairly simple code that looks like this:
sealed class Try<out T> { ... abstract fun <R> flatMap(tryBlock: (T) -> Try<R>): Try<R> data class Success<T>(private val result: T) : Try<T>() { ... override fun <R> flatMap(tryBlock: (T) -> Try<R>): Try<R> { return try { tryBlock(result) } catch (e: Throwable) { failure(e) } } } data class Failure<T>(private val throwable: Throwable) : Try<T>() { ... override fun <R> flatMap(tryBlock: (T) -> Try<R>): Try<R> { return this as Failure<R> } } }One more small feature, given that Try type has two subtypes is to deconstruct the contents when required:
val urlResult: Try<URL> = parseUrl("http://someurl") val getResult: Try<String> = urlResult.flatMap { url -> getFromARemoteUrl(url) } when (getResult) { is Try.Success -> { val (s) = getResult println("Got a clean result: $s") } is Try.Failure -> { val (e) = getResult println("An exception: $e") } }
This assumes that the user knows the subtypes which may be an okay assumption to make for this type.
Conclusion
A type like "Try" is incredibly useful in capturing a result cleanly or with exception and provides a neat alternative to using a normal try..catch block. Here I showed a way to write such a type from scratch, however this may be an overkill, a better way to get such a type is to simply use an excellent library like vavr which has the Try type already built in. I feel it is instructive to create such a type from scratch though.
Here is the code in my github repository - https://github.com/bijukunjummen/fp-experiment-kotlin/blob/master/src/main/kotlin/sample/adt/Try.kt
No comments:
Post a Comment