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

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

I just found this one.

val o1: Ordering[LocalDate] = Ordering.by(_.toString) // works
val o2: Ordering[LocalDate] = Ordering.by(_.toString).reverse // Found: Ordering[Any] Required: Ordering[java.time.LocalDate]

That’s tricky because of the lambda (cf [Prototype] Better type inference for lambdas (e.g., as used in folds) by smarter · Pull Request #9076 · lampepfl/dotty · GitHub), without .reverse we know the expected type which constrains the result type, but with .reverse we don’t really know anything about what by should return until we’ve typed it.

Rather than making inference go through heroic efforts, I think the real fix here is to make Ordering contravariant. This wasn’t something that would have worked in Scala 2 due to differences in implicit resolution, but it’d make sense now. Unfortunately, it’s not something we can do until we get to change the standard library in the distant future.

2 Likes

I’ll just leave this one here, though it’s arguably a new feature. Better implicit resolution in pattern matches

I see, thank you for the explanation!

When using the underscore in function literals, the compiler can sometimes not infer the type. Example:

  case class A(i: Int)
  val o: Option[A] = Some(A(1))
  val r1: Option[A] = o.flatMap(Option(_)) // works
  val r2: Option[Int] = o.flatMap(Option(_.i)) // missing parameter type

That’s not a type inference problem. It’s how the desugaring of _ works.

val r1: Option[A] = o.flatMap(Option(_))

desugars to

val r1: Option[A] = o.flatMap(x => Option(x))

which is correct. But

val r2: Option[Int] = o.flatMap(Option(_.i))

desugars to

val r2: Option[Int] = o.flatMap(Option(x => x.i))

which does not make sense.

You need to write

val r2: Option[Int] = o.flatMap(x => Option(x.i))

Thanks for the clarification. I had a wrong mental model in mind. I wonder if the desugering rule could be extended to translate an _ into a function binding at the next “outer” level the needs a function valued parameter but is given a parameter of a different type.

That would be extremely confusing for anyone trying to read and understand the code, and it would break (possibly silently) based on small changes in APIs or type inference.

2 Likes

Inspired by a previous conversation here, I have a branch where the warning suggests the correct syntax. This is a common case because it’s a bit subtle. I’ll try to revive that PR.

You can solve this, for now at least, with o.flatMap(Option apply _.i).

enum CounterInput[Response]:
  case Inc extends CounterInput[Unit]
  case Dec extends CounterInput[Unit]
  case Get extends CounterInput[Int]

val counterBehavior: [O] => (Int, CounterInput[O]) => (Int, O) =
  [O] =>
    (state: Int, msg: CounterInput[O]) =>
      ((state, msg) match
        case (s, CounterInput.Inc) =>
          (s + 1, ())
        case (s, CounterInput.Dec) =>
          (s + 1, ())
        case (s, CounterInput.Get) =>
          (s, s)
      ): (Int, O) // This is required due to another type inference bug? https://github.com/lampepfl/dotty/issues/15554

// fails to infer
val counterBehavior2: [O] => (Int, CounterInput[O]) => (Int, O) =
  case (s, CounterInput.Inc) =>
    (s + 1, ())
  case (s, CounterInput.Dec) =>
    (s + 1, ())
  case (s, CounterInput.Get) =>
    (s, s)

Last one gives

Missing parameter type

I could not infer the type of the parameter x$1 of expanded function:
x$1 => 
  x$1 match 
    {
      case (s, CounterInput.Inc) => 
        (s + 1, ())
      case (s, CounterInput.Dec) => 
        (s + 1, ())
      case (s, CounterInput.Get) => 
        (s, s)
    }.

I believe this is related to this:

val example: [T] => Int => Int = {case a: Int => a}
/*
Missing parameter type

I could not infer the type of the parameter x$1 of expanded function:
x$1 => 
  x$1 match 
    {
      case a:Int => 
        a
    }.
*/

Someone mentioned type inference along with eta expansion are not implemented in Dotty for polymorphic function types. I wonder if there are any plans for it. GADT kinda feels unergonomic without it.

1 Like

Polymorphic eta-expansion is on it’s way !
I started work on it last semester: https://github.com/lampepfl/dotty/pull/14015
(as a semester project)
And one of the goals of my internship is to finish it, so unless something goes wrong, there will be a fully working version before the end of next semester !
(And it’ll get incorporated in the language maybe a little later)

Do note however this might not solve the problem discussed above, as in it’s simplest form, it will not add type lambdas to functions

def metafoo(f: [T] => T => T) = ???
metafoo(t => t) // will not compile with just Polymorphic Eta Exansion, it needs something more
5 Likes