Better type inference for Scala: send us your problematic cases!

This thread is specifically about improving Dotty’s type inference. For Scala 2 issues, I suggest opening an issue on github.com/scala/bug/issues

3 Likes

The following shapeless-style derivation of a polymorphic sequence operation for Option doesn’t infer a useful type in Scala 3.0.0-M1. The type is checkable but on failure the expected type is not useful.


object Blah {

  trait Options[T <: Tuple] {
    type Out <: Tuple
    def apply(t: T): Option[Out]
  }

  object Options {

    type Aux[T <: Tuple, O <: Tuple] = Options[T] { type Out = O }

    implicit def single[H]: Aux[Option[H] *: EmptyTuple, H *: EmptyTuple] =
      new Options[Option[H] *: EmptyTuple] {
        type Out = H *: EmptyTuple
        def apply(t: Option[H] *: EmptyTuple): Option[H *: EmptyTuple] =
          t.head.map(_ *: EmptyTuple)
      }

    implicit def multiple[H, T <: Tuple](
      implicit ev: Options[T]
    ): Aux[Option[H] *: T, H *: ev.Out] =
      new Options[Option[H] *: T] {
        type Out = H *: ev.Out
        def apply(t: Option[H] *: T) =
          (t.head.flatMap(h => ev(t.tail).map(t => h *: t)))
      }

  }

  extension [T <: Tuple](t: T)(using ev: Options[T]) {
    def sequence: Option[ev.Out] = ev(t)
  }

  // If we annotate this with an incorrect type it fails to typecheck and gives the inferred
  // type as Option[?1.Out] rather than the expected fully-expanded type.
  val x: Option[(Int, String, Boolean, Double)] =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence

}

Is there a better way to do this?

Maybe the pretty-printer could try harder to get rid of these path-dependent things (bug report welcome), but if we avoid the Aux pattern and just use type parameters the problem goes away too:

object Blah {
  trait Options[T <: Tuple, Out] {
    def apply(t: T): Option[Out]
  }
  // one can always define `type Foo[T <: Tuple] = Options[T, _]`
  // to hide the parameter.

  object Options {
    implicit def single[H]: Options[Option[H] *: EmptyTuple, H *: EmptyTuple] =
      new Options[Option[H] *: EmptyTuple, H *: EmptyTuple] {
        def apply(t: Option[H] *: EmptyTuple): Option[H *: EmptyTuple] =
          t.head.map(_ *: EmptyTuple)
      }

    implicit def multiple[H, T <: Tuple, Tail <: Tuple](
      implicit ev: Options[T, Tail]
    ): Options[Option[H] *: T, H *: Tail] =
      new Options[Option[H] *: T, H *: Tail] {
        def apply(t: Option[H] *: T) =
          (t.head.flatMap(h => ev(t.tail).map(t => h *: t)))
      }
  }

  extension [T <: Tuple, Out](t: T)(using ev: Options[T, Out]) {
    def sequence: Option[Out] = ev(t)
  }

  // OK:
  val x: Option[(Int, String, Boolean, Double)] =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence 

  // error: Found:    Option[Int *: String *: Boolean *: Double *: EmptyTuple]
  //        Required: String
  val y: String =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence 
}
1 Like

This is a pretty interesting gotcha I just stumbled upon: https://scastie.scala-lang.org/8r2pKMoXRyCk1wjUYeccHQ

Inlined:

def isTrue(b: Boolean): Boolean = b
val notTrue = !isTrue(_) // Missing parameter type\n\nI could not infer the type of the parameter _$1
2 Likes

I believe that is !((b: Boolean) => b), and since Function1 has no ! method, it gets lost. The error message is deceiving. I expect you intended it to expand like this:

val notTrue = (b: Boolean) => !isTrue(b)

Which compiles fine.

Actually, the error message suggests the expansion is as intended, but then the type inference still fails:


error: missing parameter type for expanded function ((<x$1: error>) => isTrue(x$1).unary_$bang)

Equivalent case without prefix operator:

> def m(i: Int): Int = i
def m(i: Int): Int

> val isFinite = m(_).isFinite
^

error: missing parameter type for expanded function ((<x$1: error>) => m(x$1).isFinite)
2 Likes

Probably moot since we will get Enums in Dotty, but thgis is one that frequently pops up for me and I find it annoying:

trait Error
case class SomethingBadHappened(msg: String) extends Error
case object UnexpectedError extends Error
//plus a few more 

def someIO: IO[String, Thing]

def newIO: IO[Error, Thing] = someIO.leftMap(SomethingBadHappened(_)) //expected Error but got SomethingBadHappened

def newIO: IO[Error, Thing] = someIO.leftMap(SomethingBadHappened(_):Error) //compiles fine

also may be missing something with some language design principle, but this seems like a failure of the compiler to me

3 Likes

Uncommenting line 6 works, but would be nice if compiler could infer these instead of Nothing?

1 Like

I wish I could write compose as

  def compose[G[_]: Functor]: Functor[F[G[_]]] =
    Functor.make[F[G[_]]](
      [A, B] => f => fga => map(fga)(Functor[G].map(_)(f))
    )

That’s not type inference specific though, just syntax. Same for value level functions. This won’t work as well.

def compose[A, B, C](f: A => B, g: B => C): A => C = f(g(_))

Yes, for the composed type, I realised after. We could get that with a composition type operator I guess, e.g.

type ∘[F[_], G[_]] = [A] =>> F[G[A]]

But the type signature for parameters f and fga still are type-inference related, right?

Upper bound <: List[Int] on type constructor F is not satisfied by List[Int] instantiation

scala> def f[F[_ <: Int] <: List[Int], A <: Int](as: F[A]) = as
def f[F[_ <: Int] <: List[Int], A <: Int](as: F[A]): F[A]

scala> f(List(42))
       ^
       error: inferred type arguments [List,Int] do not conform to method f's type parameter bounds [F[_ <: Int] <: List[Int],A <: Int]
             ^
       error: type mismatch;
        found   : List[Int]
        required: F[A]

however the following type alias with not bound on the argument A satisfies it

scala> type Lst[A] = List[Int]
type Lst

scala> f[Lst, Int](List(42))
val res1: Lst[Int] = List(42)

Cross-posted

2 Likes

Higher-order unification in general is undecidable, so the compiler has to limit it to a reasonable subset, in particular when checking List[Int] <:< F[A] we end up checking if List <: F and Int <: A, and the first check doesn’t succeed, you can write def f[F[X <: Int] <: List[X], A <: Int](as: F[A]) = as instead.

5 Likes

The following works as expected in Scala 2:

Seq(1) ++ Seq(Seq(2)).reduce(_ ++ _)

In Scala 3, the compiler complains that value ++ is not a member of IterableOnce[Int].

Giving a type hint

Seq(1) ++ (Seq(Seq(2)).reduce(_ ++ _): Seq[Int])

makes the snippet compile.

we end up checking if List <: F and Int <: A , and the first check doesn’t succeed

Why

F[X <: Int] <: List[X]

doesn’t have the same higher-order unification difficulty like

F[X <: Int] <: List[Int]

Isn’t List <: F check performed in both cases?

Isn’t List <: F check performed in both cases?

The issue is that ?F := List which expands to ?F = [X] =>> List[X] is not a valid solution to the constraint ?F <: [X <: Int] =>> List[Int] because we check List[X] <: List[Int] for an arbitrary X. Now, could we change the subtyping rules to make this typecheck by instead restricting the check to be done for X <: Int ? I’m not sure, but it relates to some long-standing discussions about how to do subtyping with type lambdas: Subtype checking of type lambdas with bounds is too restrictive · Issue #6320 · lampepfl/dotty · GitHub, Subtype checking of type lambdas with bounds is too restrictive, part 2 · Issue #6499 · lampepfl/dotty · GitHub

3 Likes
sealed trait Foo
case class Foo1() extends Foo
case class Foo2() extends Foo

case class Wrapper[A](a: A)

val x: Wrapper[Foo] = Wrapper(Foo2())

x match {
  case w @ Wrapper(_: Foo1) =>
    forFoo1(w.asInstanceOf[Wrapper[Foo1]])
  case w @ Wrapper(_: Foo2) =>
    forFoo2(w.asInstanceOf[Wrapper[Foo2]])
}

def forFoo1(foo1: Wrapper[Foo1]) = {
  println("foo1)"sc
}

def forFoo2(foo2: Wrapper[Foo2]) = {
  println("foo2")
}

On lines 11 and 13 the compiler already knows, that type of w is narrowed by pattern matching.

2 Likes

What you’re suggesting would be unsound in general, just because Wrapper(_: Foo1) matches doesn’t mean that the type argument of Wrapper is Foo1, it could either be a subtype or a supertype:

val x: Wrapper[Any] = Wrapper(Foo1())
val f = Foo1()
val y: Wrapper[f.type] = Wrapper(f)

Wrapper is invariant, so we can’t allow either x or y to be treated as subtypes of Wrapper[Foo1].

A post was split to a new topic: Type inference for the types of params of defs