Home > OS >  Kotlin - Pass a function and its arguments to a reusable handler function
Kotlin - Pass a function and its arguments to a reusable handler function

Time:09-14

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.

CodePudding user response:

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")
}
  • Related