From The Joy of Kotlin by Pierre-Yves Saumont

This article discusses retying functions in Kotlin.


Save 37% off The Joy of Kotlin. Just enter fccsaumont into the discount code box at checkout at manning.com.


Impure functions and effects must often be retried if they don’t succeed on the first call. Not succeeding generally means throwing an exception, but retrying a function when an exception’s thrown is tedious and error prone. Imagine that you’re reading a value from some device that might throw an IOException if the device isn’t ready. You might want to retry three times, with a delay of 100 ms between each retry. The imperative solution is something like:

  
 fun get(path: String): String =   
         Random().nextInt(10).let {
             when {
                 it < 8 -> throw IOException("Error accessing file $path")
                 else -> "content of $path"
             }
         }
  
 var retries = 0
 var result: String? = null
 (0 .. 3).forEach [email protected] {            
     try {
         result = get("/my/path")
         [email protected]                 
     } catch(e: IOException) {
         if (retries < 3) {
             Thread.sleep(100)     
             retries += 1
         } else {
             throw e               
         }
     }
 }
 println(result)
  

  The get function simulates a function that will throw an exception 80% of the time.

  The [email protected] label is used to indicate where you want to return from inside the function used as the parameter of the forEach function,…

  and it is triggered when the call to get succeeds, as indicated by [email protected].

  If get throws an exception, a retry is made after waiting for 100 ms if retries is smaller than 3.

  If an exception was thrown and retries is not smaller than 3, the exception is rethrown.

This code is bad for several reasons:

  • You’re forced to use var references.
  • You have to use a nullable type for the result
  • It’s not reusable, although the concept of retrying is something which is used often.

What you need is a retry function, taking as its parameters

  1. the function to retry,
  2. a maximum number of retries, and
  3. a delay value between retries.

This function shouldn’t rethrow any exceptions. Instead, it should return a Result. Here is its signature:

  
 fun retry(f: (A) -> B, times: Int, delay: Long = 10): (A) -> Result
  

Using this function, you can write:

  
 val functionWithRetry = retry(::get, 10, 100) functionWithRetry("/my/file.txt")
     .forEach({ println(it) }, { println(it.message) })
  

You can get this result in many different ways. One way is to use a fold with short circuiting, folding the range zero to <max number of retries>, but escaping as soon as one call to the get function succeeds. You can do this easily by using a label and the standard Kotlin fold function on a Kotlin range, such as:

  
 fun <A, B> retry(f: (A) -> B, times: Int, delay: Long = 10) = [email protected] { a: A ->
     (0 .. times).fold("Not executed") { _, n ->
         try {
             print("Try $n: ")
             [email protected] "Success $n: ${f(a)}"
         } catch (e: Exception) {
             Thread.sleep(delay)
             "${e.message}"
         }
     }
 }
  

On the other hand, this won’t work with a range function. It seems to be due to a bug in Kotlin (issue KT-24055: “Incorrect use of label with local return cause internal exception in the compiler”), and in any case, it won’t compile. A way to solve the problem is to use explicit corecursion. As usual, this implies defining a helper local function:

  
 fun <A, B> retry(f: (A) -> B, times: Int, delay: Long = 10): (A) -> Result<B> {
     fun retry(a: A, result: Result<B>, e: Result<B>, tms: Int): Result<B> =
             result.orElse {
                 when (tms) {
                     0 -> e
                     else -> {
                         Thread.sleep(delay)
                         println("retry ${times - tms}") // <- log the number of retries
                         retry(a, Result.of { f(a) }, result, tms - 1)
                     }
                 }
             }
     return { a -> retry(a, Result.of { f(a) }, Result(), times - 1)}
 }
  

This implementation uses a local function that calls itself recursively with a decremented number of retries until either this number is zero or the call to function f succeeds. Note that you can’t take advantage of the tailrec keyword because Kotlin doesn’t see this function as recursive. This isn’t a problem, because the number of retries will be low. Note the println instruction, which is here only to allow you to see what happens.

The local function is initially called with Result.of { f(a) } as its parameter, which is somewhat unusual. Generally, you call the local function with the same parameters as the main function, plus additional ones. The use case is a bit special because you don’t want an initial delay.

With this function, you can transform any function into a function with automatic retry. Note that you may also use this function with pure effects (returning Unit), such as in the following example:

  
 fun show(message: String) =
     Random().nextInt(10).let {
         when {
             it < 8 -> throw IllegalStateException("Failure !!!")
             else -> println(message)
         }
     }
  
 fun main(args: Array<String>) {
     retry(::show, 10, 20)("Hello, World!")
                   .forEach(onFailure = { println(it.message) })
 }
  

And that’s all for this article. Hopefully you found it informative!


If you want to learn more about the book, check it out on our liveBook reader here and see this slide deck.