Simple Java code: a generic interface, a class that implements it, and a method that accepts its instance:
//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); } }
It compiles and even starts. What do you think will happen if you want to call the test
method from Scala?
object TestFail extends App { val genInt = new GenInt(42) val i = GenTest.test(genInt) }
We are trying to compile and see that everything is bad:
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)
This is how the powerful Scala type system breaks down about the generic method that normally digests Java.
Unlike Java, Scala does not know how to display typical parameters from parent classes. Maybe due to the fact that Java was not Nothing? If you know, please tell us.
UPD: darkdimius in the comments hinted (for which he thanks) that Scala uses the declaration-site variance in the type inference system, while in java it uses the use-site variance, that is, Scala tries to display typical parameters from the ad, and not from call I also rejoiced that the original example works in Dotty.
If you mix the two types in the system is very risky, and in the type inference system (inferece) - even more so.
Of course, in such cases we can always explicitly specify typical parameters when calling the method:
object TestExplicit extends App { val genInt = new GenInt(42) GenTest.test[GenInt, Integer](genInt) }
But, you see, this is still a bit not what we wanted.
And why does the parent class Gen[T]
not suit us? First, it does not match the boundaries of the type that the argument supports, since it is not a subtype of itself. Secondly, at the same time we will lose the original type A
, and we may need it.
We are helped by dependent types.
We will keep the class type of the successor Gen[T]
as dependent in the wrapper 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 // }
Now we can safely receive objects of the heirs of the GenS[T]
trait under its parent type, without fear of losing the original type, because it is statically preserved.
For this we will GenTest.test
wrapper for the GenTest.test
method in which we will help the compiler to infer types:
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) }
The described approach is not perfect, it requires writing wrappers for classes, nevertheless it avoids explicitly specifying all typical parameters for each call, and can help when writing Scala wrappers for Java libraries.
It is also worth noting that there will be difficulties with it when the generic interface is not directly derived from the arguments, for example, when the method takes the type Class[A]
, which we can no longer decorate so easily, and will have to resort to other tricks.
Source: https://habr.com/ru/post/335812/
All Articles