Простой Java-код, ломающий систему вывода типов Scala

в 6:15, , рубрики: dependent type, generics, java, jvm, scala, type erasure, Программирование

Простой код на Java: generic интерфейс, класс который его реализует, и метод, принимающий его экземпляр:

//Gen.java:
public interface Gen<A> {
    A value();
}

//GenInt.java:
public class GenInt implements Gen<Integer> {
    private final int i;
    public GenInt(int i) {
        this.i = i;
    }
    @Override
    public Integer value() {
        return i;
    }
}

//GenTest.java:
public class GenTest {
    public static <A extends Gen<T>, T> T test(A a) {
        return a.value();
    }
    public static void main(String[] argv) {
        GenInt g = new GenInt(42);
        Integer i = test(g);
    }
}

Он компилируется и даже запускается. Как вы думаете, что будет, если вам захочется вызывать метод test из Scala?

object TestFail extends App {
  val genInt = new GenInt(42)
  val i = GenTest.test(genInt)
}

Пытаемся скомпилировать и видим что все плохо:

Error:(3, 11) inferred type arguments [GenInt,Nothing] do not conform to method test's type parameter bounds [A <: Gen[T],T]
  GenTest.test(genInt)
Error:(3, 16) type mismatch;
 found   : GenInt
 required: A
  GenTest.test(genInt)

Вот так мощная система типов Scala ломается о generic метод, который нормально переваривает Java.

Что же произошло?

В отличие от Java, Scala не умеет выводить типовые параметры из родительских классов. Может быть из-за того что в Java не было Nothing? Если знаете — пожалуйста, расскажите.

Как с этим жить дальше?

Мы, конечно, можем всегда в таких случаях явно указывать типовые параметры при вызове метода:

object TestExplicit extends App {
  val genInt = new GenInt(42)
  GenTest.test[GenInt, Integer](genInt)
}

Но, согласитесь, это все же немного не то, чего мы хотели.

А чем нам не подходит родительский класс Gen[T]? Во-первых, он не соответствует границам типа, которые поддерживает аргумент, поскольку не является подтипом самого себя. Во-вторых, при этом мы потеряем оригинальный тип A, а он может быть нам нужен.

Workaround

На помощь нам приходят зависимые типы.

Будем сохранять тип класса наследника Gen[T] как зависимый в трейте-обертке GenS[T].

trait GenS[T] extends Gen[T] {
  type SELF <: GenS[T]
  def self: SELF
}

class GenIntS(i: Int) extends GenInt(i) with GenS[Integer] {
  type SELF = GenIntS
  def self: SELF = this // вернуть объект под его настоящим типом
}

Теперь мы можем спокойно принимать объекты наследников трейта GenS[T] под его родительским типом, не боясь потерять исходный тип, потому что он статически сохранен.

Сделаем для этого обертку метода GenTest.test в которой поможем компилятору вывести типы:

object TestWrapped extends App {

  def test[T](g: GenS[T]): T  = {
    GenTest.test[g.SELF, T](g.self)
  }

  private val v = new GenIntS(42)
  val i = test(v)
}

Заключение

Описанный подход не идеален, требует писать обертки для классов, тем не менее позволяет избежать явного указания всех типовых параметров при каждом вызове, и может помочь при написании Scala-оберток для Java-библиотек.

Также стоит заметить, что с ним будут сложности когда обобщенный интерфейс выводится из аргументов не напрямую, например когда метод принимает тип Class[A], который мы уже не сможем так легко задекорировать, и придется прибегать к другим хитростям.

Автор: Илья Симоненко

Источник

Поделиться