Простой код на 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? Если знаете — пожалуйста, расскажите.
UPD: darkdimius в комментариях намекнул (за что ему спасибо) что в Scala в системе вывода типов используется declaration-site variance, в то время как в java — use-site variance, то есть Scala пытается выводить типовые параметры от объявления, а не от вызова. А также обрадовал тем, что в Dotty оригинальный пример работает.
Если смешивать две в системе типов сильно рисковано, а в системе вывода типов (inferece) — тем более.
Как с этим жить дальше?
Мы, конечно, можем всегда в таких случаях явно указывать типовые параметры при вызове метода:
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], который мы уже не сможем так легко задекорировать, и придется прибегать к другим хитростям.
