Home > Software design >  Make Kotlin function output (T1,...,Tn) when the function has inputs or generics <T1,...,Tn>
Make Kotlin function output (T1,...,Tn) when the function has inputs or generics <T1,...,Tn>

Time:08-13

I'm passing in O(Blah1, Blah1, Blah2) into a submit function and want to receive O3<Blah1, Blah1, Blah2> as output, but instead, I'm getting O3<Blah1.Companion, Blah2.Companion, Blah3.Companion>.

How would I get the compiler to automatically handle O<Blah1.Companion...Blahn.Companion> input to have an output type of O<Blah1...Blahn> ?

Functions that convers T(1..n) to Tn:

fun <T1 : R, R> T(value1: T1) =
    T1<T1, R>(value1)

fun <T1 : R, T2 : R, R> T(value1: T1, value2: T2) =
    T2<T1, T2, R>(value1, value2)

fun <T1 : R, T2 : R, T3 : R, R> T(value1: T1, value2: T2, value3: T3) =
    T3<T1, T2, T3, R>(value1, value2, value3)

T1..Tn classes:

interface T

class T1<out T1 : R, out R>(
    private val value1: T1,
) : T, Iterable<R> {
    companion object : T
    operator fun component1() = value1
    override fun iterator(): Iterator<R> = listOf(value1).iterator()
}

class T2<out T1 : R, out T2 : R, out R>(
    private val value1: T1,
    private val value2: T2,
) : T, Iterable<R> {
    companion object : T
    operator fun component1() = value1
    operator fun component2() = value2
    override fun iterator(): Iterator<R> = listOf(value1, value2).iterator()
}

class T3<out T1 : R, out T2 : R, out T3 : R, out R>(
    private val value1: T1,
    private val value2: T2,
    private val value3: T3,
) : T, Iterable<R> {
    companion object : T
    operator fun component1() = value1
    operator fun component2() = value2
    operator fun component3() = value3
    override fun iterator(): Iterator<R> = listOf(value1, value2, value3).iterator()
}

Input and Output classes BLAH1..BLAHn:

interface Encodable {
    val value: String
}

interface Decodable {
    val code: Int
}

class BLAH1(override val value: String) : Encodable {
    companion object : Decodable {
        override val code: Int = 1
    }
}

class BLAH2(override val value: String) : Encodable {
    companion object : Decodable {
        override val code: Int = 2
    }
}

submit function that is receiving O1..n containing BLAH1.Companion or Blah2.Companion that should return O1..n of types Blah1 and Blah2 without the Companion.

fun <I: Iterable<Encodable>, O: Iterable<Decodable>> submit(i: I, o: O): O {
    i.forEach {
        // do something with input
        println("i: ${it.value}")
    }
    // ...
    // return output
    val results = mutableListOf<Decodable>()
    o.forEach {
        results.add(it)
    }

    return when (results.size) {
        1 -> T1(BLAH1("1:${results[0]}"))
        2 -> T2(BLAH1("1:${results[0]}"), BLAH1("2:${results[1]}"))
        3 -> T3(BLAH1("1:${results[0]}"), BLAH1("2:${results[1]}"), BLAH2("3:${results[2]}"))
        // ...
        // 99 -> T99...
        else -> TODO("TODO")
    } as O // this casting will obviously fail since we're going from BLAH1 to BLAH1.Compaion
}

How do I make submit return O1..n<BLAH1..BLAHn> when o contains O1..n<BLAH1.Companion..BLAHn.Companion> so that the destructuring:

val result =  submit(
    i = T(BLAH1("blah1"), BLAH2("blah2")),
    o = T(BLAH1, BLAH1, BLAH2)
)
val (a, b, c) = result

will actually contain instances of a:BLAH1, b:BLAH1, c:BLAH2 instead of a:BLAH1.Companion, b:BLAH1.Companion, c:BLAH2.Companion ?

EDIT: Here's what I'm trying to do in the broader sense:

I want to be able to destructure a database query's results directly into values

pool { this: Connection ->
    val (a /*:BOOL*/, b /*:INT2*/) = query(
        "SELECT a, b FROM table WHERE c = ? AND d = ?",
        I(INT4(42), VARCHAR("123"),
        O(BOOL, INT2)
    )
}

actually, what would be nicer is if I can do:

val (a /*:BOOL*/, b /*:INT2*/) = 
    query<BOOL, INT2>("SELECT ...", INT4(42), VARCHAR("123")

, but I could not figure out how to convert

query<A>(...) to query(...): O1<A>
query<A,B>(...) to query(...): O2<A,B>
...
query<A,B,...,Z>(...) to query(...): O26<A,B,...,Z>

So I had to put the output params in the args which meant I had to give up on using varargs for the input params leading to an api that looks like submit(sql: String, i: I, o: O) instead.

Note that there can be any number of input params and I'm expecting at most 26 output values.

EDIT2: More examples

val (a, b) = query<INT4, INT4>("SELECT 1, 2")
// should output a = INT4(1), b = INT4(2)

val (a, b, c) = query<INT4, VARCHAR, INT4>(
    "SELECT col1, col2, col3 FROM table WHERE col4 = ?", 
    BOOL(true)
)
// should generate a query
//     SELECT col1, col2, col3 FROM table WHERE col4 = true
// and output 
//     a = INT4(value of col1), 
//     b = VARCHAR(value of col2)
//     c = INT4(value of col3)

BOOL, INT4, INT2, VARCHAR are all just value classes each containing a ByteArray with the raw response from the database allowing me to do a.value or c.value which will each return an Int and b.value which will return a String, but this part is kinda irrelevant to the question, INT4 and VARCHAR could just as well have been Int and String.

CodePudding user response:

After digging through source code of other libraries and some help from the Arrow community, I have something that does exactly what I wanted.

I can now call a function with some generics and get those params out as destructured values in a typeSafe way:

(a:A,b:B) = doSomething<A,B>()
(a,b,c,d,...,z) = doSomething<A,B,C,D,...,Z>()

The magic is done with this TypeMarker sealed class

sealed interface TypeMarker<
    out T1 : Encodable,
    out T2 : Encodable,
    out T3 : Encodable,
    out T4 : Encodable,
    out T5 : Encodable,
    > {
    object IMPL : TypeMarker<Nothing, Nothing, Nothing, Nothing, Nothing>
}

and then the TypeMarker is used at the end of a varargs

fun <reified Z1 : Encodable> doSomething(
    vararg args: Any,
    typeMarker: TypeMarker<Z1, Nothing, Nothing, Nothing, Nothing> = TypeMarker.IMPL,
): Z1 {
    ...
}

fun <Z1 : Encodable, Z2 : Encodable> doSomething(
    vararg args: Any,
    typeMarker: TypeMarker<Z1, Z2, Nothing, Nothing, Nothing> = TypeMarker.IMPL,
): Pair<Z1, Z2> {
    ...
}

... rinse and repeat for Triple, Tuple4, Tuple5, etc (see the T1-T5 in the original question for an example on how to implement Tuples) and if you want to go beyond 5 params, just increase the number of params in the TypeMarker.

This now allows me to set a generic of INT4 and get one result of INT4 when destructuring or have 99 generics and be able to get 99 params out when destructuring, etc

val a: INT4 = doSomething<INT4>(
    INT4(123),
    INT8(122312)
)

val (b: INT4, c: INT8) = doSomething<INT4, INT8>(
    INT4(123),
    INT8(12)
)

val (d,e,f,g,h) = doSomething<INT4, INT8, INT4, INT8, INT4>(
    INT4(123),
    INT8(12)
)

When dealing with lots of params, you might get annoyed with all the Nothings you have to add. You can use this syntax to get around that

sealed class TypeMarker5<out T1, out T2, out T3, out T4, out T5> {
    @PublishedApi internal object IMPL: TW5<Nothing, Nothing, Nothing, Nothing, Nothing>()
}
typealias TypeMarker4<T1, T2, T3, T4> = TypeMarker5<T1, T2, T3, T4, Nothing>
typealias TypeMarker3<T1, T2, T3> = TypeMarker4<T1, T2, T3, Nothing>
typealias TypeMarker2<T1, T2> = TypeMarker3<T1, T2, Nothing>
typealias TypeMarker1<T1> = TypeMarker2<T1, Nothing>
  • Related