Suggestion for this@Companion reference in companion's delegates

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 :smiley:
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) :smiley:
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?

You can do this with a little bit of awkwardness at the declaration-site of the companion:

inline fun impl(crossinline provider: () -> Any) = object: I {
	override val value get() = provider()
}

class A {
	companion object: I by impl({ A.Companion }) //Can't use impl{ A.Companion } because the compiler thinks that you're declaring the body of the companion object
}

This restriction of not being able to access this@Companion for delegation matches the same restriction of not being able to access this in a normal class’s delegation.

class B : List<Any> by listOf(this) // Try running this example

This could be considered a bug that the compiler doesn’t catch the usage of A.Companion as an error until runtime. WIth the normal this usage it’s a compiler error.

1 Like

Yep. For these cases, you can probably also just use a lambda that returns this

And the restriction here is a dumb mistake, that leads to recursive delegation of List?

That’s what I did. Thanks.