Is there any way to avoid having to provide that f...
# getting-started
d
Is there any way to avoid having to provide that factory method (apart from reflection)? I'm trying to wrap value class ids returned by a db query, and it's still a bit verbose to have to do`result.asListOfIds(::FooId)`
Copy code
fun <T : Any> QueryResult.asListOfIds(factory: (Int) -> T, col: String = "id"): List<T> =
    rows.mapNotNull { it.getInt(col)?.let { id -> factory(id) } }
r
Interesting problem... obviously you can use a reified type to get
T::class
passed into
asListOfIds
, but I can't see how you can use
T::class
without doing reflection, unless you do this:
Copy code
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.
d
Yeah, that might be an idea, but like you said... really ugly 🤷🏼‍♂️.
And you need to put all those value classes there, even unrelated ones.. when splitting modules by feature, that might not be so nice.
r
With reflection:
Copy code
interface 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.
🤔 1
Or another reflection version:
Copy code
interface 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.
That last one's not really any different to just assuming there's a constructor that takes an
Int
.
d
But the last one does have only one type parameter... so it's maybe better in that sense...? Only like you said, not really type-safe.
I guess the reflection solution at least allows to use the function across feature modules and without having all those value classes in one humongous
when
in the utils module
Thanks for the suggestions!
r
Was more joining in, I'm not terribly happy with any of them! Be fascinated if someone has a better way of enforcing a contract in the compiler other than your original.
1
w
There is a bunch of different approaches that languages have to solve this. A rather intuitive way would be to have abstract static members like C#, Rust, Swift and other languages have. Kotlin does not have this though. There is a proposal for this KT-49392. If we assume that Kotlin would have this feature as proposed in the named issue, it would look like:
Copy code
interface 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.
d
That feature of abstract static members is amazing! The thing is that in value classes, using them might cause them to be boxed...?
w
Really depends on how this were to be implemented. It can be implemented without boxing.
d
I wonder if it would be anywhere in the near future... it seems there's no plans for it yet... would type classes be in the near future? How could they help in this problem?
I find that the abstract static members could solve a bunch of problems, including Jetbrains Exposed's requiring companion objects that need to be inherited from... currently there's no way to enforce that..
w
First, the hidden feature that my snippet uses is using a
Self
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.
🤔 1
And if you wonder what they are, you can actually already use type classes in Kotlin. So let's apply them to your problem:
Copy code
interface 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) } }
Now you might wonder, but this doesn't make it nicer to use right? So now I will show you how type classes are used in a functional programming language like Scala
Copy code
trait 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:
Copy code
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 :)
d
😮 Yeah, the previous feature was clearer... but you said
you can actually already use type classes in Kotlin
?
It seems like this way of doing things is relying more on helping the compiler's inference system... it helps assure type safety too, but having the other features could result in less boilerplate.
w
Yes you can, that's because technically the following:
Copy code
interface 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.
🤔 1
d
Copy code
given object: InstantiableFromId<T> {
    fun instantiate(id: Int): User = User(id)
}
would need to be implemented for each type needed...
r
I have to say, the Scala version is a good example of the sort of thing I found illegible about Scala code. The moment it went away and just... found parameters without me specifying them.
I much prefer the combination of: Self type Abstract static members Extended conformance in terms of me having a clue what's going on.
2
d
Whereas:
Copy code
interface 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
...)
w
Yes, but sometimes it's also a problem that it's straight in the value class concrete type* :). You can not make it work for third party types. In that case you need extended conformance
d
True, but at least in this use case, it would be perfect!
And I'm sure lots of others.
w
I agree that Swift's or Rust's approach is probably better (they have all 3 features). But sadly it's actually very hard to make this work in Kotlin
Not so much abstract static conformance or Self types, but extended conformance is hard to model in the ABI of some of Kotlin's backends AFAI can imagine
d
The first two are already pretty good by themselves... Thanks for the explanation and all the references! I guess there's a bunch to look forward to 😁!