4

Let's say there's a framework type Row with multiple type parameters, and a method that works with instances of the Row type and uses all these type parameters.

I have a method that works with any type of Row, even different types at the same time, so obviously I'm using the wildcard type Row<?,?>. The question is, how do I invoke a method that takes a Row<R,K> with a Row<?,?>?

My line of thought: I don't exactly know what type Row<?,?> is, but it's surely some kind of Row alright. And when a generic method takes Row<R,K> it means that it wants to do something with R and K but otherwise it can deal with any type of Row. So my "any" type should work with a method that takes "any" type, right?

I'm attaching sample code below with things that I tried. The weirdest thing is that the very last line actually works, but it's not any more type safe than anything else I think. So basically I'd like a cleaner solution than this if possible or an explanation why this is the way to go.

package foo;

public class Experiment {
  // Defined by a framework.
  interface Key<K extends Key<K>> {}

  interface Row<R extends Row<R, K>, K extends Key<K>> {}

  static <R extends Row<R, K>, K extends Key<K>> R copyRow(R row) {
    return row;
  }

  // My experiments below.
  static class Wrapper<R extends Row<R, K>, K extends Key<K>> {
    public final R row = null; // fixme
    public final Class<R> clazz = null; // fixme
  }

  static <R extends Row<R, K>, K extends Key<K>> R upcast(Row<?, ?> row) {
    return (R) row;
  }

  static <R extends Row<R, K>, K extends Key<K>> R upcast(Row<?, ?> row, Class<R> clazz) {
    assert row.getClass().equals(clazz);
    return (R) row;
  }

  public static void main(String[] args) {
    Wrapper<?, ?> wr = null; // fixme
    copyRow(wr.row); // Compilation error
    copyRow(upcast(wr.row)); // Compilation error
    copyRow(upcast(wr.row, wr.clazz)); // This works, why?
  }
}

(You can send this sample straight to javac to see what happens. With Java 1.8: https://pastebin.com/LB10ySsD)

fejesjoco
  • 11,763
  • 3
  • 35
  • 65
  • 1
    You should be able to pass a `Row, ?>` to a generic method taking a `Row`, where `R` and `K` are generics types. I'm not sure what the problem is? You're line `copyRow(wr.row)` also compiles fine for me. – Jorn Vernee May 21 '18 at 18:11
  • Not when R and K are "related". This is what I get where the comment says "error": https://pastebin.com/LB10ySsD – fejesjoco May 21 '18 at 18:14
  • I'm using javac 1.8 from openjdk, what did you use to compile this code? – fejesjoco May 21 '18 at 18:16
  • I'm using eclipse 4.7.3a, but it looks like `javac` rejects it up to version 11. – Jorn Vernee May 21 '18 at 18:19
  • Yeah, IntelliJ doesn't give me the red underlining either, only when I try to compile it. And when I move the static helper into Wrapper, the last line no longer works either. The upcast() hack must be outside, in a static context, and then the last line works... – fejesjoco May 21 '18 at 18:38

1 Answers1

0

Here is one possibility:

public class WildcardsExperiment {
  // Defined by a framework.  <begin unmodifiable>
  interface Key<K extends Key<K>> {}
  interface Row<R extends Row<R, K>, K extends Key<K>> {}

  static <R extends Row<R, K>, K extends Key<K>> R copyRow(R row) {
    return row;
  }
  // <end unmodifiable>

  interface NaturalRowTransformer {
    <R extends Row<R, K>, K extends Key<K>> R apply(R r);
  }

  class Wrapper<R extends Row<R, K>, K extends Key<K>> {
    private final R row = null; // fixme (constructor etc.)
    public final Class<R> clazz = null; // fixme

    R invokeNat(NaturalRowTransformer nat) {
      return nat.apply(row);
    }
  }

  static final NaturalRowTransformer CopyRow = new NaturalRowTransformer() {
    public <R extends Row<R, K>, K extends Key<K>> R apply(R row) {
      return copyRow(row);
    }
  };

  public static void main(String[] args) {
    Wrapper<?, ?> wr = null; // fixme
    wr.invokeNat(CopyRow); // compiles
  }
}

Essentially, the copyRow method is wrapped into a visitor NaturalRowTransformer, which guarantees that it can handle all possible valid combinations of F-bounded type pairs R and K. The Wrapper then provides an accepting method invokeNat, which accepts the visitor and performs the copyRow operation in a scope where R and K are concrete (not wildcards).

The trick is inspired by category theory (hence the "natural" in the name), and imported from Scala, even though current implementations of Scala's pattern matching on types allow for more concise solutions. This solution is known to work under slightly more complex constraints.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Interesting approach, although it looks a bit more convoluted than mine. And it is a bit more work to scale for multiple methods. But I completely agree that this avoids my type binding problem entirely. – fejesjoco May 21 '18 at 20:07
  • @fejesjoco It avoids all type-casts. And it requires exactly two lines for `new NatTrafo() {` and `};` to wrap each polymorphic method, it's a bit of boilerplate, yes, but it's not excessive. – Andrey Tyukin May 22 '18 at 00:53