Search code examples
kotlingenericslambdadelegationside-effects

Add a side-effect to a function in a generic way


How can I write a Kotlin generic function that takes a function as an argument and adds a side-effect to it? For instance,

fun something(one: Int, two: String): String { return "${one}, ${two}" }
fun somethingElse(arg: Array<String>): String { return "${arg}" }

val w1 = wrapped(::something)
w1(42, "hello")

val w2 = wrapped(::somethingElse)
w2(arrayOf("ichi", "ni"))

The following works for functions that take only a single parameter:

fun <A, R> wrapped(theFun: (a: A) -> R): (a: A) -> R {
    return { a: A ->
        theFun(a).also { println("wrapped: result is $it") }
    }
}

To make this work with an arbitrary number of arguments, I'd need some construct that gives me the type of the argument list. Unfortunately, the Function generic can't be used since it takes only one parameter. The following does not compile:

fun <A, R> wrapped(theFun: Function<A, R>): Function<A, R> {
    return { args: A ->
        theFun(*args).also { println("wrapped: result is ${it}") }
    }
}

Or maybe I could use varargs? Does not seem to work with lambdas. Or Kotlin reflection?


Solution

  • Solution using reflection:

    class KFunctionWithSideEffect<R>(private val f: KFunction<R>, private val sideEffect: (R) -> Unit) : KFunction<R> by f {
        override fun call(vararg args: Any?) = f.call(*args).also { sideEffect(it) }
    
        override fun callBy(args: Map<KParameter, Any?>) = f.callBy(args).also { sideEffect(it) }
    }
    
    fun <R> wrapped(theFun: KFunction<R>, sideEffect: (R) -> Unit = { str -> println("wrapped: result is $str") }) =
        KFunctionWithSideEffect(theFun, sideEffect)
    

    Usage:

    val w1 = wrapped(::something)
    w1.call(42, "hello")
    
    val w2 = wrapped(::somethingElse)
    w2.call(arrayOf("ichi", "ni"))