EDIT. Here is some TL;DR
This does not work:
interface I {
val value: Any
}
fun impl(a: Any) = object: I {
override val value = a
}
class A {
companion object: I by impl(A.Companion) // because A.Companion is null here
}
Technically impl
invoked within the constructor of the companion object. That is, at the moment of impl
invocation, A.Companion
reference is not initialized.
It would be nice to have a direct reference to this@Companion
when using delegates.
The original post
I guess I’m at the point of maximum abuse of companion objects and delegates
I’m writing a data-driven app which relies heavily on JSON deserialization. It would not be a problem, as I could use kotlinx.serialization or any other library, but I need lots of control over serialization process and fail-full error handling (not sure if it’s correct term, I just need to collect as many as possible errors and show them all to user)
The real code relies heavily on arrow-kt lib and does all error handling via Validated and Either monads. But for simplicity I’ll just use exceptions.
So, here is what I’m trying to do. Name
’s companion object implements FromString
and FromJson
interfaces.
In order to reuse code, I wrote FromJson
implementation that delegates conversion to FromString
.
class Name private constructor(
private val value: String
) {
companion object :
FromString<Name> by LengthLimitedFromString(::Name, maxLength = 50),
FromJson<Name> by FromJsonViaFromString(Name) // exception here, Name.Companion is not yet initialized
}
interface FromString<T> {
fun fromString(v: String): T
}
interface FromJson<T> {
fun fromJson(v: JsonValue): T
}
class LengthLimitedFromString<T>(
val makeObject: (String) -> T,
val maxLength: Int,
) : FromString<T> {
init { require(maxLength > 0) { "Max length must be a positive number"} }
override fun fromString(v: String) =
if (v.length <= maxLength) makeObject(v)
else throw Exception("The value is too long")
}
class FromJsonViaFromString<T>(val fromString: FromString<T>) : FromJson<T> {
override fun fromJson(v: JsonValue) = fromString.fromString(v.toStringOrThrow())
}
The problem is that I cannot initialize FromJsonViaFromString (line 6). It throws ExceptionInInitializerError, and for a good reason: Name.Companion has not been assigned yet, since the object is in construction process.
I know, there are workarounds, there is maybe a design flaw in my app. But it’s so cool that I can just pass Name
to other functions like jsonValue.deserialize(Name)
And what would be really, really helpful here is to Specify something like
FromJson<Name> by FromJsonViaFromString(this@Companion)
// or even this, though it is not clear what's the context of the `this` reference
FromJson<Name> by FromJsonViaFromString(this)
So that compiler generates code like this (just some java instead of bytecode):
public static final class Companion implements FromString, FromJson {
private final LengthLimitedFromString delegate_0;
private final FromJsonViaFromString delegate_1;
private Companion(Function factory) {
this.delegate_0 = new LengthLimitedFromString(factory, 50);
this.delegate_1 = new FromJsonViaFromString(Companion.this);
// instead of this:
this.delegate_1 = new FromJsonViaFromString(Name.Companion);
}
// ... implementations
}
On the other hand, if it does not make sense, maybe then it makes sense to forbid using companions in such way?
Does any of these make any sense?