3

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 ?

jaumard
  • 8,202
  • 3
  • 40
  • 63

3 Answers3

1

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(...)
Community
  • 1
  • 1
s1m0nw1
  • 76,759
  • 17
  • 167
  • 196
  • I tried to test some your solution but wasn't able to make it compile either, it's part of some legacy system written in Java and I need to keep the same structure in Kotlin :( I update the issue with a simple gist file to show you the structure I have if you don't mind give it a try – jaumard Apr 19 '18 at 09:11
0

@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.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • I also tried to test your solution but wasn't able to make it compile either, it's part of some legacy system written in Java and I need to keep the same structure in Kotlin :( I update the issue with a simple gist file to show you the structure I have if you don't mind give it a try – jaumard Apr 19 '18 at 09:11
  • I don't store the formFieldDefinition as I'm already under a loop a fieldDefinition at the time I want to setValueToBuilder :( – jaumard Apr 19 '18 at 09:12
  • I've edited my answer to explain why your Java code is likely simply hiding the problems. – Alexey Romanov Apr 19 '18 at 09:57
  • "as I'm already under a loop a fieldDefinition at the time I want to setValueToBuilder" That's actually better, it means it's already available and doesn't need to be stored. – Alexey Romanov Apr 19 '18 at 10:00
  • Thanks a lot for the explanation, you're right it use the old raw type... I say fieldDefinition but it's formField, I didn't have the formDefinition at that point unfortunately :( – jaumard Apr 19 '18 at 13:34
0

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
}
Evgeniy
  • 509
  • 5
  • 14