roookeee
roookeee

Reputation: 1828

Why is Kotlins type inference failing when Javas isn't?

Given the following code (which uses a dummy return but shows the issue):

import com.github.roookeee.datus.api.Datus
import com.github.roookeee.datus.api.Mapper
import com.github.roookeee.datus.immutable.ConstructorParameter

data class EntryDto(val id: Long?, val title: String, val content: String)
data class EntryEntity(val id: Long? = null, val title: String, val content: String) {
    fun toDto(): EntryDto {
        val mapper: Mapper<EntryEntity, EntryDto> = Datus.forTypes(this.javaClass, EntryDto::class.java)
                .immutable(::EntryDto)
                .from(EntryEntity::id).to(ConstructorParameter::bind)
                .from(EntryEntity::title).to(ConstructorParameter::bind)
                .from(EntryEntity::content).to(ConstructorParameter::bind)
                .build()
        return EntryDto(null, "", "")
    }
}

Kotlin is not able to infer the correct generic types whereas Java >= 8 does (given two Java classes that are identical to the data classes here - two immutable object classes). I tried with the defaults for Kotlin 1.3.0 and -XXLanguage:+NewInference but the later couldn't even infer the right overload to pick for .immutable.

Here is the datus dependency information to make the above code compile (I can't reduce this problem without the library, it's too complex in its generic usage):

<dependency>
    <groupId>com.github.roookeee</groupId>
    <artifactId>datus</artifactId>
    <version>1.3.0</version>
</dependency>

Am I missing something? I would love to make my library more compatible with kotlin but am at a loss how to go from here or what the exact name of the inference error is.

You can find datus sources here.

This is the corresponding java code:

import com.github.roookeee.datus.api.Datus;
import com.github.roookeee.datus.api.Mapper;
import com.github.roookeee.datus.immutable.ConstructorParameter;

class Java8Code {
    static class EntryDto {
        private final Long id;
        private final String title;
        private final String content;

        EntryDto(Long id, String title, String content) {
            this.id = id;
            this.title = title;
            this.content = content;
        }

        public Long getId() {
            return id;
        }

        public String getTitle() {
            return title;
        }

        public String getContent() {
            return content;
        }
    }

    static class EntryEntity {
        private final Long id;
        private final String title;
        private final String content;

        EntryEntity(Long id, String title, String content) {
            this.id = id;
            this.title = title;
            this.content = content;
        }

        public Long getId() {
            return id;
        }

        public String getTitle() {
            return title;
        }

        public String getContent() {
            return content;
        }

        public EntryDto toDto() {
            Mapper<EntryEntity, EntryDto> mapper = Datus.forTypes(EntryEntity.class, EntryDto.class)
                    .immutable(EntryDto::new)
                    .from(EntryEntity::getId).to(ConstructorParameter::bind)
                    .from(EntryEntity::getTitle).to(ConstructorParameter::bind)
                    .from(EntryEntity::getContent).to(ConstructorParameter::bind)
                    .build();
            return mapper.convert(this);
        }
    }

}

EDIT 2: An image of the error message + some notes below enter image description here

3 type arguments expected for interface ConstructorParameter<In : Any!, GetterReturnType : Any!, Result : Any!> - Kotlin seems to expect generic type parameters for the interfaces method reference but that simply isn't possible in Kotlin nor needed in Java.

Upvotes: 1

Views: 195

Answers (1)

George Leung
George Leung

Reputation: 1552

The to extension function is not the problem. It just pops up because the compiler does not see the correct parameter for the member method.

What Kotlin doesn't like, is the generic type without the type parameter attached. ConstructorParameter is not a type until you specify the type parameters. So it complains when it sees ConstructorParameter::bind, the left hand side of :: should be a type.

If you write .to { x, y -> x.bind(y) }, Kotlin can infer the type just fine. But you can't expect your users to write this "same" lambda so many times.

Extension functions to the rescue!

fun <In, CurrentType, Next> ConstructorParameterBinding<In, CurrentType, out ConstructorParameter<In, CurrentType, Next>>.bind(): Next =
    this.to { x, y -> x.bind(y) }

val mapper: Mapper<EntryEntity, EntryDto> = Datus.forTypes(this.javaClass, EntryDto::class.java)
    .immutable(::EntryDto)
    .from(EntryEntity::id).bind()
    .from(EntryEntity::title).bind()
    .from(EntryEntity::content).bind()
    .build()

Upvotes: 1

Related Questions