https://kotlinlang.org logo
#getting-started
Title
# getting-started
k

krzysztof

03/24/2023, 10:26 PM
Hey, I’m looking for a way to wrap a method into a higher-order function that runs some checks, before running that method. 🧵
I have a few methods that I pass down to other method as callbacks:
Copy code
giantMethod(
 onCallbackOne: inst::method1,
 onCallbackTwo: inst::method2
)
both
method1
and
method2
have different arity and/or parameter types
I’d like to run some checks, before
method*
is called, but still be able to pass it down to
giantMethod
like so:
Copy code
giantMethod(
 onCallbackOne: check(inst::method1),
 onCallbackTwo: check(inst::method2)
)
Is there a way to achieve this in some nice way?
j

Jimmy Zhou

03/24/2023, 10:35 PM
how do you plan to wrap them? by annotation or by abstract class?
y

Youssef Shoaib [MOD]

03/24/2023, 10:39 PM
I had just implemented something similar. One sec while I adapt it to your needs
j

Jimmy Zhou

03/24/2023, 10:41 PM
you can define an abstract class and do something like this
Copy code
open suspend fun <T : Any?> giantMethod(
        function: suspend () -> T?
    ): T? {
         // add some other logic
         runCatching { function() }
         ....
         // some other logic
    }
or you can use
@AspectJ
then use joinPoints and annotate your method/class
e

ephemient

03/24/2023, 10:44 PM
in pure Kotlin, you need overloads for different arities
k

krzysztof

03/24/2023, 10:50 PM
Looking for something that resembles JS’ spread operator, such as:
Copy code
giantMethod(
 onCallbackOne: (...args) => check(method1, args))
)
So that once logic in
check
method is completed,
method1
will be called with
args
arguments passed
e

ephemient

03/24/2023, 10:50 PM
how would you write that type in Kotlin?
y

Youssef Shoaib [MOD]

03/24/2023, 10:55 PM
(Playground):
Copy code
import kotlin.reflect.*
typealias Wrapper<R> = (() -> R) -> R

fun <R> checker(): Wrapper<R> = { block ->
    // Run some checks
    require(Unit is Unit)
    println("Checking")
    block()
}

fun giantMethod(getter: () -> Int, setter: (Int) -> Unit) {
    setter(getter() + 1)
}
var myInt = 41
fun main() {
    val setter: (Int) -> Unit = { myInt = it }
    giantMethod(::myInt.wrappedWith(checker()), setter.wrappedWith(checker()))
    println(myInt)
}

// You'll need one wrappedWith function per arity, but that shouldn't be that big of a deal since
// you usually won't have a function bigger than 10-15 parameters.
fun <R> (() -> R).wrappedWith(wrapper: Wrapper<R>): () -> R = {
    wrapper {
        invoke()
    }
}
fun <T1, R> ((T1) -> R).wrappedWith(wrapper: Wrapper<R>): (T1) -> R = { t1 ->
    wrapper {
        invoke(t1)
    }
}
fun <T1, T2, R> ((T1, T2) -> R).wrappedWith(wrapper: Wrapper<R>): (T1, T2) -> R = { t1, t2 ->
    wrapper {
        invoke(t1, t2)
    }
}
fun <T1, T2, T3, R> ((T1, T2, T3) -> R).wrappedWith(wrapper: Wrapper<R>): (T1, T2, T3) -> R = { t1, t2, t3 ->
    wrapper {
        invoke(t1, t2, t3)
    }
}
fun <T1, T2, T3, T4, R> ((T1, T2, T3, T4) -> R).wrappedWith(wrapper: Wrapper<R>): (T1, T2, T3, T4) -> R = { t1, t2, t3, t4 ->
    wrapper {
        invoke(t1, t2, t3, t4)
    }
}
👍 2
It requires a
wrappedWith
method for every arity, but it allows you to have unlimited
checker
varieties, because the checker doesn't need to know anything about the arguments of the function
e

ephemient

03/24/2023, 10:56 PM
yeah, as I mentioned earlier
it'll fail after 22 parameters because the function types aren't different anymore, but it should be enough for almost all practical purposes
k

krzysztof

03/24/2023, 11:02 PM
that’s super useful, thank you 🙏 I guess I expected that
vararg arg: Any?
would cover cases for arbitrary arity, but I expected wrongly
e

ephemient

03/24/2023, 11:03 PM
no, because a vararg function has a different signature than a function taking a fixed number of arguments
👍 1
y

Youssef Shoaib [MOD]

03/24/2023, 11:08 PM
ephemient is right in that it fails after 22, but not because of the compiler type system. In fact, the type system can still differentiate between a 23 vs a 24 function. It fails because it gets compiled down to the same class (
FunctionN
I think). This can be easily solved with
@JvmName
though. Here's an example of it working for 23 vs 24 (Playground):
Copy code
import kotlin.reflect.*
typealias Wrapper<R> = (() -> R) -> R

fun <R> checker(): Wrapper<R> = { block ->
    // Run some checks
    require(Unit is Unit)
    println("Checking")
    block()
}

fun giantMethod(getter: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Int,
                setter: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Unit) {
    setter(getter(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + 1,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
}
var myInt = 41
fun main() {
    // 23 args
    val getter: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Int = 
    { _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ -> 
        myInt 
    }
    // 24 args
    val setter: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> Unit = 
    { it, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _ -> 
        myInt = it 
    }
    giantMethod(getter.wrappedWith(checker()), setter.wrappedWith(checker()))
    println(myInt)
}

// 23
@JvmName("wrappedWith23")
fun <T, R> ((T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T) -> R).wrappedWith(wrapper: Wrapper<R>): (T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T) -> R = 
    { t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23 ->
    wrapper {
        invoke(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23)
    }
}
// 24
@JvmName("wrappedWith24")
fun <T, R> ((T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T) -> R).wrappedWith(wrapper: Wrapper<R>): (T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T) -> R = 
    { t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24 ->
    wrapper {
        invoke(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24)
    }
}
e

ephemient

03/24/2023, 11:09 PM
well, I didn't go into that detail, but that's what I meant 😛 it's documented in https://github.com/Kotlin/KEEP/blob/master/proposals/functional-types-with-big-arity-on-jvm.md
also, suspend functions "use up" one parameter, so if you create
Copy code
suspend fun <T1...T21, R> wrapper(block: suspend (T1...T21) -> R): suspend (T1..T21) -> R
etc. overloads, you run into the issue a little earlier
k

krzysztof

03/24/2023, 11:24 PM
Thank you both 🙏
3 Views