1

I have a series of Java classes that acts as wrappers for java classes, e.g. Integer, String, ZonedDateTime, etc, and I put them into this Type<T> interface, where T is what the actual underlying Java type would be.

There's another class called: final class Field<T, U extends Type<T>>.

Finally, I have a following builder interface.

class DataBuilder {
    <T, U extends Type<T>> DataBuilder addEntry(Field<T, U> field, T value) {
        return this;
    }
}

This works fine calling from Java side:

    public static void main(String[] args) {
        Field<String, StringType> field1 = new Field<>();
        Field<Boolean, BooleanType> field2 = new Field<>();

        Map<Field, Object> map = new HashMap<>();
        map.put(field1, "abc");
        map.put(field2, true);

        DataBuilder dataBuilder = new DataBuilder();
        map.forEach(dataBuilder::addEntry);

        System.out.println("good");
    }

Calling this from Scala side causes some issue.

object Hello extends App {
  val field1 = new Field[String, StringType]
  val field2 = new Field[java.lang.Boolean, BooleanType]

  val map = Map(
    field1 -> "abc",
    field2 -> boolean2Boolean(true)
  )

  val dataBuilder: DataBuilder = new DataBuilder

  map.foreach { case (key, value) => dataBuilder.addEntry(key, value) }
}

This gives me three errors:

Error:(14, 50) inferred type arguments [Comparable[_ >: Boolean with String <: Comparable[_ >: Boolean with String <: java.io.Serializable] with java.io.Serializable] with java.io.Serializable,_2] do not conform to method addEntry's type parameter bounds [T,U <: example.Type[T]]
  map.foreach { case (key, value) => dataBuilder.addEntry(key, value) }
Error:(14, 59) type mismatch;
 found   : example.Field[_1,_2] where type _2 >: example.BooleanType with example.StringType <: example.Type[_ >: Boolean with String <: Comparable[_ >: Boolean with String <: java.io.Serializable] with java.io.Serializable], type _1 >: Boolean with String <: Comparable[_ >: Boolean with String <: Comparable[_ >: Boolean with String <: java.io.Serializable] with java.io.Serializable] with java.io.Serializable
 required: example.Field[T,U]
  map.foreach { case (key, value) => dataBuilder.addEntry(key, value) }
Error:(14, 64) type mismatch;
 found   : Comparable[_ >: Boolean with String <: Comparable[_ >: Boolean with String <: Comparable[_ >: Boolean with String <: java.io.Serializable] with java.io.Serializable] with java.io.Serializable] with java.io.Serializable
 required: T
  map.foreach { case (key, value) => dataBuilder.addEntry(key, value) }

I understand that scala is trying to infer the most accurate type by trying to find a common type across all the ones in the Map, but is there a way to not make it that explicit while still allowing java library to function in scala?

See demo code in github: https://github.com/ssgao/java-scala-type-issue

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
ssgao
  • 5,151
  • 6
  • 36
  • 52
  • 1
    Going through a `Map` with `Any` is rarely a good idea when goal is to keep types (as there for the builder use after) – cchantep Nov 11 '19 at 21:41
  • The reason behind using a map for holding those pairs is for code reusability: I also want to just log the entire `Map` out. – ssgao Nov 11 '19 at 22:00

2 Answers2

2

Err, I guess this works? It's basically the Java code, except some of the type-hackery is made explicit.

// The forSome type is not inferred
// Instead, I think this is where the wonky Comparable[_ >: What with Even <: Is] with This type comes from
val map = Map[Field[T, U] forSome { type T; type U <: Type[T] }, Any](
  field1 -> "abc",
  field2 -> boolean2Boolean(true)
  // field2 -> new AnyRef // works, there's no checking
)
val dataBuilder = new DataBuilder
// we know that key: Field[T, U] forSome { type T; type U <: Type[T] }
// the second match doesn't *do* anything, but it lets us name these
// two types something (here, t, u) and use them as type arguments to addEntry
map.foreach { case (key, value) => key match {
  case key: Field[t, u] => dataBuilder.addEntry[t, u](key, value.asInstanceOf[t])
} }

Needless, to say, I kind of hate it. I think the only way to really make this sane would be to write some sort of heterogenous map class (I don't think even shapeless's HMap does the trick here, though), but that's hard.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • My compiler doesn't recognize what `Field[t, u]` is. Typechecker complains about the usage of existential types and [this post](https://stackoverflow.com/questions/21226004/what-is-and-when-to-use-scalas-forsome-keyword/21824795) says it'll likely go away soon... Is there a way around using existential type? – ssgao Nov 12 '19 at 13:50
  • Binding type variables is very finicky, but Scala 2.13 handles this code just fine. If you want to remove the existentials, since this code isn't even trying to be safe, just make the key type of the `Map` `Field[_, _]`. This makes it less precise, but that's fine if you just don't care. If you did care, you'd have to make a wrapper class `implicit class FieldOf[T](val field: Field[T, _ <: Type[T]]) extends AnyVal`. – HTNW Nov 12 '19 at 14:44
  • Actually, on second thought, if the replacement for existentials ends up working like it does in Dotty, `Field[_, _]` will actually end up *being the same* as the `forSome` type here. So, there’s really no worry. – HTNW Nov 12 '19 at 17:25
0

You can use a "heterogenous map". A heterogenous map is a map where the keys and values may have different types. I don't know of any existing implementation of a type-safe heterogenous map that satisfies your needs, but here's a really simple implementation of one. I think what you really want is the ability to use the standard collection operations on this funny map, and this should help with that.

import scala.language.existentials
import scala.collection._

class HMap[K[_], V[_]](underlying: immutable.Map[K[_], V[_]] = immutable.Map.empty[K[_], V[_]])
  extends Iterable[(K[T], V[T]) forSome { type T }]
     with IterableOps[(K[T], V[T]) forSome { type T }, Iterable, HMap[K, V]] {
  // collections boilerplate
  override protected def fromSpecific(coll: IterableOnce[(K[T], V[T]) forSome { type T }]): HMap[K, V]
  = new HMap[K, V](immutable.Map.from(coll))
  override protected def newSpecificBuilder: mutable.Builder[(K[T], V[T]) forSome { type T }, HMap[K, V]]
  = immutable.Map.newBuilder[K[_], V[_]].mapResult(new HMap[K, V](_))
  override def empty: HMap[K, V] = new HMap[K, V]
  override def iterator: Iterator[(K[T], V[T]) forSome { type T }]
  = new Iterator[(K[T], V[T]) forSome { type T }] {
    val underlying = HMap.this.underlying.iterator
    override def hasNext: Boolean = underlying.hasNext
    override def next(): (K[T], V[T]) forSome { type T }
    = underlying.next().asInstanceOf[(K[T], V[T]) forSome { type T}]
  }

  // Mappy operations
  def get[T](k: K[T]): Option[V[T]] = underlying.get(k).map(_.asInstanceOf[V[T]])
  def +[T](kv: (K[T], V[T])): HMap[K, V] = new HMap[K, V](underlying + kv)
  def -[T](k: K[T]): HMap[K, V] = new HMap[K, V](underlying - k)
}
object HMap {
  // Mappy construction
  def apply[K[_], V[_]](elems: (K[T], V[T]) forSome { type T }*): HMap[K, V] = new HMap[K, V](immutable.Map(elems: _*))
}

Now everything works pretty neatly

type FieldOf[T] = Field[T, _ <: Type[T]]
type Id[T] = T
val map = HMap[FieldOf, Id](
  field1 -> "abc",
  field2 -> boolean2Boolean(true)
  // field2 -> new AnyRef // doesn't work, readable(-ish) error message
)
val dataBuilder = new DataBuilder
map.foreach { case (key, value) => dataBuilder.addEntry(key, value) }

If existentials make you uncomfortable, it should work to replace (K[T], V[T]) forSome { type T } with Prod[K, V, _] where

case class Prod[K[_], V[_], T](k: K[T], v: V[T])
object Prod {
    implicit def fromTup2[K[_], V[_], T](kv: (K[T], V[T])): Prod[K, V]
    = Prod(kv._1, kv._2)
}

(with a few more changes where needed)

HTNW
  • 27,182
  • 1
  • 32
  • 60