Java generics can infer the type of generic type parameters based on the return type of expressions. Consider the following:
public static <T> T uncheckedCast(Object o) {
return (T)o;
}
We can call it such as:
Map<Baz,Bog> bazbogMap = new HashMap<Baz,Bog>();
String foo = uncheckedCast(bazbogMap);
This will compile but throw a RuntimeException
when it is invoked, as it will try to cast the Map
to a String
but fail. But the point is that Java inferred the value of <T>
based on the expected resultant type at the callsite.
We can also do this in Scala with:
def uncheckedCast[T](o: AnyRef): T = o.asInstanceOf[T]
So far, so good.
Java can also infer type arguments from nested definitions and return those (i.e. we do not actually have to assign the result to a type to use it as above before using it; Java knows what it is already.)
A simple class showing this:
import java.util.HashMap;
import java.util.Map;
public class KeysAndValues {
public interface Key<T> {}
public static class StringKey implements Key<String> {}
private final Map<Class<?>, Object> lookup;
public KeysAndValues() {
lookup = new HashMap<Class<?>, Object>();
}
@SuppressWarnings("unchecked")
public <V, K extends Key<V>> V put(Class<K> key, V value) {
return (V) lookup.put(key, value);
}
@SuppressWarnings("unchecked")
public <V, K extends Key<V>> V get(Class<K> key) {
return (V) lookup.get(key);
}
public static void test() {
KeysAndValues kv = new KeysAndValues();
kv.put(StringKey.class, "BAM!"); // returns null
kv.put(StringKey.class, "BOOM!"); // returns "BAM!"
kv.get(StringKey.class); // returns "BOOM!"
}
}
However, in Scala this class causes problems. The REPL:
scala> val kv = new KeysAndValues
kv: KeysAndValues = KeysAndValues@13c2982e
scala> import KeysAndValues.StringKey
import KeysAndValues.StringKey
scala> kv.put(classOf[StringKey], "BAM!")
res0: java.lang.String = null
scala> kv.put(classOf[StringKey], "BOOM!")
res1: java.lang.String = BAM!
scala> kv.get(classOf[StringKey])
<console>:10: error: inferred type arguments [Nothing,KeysAndValues.StringKey] do not conform to method get's type parameter bounds [V,K <: KeysAndValues.Key[V]]
kv.get(classOf[StringKey])
The two calls to put()
specify a value for the V
type parameter which allows them to work, but as soon as the V
parameter needs to be extracted from the inheritance of the K
parameter, things break down. The Java code has no such issue.
The only way to get Scala not to complain is to explicitly define the types:
kv.get[String, StringKey](classOf[StringKey])
(For something simple and contrived like the above it's marginally alright (violates DRY), but for something more involved, like The Stanford Core NLP API, where you have to do something like:
doc.get[java.util.Map[Integer, CorefChain], CorefChainAnnotation](classOf[CorefChainAnnotation])
Which includes manually looking up the nested types so you can add them, is quite a pain.)
The Question(s):
Is there a way to get this to work without having to specify the type parameters so verbosely? More importantly, why is this a problem to begin with? How is Java able to infer the type argument when Scala is not?
EDIT 1: Removed most of the code demonstrating the Stanford Core NLP problem and replaced it with a general example of the difference in Scala/Java generics that exemplifies the issue.
I can't tell you exactly why Scala can't infer the Java type parameters, but I can at least show you how to declare the type parameters in Java so that Scala can infer them:
@SuppressWarnings("unchecked")
public <V> V put(Class<? extends Key<V>> key, V value) {
return (V) lookup.put(key, value);
}
@SuppressWarnings("unchecked")
public <V> V get(Class<? extends Key<V>> key) {
return (V) lookup.get(key);
}
I think the key point here is that you don't need that K
type parameter at all - a wildcarded type parameter, ? extends Key<V>
will give you the same functionality. This then works happily from Scala without having to specify any type parameters:
scala> kv.put(classOf[StringKey], "BAM!")
res0: String = null
scala> kv.get(classOf[StringKey])
res1: String = BAM!
EDIT:
Here's the best I could do for a workaround. It requires higher-kinded types and a cast. Not sure that I can explain why this compiles but the other version doesn't though. ;-)
scala> import KeysAndValues._
import KeysAndValues._
scala> import scala.language.higherKinds
import scala.language.higherKinds
scala> def get[V, K[X] <: Key[X]](kv: KeysAndValues, key: Class[_ <: K[V]]) =
| kv.get[V, K[V]](key.asInstanceOf[Class[K[V]]])
get: [V, K[X] <: KeysAndValues.Key[X]](kv: KeysAndValues, key: Class[_ <: K[V]])V
scala> val kv = new KeysAndValues
kv: KeysAndValues = KeysAndValues@24c63dac
scala> kv.put(classOf[StringKey], "BAM!")
res0: String = null
scala> get(kv, classOf[StringKey])
res1: String = BAM!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With