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

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

This works:

def showRestriction[A](toString: A => String = (a: A) => a.toString): String = ???

But this does not:

def showRestriction[A](toString: A => String = _.toString): String = ???

That’s because the type of the default is not always a subtype of the declared type of the parameter.
E.g.

scala> def fun[A](x: A = 1) = x
def fun[A](x: A): A
                                                                                                                                                
scala> fun()
val res0: Int = 1

In those cases the type must be explicit (there is no way to infer it):

def showRestriction[A](f: A => String = (a: Int) => a.toString): String = ???

But I don’t see why that excludes the type from being inferred in:

def showRestriction[A](f: A => String = _.toString): String = ???

The only possible type to infer must be A (the type of f dictates it).

I’m just trying to understand why the first case prevents type inference on the second case.

It’s just more complicated. We would have to have some logic that says we propagate A if the type is needed for typing the default expression. Initially, this was not something our compiler was prepared to do. But by now we have some infrastructure in place that might work for this use case. I can give it a try.

3 Likes

I’m not sure if this is intended to work, but implicit search fails here

trait Typeclass[Self] {
  extension (self: Self) def name: String
}

object Hidden {
  given Typeclass[String] with {
    extension (self: String) def name: String = self
  }

  given Typeclass[Int] with {
    extension (self: Int) def name: String = self.toString
  }
}

class TypeclassBox[T: Typeclass](t: T) {
  def apply[U](f: T => Typeclass[T] ?=> U): U = f(t)
}

def branch(b: Boolean): TypeclassBox[_] = {
  import Hidden.given
  if (b) TypeclassBox("4") else TypeclassBox(3)
}
val box = branch(true)
box(_.name) // error: value name is not a member of Playground.box.T

It works fine if branch statically returns TypeclassBox[String] or TypeclassBox[Int]. I’m not sure if there’s any expectation of implicit search on wildcard types, but it’d be nice!

Sorry, I realized the problem here is not implicit search, that works fine. It’s that extension methods must be resolved at compile time. A better alternative is described here.