Is it possible in Kotlin get function argument fro...
# announcements
i
Is it possible in Kotlin get function argument from function instance? For example i want kind of
retry
function
Copy code
fun <T, R> ((T, R) -> Unit).retry() {
        this(argument1, argument2)
    }
My real case a bit more complicated, here just extraction for clear my question.
Or maybe i can just invoke it without passing arguments?
I understood that it is just an interface under the hood, like an
Copy code
interface Function<T, R> {
    fun invoke(arg: T, arg1: R)
}
but maybe there is any mechanism that i don’t know?
l
You can return a wrapper function from retry ? Or is it that you explicitly want to intercept the arguments of the original functions without changing their consumers ?
i
I want to write mechanism that can retry function without knowing about it’s purpose to get rid code copy-pasting and repeating. So yes, i want to intercept arguments or just call original function again (maybe even without using its arguments)
j
Copy code
inline fun <T> retry(f: () -> T) = TODO()

retry { foo(bar) }
Notice how the
bar
argument is captured, but your retry function doesn't know about it.
i
It is not what am i want. It is how it wrote for now. But this construction look the same in all call places and i want to extract it in function. i.e. i want
retry
to be called from single place. I planned to do it with few extensions
Copy code
fun (() -> Unit).retry() {
        this()
    }

    fun <T> ((T) -> Unit).retry() {
        this(argument1, argument2)
    }

    fun <T, R> ((T, R) -> Unit).retry() {
        this(argument1, argument2)
    }
but stucking in arguments problem
c
If you want to know the arguments:
Copy code
fun <I1, I2, O> ((I1, I2) -> Result<O>).retry(arg1: I1, arg2: I2): O {
  var result: Result<O>
  do {
    result = this(arg1, arg2)
  } while (result.isFailure)
  return result.getOrNull() ?: error("Impossible")
}
Copy code
// Usage
fun plus(a: Int, b: Int): Result<Int> = Success(a + b)
::plus.retry(4, 5)
However with this solution, you will need to write one version per arity (number of parameters). If you don't want to write one version per arity, then @Johannes Koenen's solution is the best one:
Copy code
fun <O> retry(block: () -> Result<O>): O {
  var result: Result<O>
  do {
    result = block()
  } while(result.isFailure)
  return result
}
Copy code
// Usage
fun plus(a: Int, b: Int): Result<Int> = Success(a + b)
retry { plus(3, 5) }
i
looks like my objective is far from to be clear. Or i don’t understand that you say “Hey dude, thing what you want is impossible”. Okay. Maybe later
y
Okay, let's discuss this slowly. what exact call site syntax do you want? Like do you want something like this:
Copy code
val result = myFun()
if(result != SOME_EXPECTED_VALUE){
    result.retry()
}
i
Exactly (a bit different, but yes). I want to my function be able to retry itself without care about what exactly need to be retried. Speaking clearly - usecase - creating unieversal mechanism for ui. If server not available it throws TimeoutException (for example), but after time, a few or much, server can became available. Here i am.
y
Okay, how about something like this:
Copy code
fun main(){
    repeat(3){
        var result = fetchSomeDataRetryable("test")
        println(result.value)
        while(result.value != 42){
            result = result.retry()
            println(result.value)
        }
    }
    
    println(submitJsonToServer(4, "test").value)
    val failedSubmission = submitJsonToServer(6, "high id")
    println(failedSubmission.value)
    if(!failedSubmission.value.endsWith("200 OK")){
        maxIdAllowed = 6
        val successfulSubmission = failedSubmission.retry()
        println(successfulSubmission.value)
    }
    
    runBlocking {
        var newPostsResult = pingServerForNewPosts("hunter123", Duration.milliseconds(20), 30) // 1 in 30 chance of there being new posts
        println(newPostsResult.value)
        while(newPostsResult.value.isFailure) newPostsResult = newPostsResult.retry().also { println(it.value) }
    }
}
Would that look like the call site syntax that you want? the implementation is this: (playground)
Copy code
data class RetryableResult<R>(val value: R, private val _retry: () -> R){
    constructor(retry: () -> R): this(retry(), retry)
    fun retry(): RetryableResult<R> = copy(value = _retry())
}

fun <R> retryableResult(retry: () -> R, unit: Unit = Unit) = RetryableResult(retry)

data class SuspendRetryableResult<R>(val value: R, private val _retry: suspend () -> R){
    companion object {
        operator suspend fun <R> invoke(retry: suspend() -> R) = SuspendRetryableResult(retry(), retry)
    }
    suspend fun retry(): SuspendRetryableResult<R> = copy(value = _retry())
}
suspend fun <R> retryableResult(retry: suspend () -> R) = SuspendRetryableResult(retry)

fun interface RetryableFunction1<P1, R> {
    fun realInvoke(p1: P1): R
    operator fun invoke(p1: P1): RetryableResult<R> {
        return RetryableResult { realInvoke(p1) }
    }
}
val <P1, R> ((P1) -> R).retryable: RetryableFunction1<P1, R> get() = RetryableFunction1(this)
@OverloadResolutionByLambdaReturnType
fun <P1, R> retryable(function: (P1) -> R) = function.retryable

fun interface RetryableFunction2<P1, P2, R> {
    fun realInvoke(p1: P1, p2: P2): R
    operator fun invoke(p1: P1, p2: P2): RetryableResult<R> {
        return RetryableResult { realInvoke(p1, p2) }
    }
}
val <P1, P2, R> ((P1, P2) -> R).retryable: RetryableFunction2<P1, P2, R> get() = RetryableFunction2(this)
fun <P1, P2, R> retryable(function: (P1, P2) -> R) = function.retryable


fun fetchSomeData(url: String): Int{
    return listOf(42, 64).random()
}

val fetchSomeDataRetryable = ::fetchSomeData.retryable

var maxIdAllowed = 5
val submitJsonToServer = retryable { id: Int, newJson: String ->
    "$id: $newJson returned ${if(id <= maxIdAllowed) "200 OK" else "404 NOT FOUND" }"
}

suspend fun pingServerForNewPosts(userId: String, delay: Duration, randomChanceOneIn: Int) = retryableResult {
    delay(delay.inWholeMilliseconds)
    println(userId)
    runCatching { 
        if(Random.nextInt(1, randomChanceOneIn) == 1) 
        	"New posts found" 
        else 
        	TODO("No New posts")
    }
}
i
Wow. I’ll check it. Thanks in advance
y
I included 3 examples of how you can define a retryable function. The first one
fetchSomeData
is how it should be defined if either you don't have control over the original function or if you want to expose both the function itself and the retryable version. The next one is
submitJsonToServer
which exposes it as a
RetryableFunction2
. The issue with this example is that it messes up named parameters and default parameters. The last one, which in my opinion is the best if you have control over the function, simply takes in the parameters normally and returns a
(Suspend)RetryableResult
object which can be `retry()`ed. It preserves default and named parameters, and just IMO it looks the cleanest. Check the playground link for the whole code and to be able to edit it and tinker around with it yourself. Keep in mind, btw, that this implementation has certain performance overheads because it creates a lambda or 2 for each function call, and so it should really be used when you already have a performance-heavy function (like a network call or database fetch) since then the cost of the lambda becomes nothing relative to that operation.
I took a few liberties with the syntax since you hadn't specified an insanely-specific wanted syntax, and so please tell me if you'd like any tweaks to it to make it look better or more "DSL-y"
i
You’ve made a great work. I doubt that there are much people who can give so cool and extensive answer. Thank you, I’ll dive deeper a bit later, there are things that i must do first. But first dive let me to feel kotlin might ) Even thought i use it since 2017 i don’t even know about constructions like
Copy code
RetryableFunction2(this)
To be clear i even must to play with it, because i don’t understand yet how it works )
Thank you again, i sure i’ll find what i am looking for in your answer