jaumard
jaumard

Reputation: 8292

Kotlin generic Out-projected type prohibits the use of

I'm using kind of dynamic form system coming from the backend. To be able to map my form I have a visitor pattern with generics, I have it working in Java but I can't make it to work in Kotlin.

I have this interface:

internal interface FormFieldAccessor<T> {

    fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>

    fun setValueToBuilder(builder: Builder, value: T)

    fun accept(visitor: FormFieldVisitor)

    fun getValue(personalInfo: PersonalInfo): T
}

Then I have my list of accessors like this:

val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
accessors[FIRST_NAME] = object : FormFieldAccessor<String> {
            override fun getValue(personalInfo: PersonalInfo): String {
                return personalInfo.surname
            }

            override fun accept(visitor: FormFieldVisitor) {
                visitor.visitString(this)
            }

            override fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<String> {
                //not relevant
            }

            override fun setValueToBuilder(builder: Builder, value: String) {
                builder.withSurname(value)
            }
        }
//more other accessors with different type like Int or Boolean

And want to use it like this:

accessors[FIRST_NAME]!!.setValueToBuilder(builder, field.value )

But this is not working and give me:

Out-projected type 'FormFieldAccessor<*>' prohibits the use of 'public abstract fun setValueToBuilder(builder: Builder, value: T): Unit defined in FormFieldAccessor'

If you have an idea of what I'm doing wrong would be cool :)

EDIT: here is a smaller gist of the structure I have https://gist.github.com/jaumard/1fd1ccc9db0374cb5d08f047414a6bc8

I don't want to loose the type by using Any, feel frustrated compare to Java as it's really easy to implement. I understand the problem with the star projection now but is there anything else than this to achieve the same as in java ?

Upvotes: 3

Views: 2584

Answers (3)

Evgeniy
Evgeniy

Reputation: 559

In this case you can add a wrapper method that takes Any as a value and checks its type

internal interface FormFieldAccessor<T> {


    fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>

    fun _setValueToBuilder(builder: Builder, value: T)

    fun setValueToBuilder(builder: Builder, value: Any){
        val value  = value as? T ?: return
        _setValueToBuilder(builder, value)
    }

    fun accept(visitor: FormFieldVisitor)

    fun getValue(personalInfo: PersonalInfo): T
}

Upvotes: 0

Alexey Romanov
Alexey Romanov

Reputation: 170795

@s1m0nw1's answer gives you the reason for the problem and the simple fix. However, with your setup there may also be another possibility.

Add

fun setValueFromForm(builder: Builder, fieldDefinition: FormFieldDefinition) { 
    setValueToBuilder(builder, getFormField(fieldDefinition).value)
}

to FormFieldAccessor<T>. Because its signature doesn't involve T, it can be safely called on a FormFieldAccessor<*>.

And if you also store FormFieldDefinitions in a map, you can now call it

accessors[FIRST_NAME]!!.setValueToBuilder(builder, fieldDefinitions[FIRST_NAME]!!)

Further improvements would depend on details of your system.

EDIT:

I'm reasonably certain even without seeing the Java code that it uses raw types (i.e. FormFieldAccessor instead of FormFieldAccessor<Something> or FormFieldAccessor<?>). Something like

Map<String, FormFieldAccessor> accessors = new HashMap<>();
...
accessors.get(FIRST_NAME).setValueToBuilder(builder, field.value);

But this is also unsafe, the compiler just ignores the problems instead of telling you about them. The value of accessors.get(FIRST_NAME) can still actually be e.g. an FormFieldAccessor<Boolean>, in which case setValueToBuilder will fail with a ClassCastException. You can see this by accidentally passing the wrong name to get or storing the wrong accessor in the map: it won't stop anything from compiling.

Instead, better Java code would use Map<String, FormFieldAccessor<?>> and then require a cast after get just as Kotlin code does.

Raw types exist primarily to allow very old pre-Java-5 code to still compile. Kotlin doesn't have this consideration, so it doesn't support raw types and your only option there is to do what you should do in Java.

Upvotes: 0

s1m0nw1
s1m0nw1

Reputation: 81989

Using star-projection indicates that you know nothing about the actual type, as the documentation tells:

Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.

[...]

For Foo<out T : TUpper>, where T is a covariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper>. It means that when the T is unknown you can safely read values of TUpper from Foo<*>.

What you can do is casting to the appropriate type:

(accessors[FIRST_NAME] as FormFieldAccessor<String>).setValueToBuilder(builder, field.value)

Yet, these types of casts are error-prone and a safer way would be the following;

object FormFieldProvider {
    private val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
    fun <T : Any> addAccessor(key: String, fieldValidator: FormFieldAccessor<T>) {
        accessors[key] = fieldValidator
    }

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Any> get(key: String): FormFieldAccessor<T> =
            accessors[key] as? FormFieldAccessor<T>
                    ?: throw IllegalArgumentException(
                            "No accessor found for $key")
}

The access to the star-projected map got wrapped in an object and accessing the values is safe with this solution.

You can use it like this:

FormFieldProvider.addAccessor(FIRST_NAME, object : FormFieldAccessor<String> {
    //...
})

FormFieldProvider.get<String>(FIRST_NAME).setValueToBuilder(...)

Upvotes: 1

Related Questions