Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala and Java Generics -- Extracting and returning nested types

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.

like image 614
DK_ Avatar asked Feb 11 '13 15:02

DK_


1 Answers

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!
like image 178
Steve Avatar answered Oct 29 '22 17:10

Steve