Kotlin - Pass a function and its arguments to a reusable handler function

Viewed 45

My problem comes from an error handler that I need to write. The handler is meant to catch an error, do an action 'A', and then proceed to retry the original function that failed in the first place. The blocker comes when I need to generalise the handler to be able to accept any function definition.

For example:

fun foo(a: Any) {
  // Do something and return null if unsuccessful
}

fun bar(b: Any, c: Any) {
  // Do something else and return false if unsuccessful
}

fun handler(fn: Function???, args: Array[arguments???]) {
  log.warn("Error has been caught at <${functionName}>, doing action A and retrying")
  // Do action A
  log.warn("Retrying <${functionName}>")
  fn(args)
}

fun main() {
  val resultFoo = foo('bar')
  if (resultFoo == null) {
    handler(foo, ['bar'])
  }

  val resultBar = bar('foo', { 'foo': 'bar' })
  if (!resultBar) {
    handler(bar, ['foo', { 'foo': 'bar' }])
  }
}

As you can see, I have two functions foo and bar with different definitions which I would like to be able to call from handler. Otherwise I would need to copy-paste the body inside handler to each individual function and handle the error inside there. I was hoping to be able to generalise this and be able to reuse it.

2 Answers

If you want to avoid reflection, which would provide no compile-time error checking, you should overload your function to have variants with different numbers of variables. Then you can use generics, and you'll get compile-time checking.

There's not a clean way to pass functions along with their names, so I made the name for logging a separate argument in this example.

// zero args handler
inline fun <R> handler(loggedFunctionName: String?, fn: ()->R): R {
    val functionName = "<$loggedFunctionName>" ?: "function"
    log.warn("Error has been caught at $functionName, doing action A and retrying")
    // Do action A
    log.warn("Retrying $functionName")
    return fn()
}

// one arg handler
inline fun <A, R> handler(loggedFunctionName: String?, fn: (A)->R, argA: A): R
    = handler(loggedFunctionName) { fn(argA) }

// two args handler
inline fun <A, B, R> handler(loggedFunctionName: String?, fn: (A, B)->R, argA: A, argB: B): R
    = handler(loggedFunctionName) { fn(argA, argB) }

fun main() {
    val resultFoo = foo("bar")
    if (resultFoo == null) {
        handler("foo", ::foo, "arg")
    }

    val resultBar = bar("arg1", "arg2")
    if (!resultBar) {
        handler("bar", ::bar, "arg1", "arg2")
    }
}

However, it seems you could reduce boilerplate further by incorporating your logical test of the result as another argument:

// zero args handler
inline fun <R> handler(loggedFunctionName: String?, fn: ()->R, verifyResult: (R)->Boolean): R {
    val firstResult = fn()
    if (verifyResult(firstResult)){
        return firstResult
    }
    val functionName = "<$loggedFunctionName>" ?: "function"
    log.warn("Error has been caught at $functionName, doing action A and retrying")
    // Do action A
    log.warn("Retrying $functionName")
    return fn()
}

// one arg handler
inline fun <A, R> handler(loggedFunctionName: String?, fn: (A)->R, verifyResult: (R)->Boolean, argA: A): R
    = handler(loggedFunctionName, { fn(argA) }, verifyResult)

// two args handler
inline fun <A, B, R> handler(loggedFunctionName: String?, fn: (A, B)->R, verifyResult: (R)->Boolean, argA: A, argB: B): R
    = handler(loggedFunctionName, { fn(argA, argB) }, verifyResult)

fun main() {
    handler("foo", ::foo, { it != null }, "arg")
    handler("bar", ::bar, { it }, "arg1", "arg2")
}

First of all you need to know, what exactly you want to generalize.

As I can see from your example, you want to repeat function call with same arguments. So instead all this messing with argument-arrays you can wrap payload (function call with arguments) into lambda and call it twice.

Second question is repeat condition. In foo it's result == null, in bar it's result == false. So condition is different for different functions. You can calculate condition in separate lambda.

So you can write something like this:

fun <T, F: KFunction<*>> F.callAndRepeatOnCondition (
    payload: (F) -> T,
    condition: (T) -> Boolean,
): T {
    var result = payload(this)
    if (condition(result)) {
        println("log: doing something and repeat function ${this.name} ")
        println("some super logic before repeat")
        result = payload(this)
    }
    return result
}

fun foo(a: Int, b: String): String? { return null }
fun bar(c: Int): Boolean { return false }


fun main() {
    val resultFoo = ::foo.callAndRepeatOnCondition({ it(1, "a") }, { it == null })
    val resultBar = ::bar.callAndRepeatOnCondition({ it(2) }, { !it })
}

PS. If you don't need to log function name or can pass it explicitly, here is a little less ugly solution:

fun foo(a: Int, b: String): String? { return null }
fun bar(c: Int): Boolean { return false }

fun <T> (() -> T).repeatIf(condition: (T) -> Boolean): T {
    val result = this()
    if (!condition(result)) return result
    println("some super logic before repeat")
    return this()
}

fun <T> (() -> T).repeatWithLoggingIf(logName: String, condition: (T) -> Boolean): T {
    val result = this()
    if (!condition(result)) return result
    println("log: repeating $logName")
    println("some super logic before repeat")
    return this()
}

fun main() {
    run { // without logName 
        val resultFoo =  { foo(1, "a") }.repeatIf { it == null }
        val resultBar = { bar(2) }.repeatIf { !it }
    }
    run { // with logName
        val resultFoo = { foo(1, "a") }.repeatWithLoggingIf("foo") { it == null }
        val resultBar = { bar(2) }.repeatWithLoggingIf("bar") { !it }
    }
}
Related