dave08
07/08/2024, 11:48 AMfun <T : Any> QueryResult.asListOfIds(factory: (Int) -> T, col: String = "id"): List<T> =
rows.mapNotNull { it.getInt(col)?.let { id -> factory(id) } }
Rob Elliot
07/08/2024, 12:44 PMT::class
passed into asListOfIds
, but I can't see how you can use T::class
without doing reflection, unless you do this:
inline fun <reified I : Id> Int.toId(): I = when (I::class) {
UserId::class -> UserId(this) as I
else -> throw IllegalStateException("this should be impossible")
}
which is ugly as sin.dave08
07/08/2024, 12:47 PMdave08
07/08/2024, 12:48 PMRob Elliot
07/08/2024, 12:48 PMinterface IdFactory<T : Any> {
fun build(value: Int): T
}
inline fun <I, reified F : IdFactory<I>> QueryResult.asListOfIds(col: String = "id"): List<I> =
rows.mapNotNull { it.getInt(col)?.toId<I, F>() }
inline fun <I, reified T : IdFactory<I>> Int.toId(): I = T::class.objectInstance!!.build(this)
but the compiler can't enforce the contract that subtypes of IdFactory
must be objects, and passing both the generic types around is tedious.Rob Elliot
07/08/2024, 12:53 PMinterface IdFactory<I> {
fun build(value: Int): I
}
data class UserId(val value: Int) {
companion object : IdFactory<UserId> {
override fun build(value: Int): UserId = UserId(value)
}
}
inline fun <reified I> QueryResult.asListOfIds(col: String = "id"): List<I> =
rows.mapNotNull { it.getInt(col)?.toId<I>() }
inline fun <reified I> Int.toId(): I = (I::class.companionObjectInstance as IdFactory<I>).build(this)
This time the compiler can't enforce the contract that any Id class must have a companion object that implements IdFactory<Self>
.
Be fascinated to know if there's a typesafe way of doing this.Rob Elliot
07/08/2024, 12:54 PMInt
.dave08
07/08/2024, 12:56 PMdave08
07/08/2024, 12:58 PMwhen
in the utils moduledave08
07/08/2024, 1:21 PMRob Elliot
07/08/2024, 1:24 PMWout Werkman
07/08/2024, 2:03 PMinterface InstantiatableFromId {
abstract companion object {
abstract fun instantiate(id: Int): Self
}
}
fun <refied T: InstantiatableFromId> QueryResult.asListOfIds(col: String = "id"): List<T> =
rows.mapNotNull { it.getInt(col)?.let { id -> T.instantiate(id) } }
There's another feature hidden here, and this has a couple of issues that are out of scope.
Another alternative could be type classes, but those are also not available in Kotlin as a language feature.
So the only thing left is meta programming (in Kotlin that's compiler plugin or reflection), or the factory pattern. And what you are showing is practically the factory pattern.
And since reflection is generally not recommended, your solution is what I would consider the most idiomatic approach to this exact problem.dave08
07/08/2024, 2:08 PMWout Werkman
07/08/2024, 2:12 PMdave08
07/08/2024, 2:14 PMdave08
07/08/2024, 2:16 PMWout Werkman
07/08/2024, 2:31 PMSelf
type.
I can tell you that all three of these features are not on the roadmap as we speak. Abstract static members and Self types have quite some use cases though, so I could imagine a somewhat near future where they will be considered (I don't work in the team that makes these decisions, I'm just basing it on how much a feature contributes, how hard it is to comprehend, and how hard it is to technically implement).
Type classes would in theory make the following three features redundant:
• Self type
• Abstract static members
• Extended conformance
However, making sure that people don't abuse them as well as making them performant and intuitive for Kotlin developers is an extremely hard task.Wout Werkman
07/08/2024, 2:35 PMinterface FromIdInstantator<T> {
fun instantiate(id: Int): T
}
fun <T> QueryResult.asListOfIds(instantiator: FromIdInstantiator<T>, col: String = "id"): List<T> =
rows.mapNotNull { it.getInt(col)?.let { id -> instantiator.instantiate(id) } }
Wout Werkman
07/08/2024, 2:36 PMWout Werkman
07/08/2024, 2:54 PMtrait InstantiatableFromId[T]:
extension (id: Int) def instantiate: T
extension (qr: QueryResult)
def asListOfIds[T: InstantiableFromId](col: String = "id"): List[T] =
qr.rows.collect {
case row if row.getInt(col).isDefined => summon[InstantiableFromId[T]].instantiate(row.getInt(col).get)
}
given InstantiableFromId[User] with {
def instantiate(id: Int): User = User(id)
}
def foo(users: QueryResult):
users.asListOfIds[User]()
This actually pretty much creates the same functions, asListOfIds
still takes 3 parameters, (QueryResult
, InstantiableFromId
, and col
). But Scala will automatically pass the right InstantiableFromId
parameter during compile time based on which T
you pass.
So let's imagine this would exist in Kotlin, then you could imagine it like this:
trait interface InstantiableFromId<T> {
fun instantiate(id: Int): T
}
givenContext(instantiator: InstantiableFromId<T>)
fun <T> QueryResult.asListOfIds(col: String = "id"): List<T> =
rows.mapNotNull { it.getInt(col)?.let { id -> instantiator.instantiate(id) } }
given object: InstantiableFromId<T> {
fun instantiate(id: Int): User = User(id)
}
fun foo(result: QueryResult) {
result.asListOfIds<User>() // Compiler knows to pick the right given object
}
Now you can probably imagine that it's quite hard to make this intuitive to users :)dave08
07/08/2024, 2:57 PMyou can actually already use type classes in Kotlin
?dave08
07/08/2024, 2:59 PMWout Werkman
07/08/2024, 2:59 PMinterface FromIdInstantator<T> {
fun instantiate(id: Int): T
}
Is already a type class. But generally with "supporting type classes", people mean that they are automatically picked by the compiler instead of manually passed as argument.dave08
07/08/2024, 3:00 PMgiven object: InstantiableFromId<T> {
fun instantiate(id: Int): User = User(id)
}
would need to be implemented for each type needed...Rob Elliot
07/08/2024, 3:01 PMRob Elliot
07/08/2024, 3:01 PMdave08
07/08/2024, 3:02 PMinterface InstantiatableFromId {
abstract companion object {
abstract fun instantiate(id: Int): Self
}
}
is straight in the value class... and required (where one may forget to implement the proper given object
...)Wout Werkman
07/08/2024, 3:03 PMdave08
07/08/2024, 3:09 PMdave08
07/08/2024, 3:09 PMWout Werkman
07/08/2024, 3:09 PMWout Werkman
07/08/2024, 3:10 PMdave08
07/08/2024, 3:15 PM