scaladictionaryscala-collectionstype-boundstype-constructor

Non-unary type constructor bounded by unary type constructor


The title is attempting to describe the following subtyping

implicitly[Map[Int, String] <:< Iterable[(Int, String)]]

Type parameter A is inferred to (Int, String) here

def foo[A](cc: Iterable[A]): A = cc.head
lazy val e: (Int, String) = foo(Map.empty[Int, String])

however attempting to achieve similar effect using type parameter bounds the best I can do is explicitly specifying arity of the type constructor like so

def foo[F[x,y] <: Iterable[(x,y)], A, B](cc: F[A, B]): (A, B) = cc.head
lazy val e: (Int, String) = foo(Map.empty[Int, String])

because the following errors

def foo[F[x] <: Iterable[x], A](cc: F[A]) = cc.head
lazy val e: (Int, String) = foo(Map.empty[Int, String])
// type mismatch;
// [error]  found   : A
// [error]  required: (Int, String)
// [error]   lazy val e: (Int, String) = foo(Map.empty[Int, String])
// [error]                                  ^

Hence using Iterable as upper bound it seems we need one signature to handle unary type constructors Seq and Set, and a separate signature to handle 2-arity type constructor Map

def foo[F[x] <: Iterable[x], A](cc: F[A]): A                  // When F is Seq or Set
def foo[F[x,y] <: Iterable[(x,y)], A, B](cc: F[A, B]): (A, B) // When F is Map

Is there a way to have a single signature using type bounds that works for all three? Putting it differently, how could we write, say, an extension method that works across all collections?


Solution

  • I think the issue here is that F is set to Map, and kindness is wrong. You would have to have say: I have some type X, that extends F[A], so that when I upcast it, I can use it as F[A] - which in turn we want to be a subtype of Iterable[A]. If we ask about it this way, it sounds hard.

    Which is why I personally would just stay at:

    @ def foo[A](x: Iterable[A]): A = x.head
    defined function foo
    
    @ foo(List(1 -> "test"))
    res24: (Int, String) = (1, "test")
    
    @ foo(Map(1 -> "test"))
    res25: (Int, String) = (1, "test")
    

    "Give me any x that is an instance of Iterable[A] for A".

    If I had to do some derivation... I would probably also go this way. I think this limitation is the reason CanBuildFrom works the way it works - providing matching for part of the type is hard, especially in cases like Map, so let's provide a whole type at once as a parameter, to limit the number of inference needed.