Dylan Dylan - 6 days ago 6
Scala Question

Get the "most specific" input type

Suppose I have a function-like type e.g.

trait Parser[-Context, +Out]


and I want to be able to combine multiple parsers such that the combined
Context
will be the most-specific type among the combined parsers' contexts. For example:

Parser[Any, Int] + Parser[String, Long] = Parser[String, (Int, Long)]
Parser[String, Int] + Parser[Any, Long] = Parser[String, (Int, Long)]
Parser[Option[Int], Foo] + Parser[Some[Int], Bar] = Parser[Some[Int], (Foo, Bar)]
Parser[String, Foo] + Parser[Int, Bar] = <should be a compile error>


To put the example in more concrete terms, suppose I have a function combiner like

def zipFuncs[A, B1, B2](f1: A => B1, f2: A => B2): A => (B1, B2) = {
a => (f1(a), f2(a))
}


and some functions like

val f1 = { a: Any => 123 }
val f2 = { a: String => 123 }
val f3 = { a: Option[Int] => 123 }


Now I can do

> zipFuncs(f1, f2)
res1: String => (Int, Int) = <function>

> zipFuncs(f1, f3)
res2: Option[Int] => (Int, Int) = <function>

> zipFuncs(f2, f3)
res3: Option[Int] with String => (Int, Int) = <function1>


But what I want is for
zipFuncs(f2, f3)
to not compile at all. Since
String
is not a subtype of
Option[Int]
, and
Option[Int]
is not a subtype of
String
, there's no way to construct an input value for
res3
.

I did create a typeclass:

// this says type `T` is the most specific type between `T1` and `T2`
sealed trait MostSpecificType[T, T1, T2] extends (T => (T1, T2))
// implementation of `object MostSpecificType` omitted

def zipFuncs[A, A1, A2, B1, B2](f1: A1 => B1, f2: A2 => B2)(
implicit mst: MostSpecificType[A, A1, A2]
): A => (B1, B2) = { a: A =>
val (a1, a2) = mst(a)
f1(a1) -> f2(a2)
}


This accomplishes the goal described above, but with a really annoying problem. IntelliJ will highlight valid combinations as errors, inferring that the "most specific type (
A
)" is actually
Nothing
when it is in fact a real value. Here's the actual issue in practice.

The highlighting issue is surely a bug in IntelliJ, and google searching seems to imply that various resets/cache wipes/etc should fix it (it didn't). Regardless of the blame, I'm hoping to find an alternate approach that both satisfies my original requirement, and doesn't confuse IntelliJ.

Answer

If you want this to work only when one of the types is a subtype of the other, then you can do this:

def Zip[A,X,Y](f: A => X, g: A => Y): A => (X,Y) = a => (f(a), g(a))

implicit class ZipOps[A,X](val f: A => X) extends AnyVal {

  def zip[A0, Y](g: A0 => Y)(implicit ev: A0 <:< A): A0 => (X,Y) = 
    Zip({a: A0 => f(a)},g)

  def zip[A0 >: A, Y](g: A0 => Y): A => (X,Y) = 
    Zip(f,g)

}

val f1: Any => Int = { a: Any => 123 }
val f2: String => Int = { a: String => 123 }
val f3: Option[Int] => Int = { a: Option[Int] => 123 }

val x1 = f1 zip f2 // works
val x1swap = f2 zip f1 // works
val x2 = f1 zip f3 // works
val x3 = f2 zip f3 // cannot prove that Option[Int] <:< String
val x3swap = f3 zip f2 // cannot prove that String <:< Option[Int]
Comments