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

Reliable and powerful type inference in a language like Scala is crucial to providing a good developer experience. This has been on our mind while developing Dotty and as a result we’ve tried pretty hard to keep inferring things that Scala 2 is able to infer while at the same time simplifying and generalizing the type inference rules so that we may infer more things (there is no written documentation on this yet, but I have an old talk where I go into details: video slides)

However, it’s pretty hard to find examples of code that should infer but doesn’t, this is why I’d like to use this thread to ask for code samples where you feel that type inference should be able to do a better job. If possible:

  • Try keeping the code as simple and small as possible
  • Avoid relying on external dependencies
  • Avoid depending on macros (e.g., replace macros by dummy code with the same signature, protip: you can pass -Xprint:typer to scalac to see what your code looks like after typechecking and macro expansion, and you can also pass -Xprint-types to see the type of every tree node).

But I’ll accept any kind of code that I can run. My preferred delivery format would be a link to a page on https://scastie.scala-lang.org/ (you can add all the dependencies you want in the “Build Settings” page).
Feel free to also explain why you think that inference should work in this particular case!

Thank you for participating!
Guillaume

15 Likes

A great example of the kind of insights I’m after is @mpilquist blog post on Inference Driven Design where he explains what code he would like to write and what code he actually has to write to get inference to work properly for users of his library (in this particular case, Dotty inference is good enough to work without any of the workaround needed for Scala 2, we also managed to improve the situation for Scala 2 in https://github.com/functional-streams-for-scala/fs2/pull/1173)

1 Like

Here’s a problem that sometimes comes up with existentials:

// Set is an example; any generic, invariant type would do
def remove[A](set: Set[A], a: A): Set[A] = set - a 
val set = Set(1) 
remove(set, set.head) // OK
val set2: Set[_] = set
remove(set2, set2.head) // Does not compile

I usually get an existential type by aggregating instances with different type arguments. A List(Set(1), Set("")) has an inferred type like List[Set[_ >: bounds...]]. In dotty, the inferred type is List[Set[Any]], so the situation is already much better. This works in dotty, but not in scala:

List(Set(1), Set("foo")).map(s => s - s.head)
1 Like

@danarmak Thanks for the example, I’ll keep that in mind though we need to be careful with wildcards: Java tries pretty hard to make them easy to use but sometimes gets it very wrong: http://wouter.coekaerts.be/2018/java-type-system-broken

The smallest example I can think of:

  Some(1).fold(List.empty)(_ => List("a", "b", "c"))

produces:

[error]  type mismatch;
[error]  found   : String("c")
[error]  required: Nothing
[error]   Some(1).fold(List.empty)(_ => List("a", "b", "c"))
[error]      
4 Likes

@guizmaii This particular example works with Dotty, though more complex examples involving folds do not work, e.g. List(1).foldLeft(Nil)((acc, x) => x :: acc) fails, because we desugar the lambda to a method, and we need to give types to the method parameters, but these types haven’t been fully inferred yet. I’d like to see if I can improve this, but no promise for now :).

4 Likes

scala> List(“Hello”, “World”).toSet.map(.size)
:12: error: missing parameter type for expanded function ((x$1: ) => x$1.size)
List(“Hello”, “World”).toSet.map(
.size)

** ^**

I feel it should work, because the following works:

scala> val strings = List(“Hello”, “World”).toSet; strings.map(_.size)
strings: scala.collection.immutable.Set[String] = Set(Hello, World)
res3: scala.collection.immutable.Set[Int] = Set(5)

@curoli Your example works in Dotty already.

2 Likes

Sweet!

I have one that already works with Dotty, but I would like to ensure that it keeps working with Dotty. Could we add it as a test case to the Dotty test suite?
https://scastie.scala-lang.org/RiIaDqbHTfatTRBWWjFV7A

I would like to +1 the suggestion of improving inference for fold.

@olhotak Yes, PRs adding test cases are always welcome (see http://dotty.epfl.ch/docs/contributing/testing.html#integration-tests)

1 Like

These all produce a missing parameter type compiler error on s in Scala 2.12.6:

  def foo[T, U](bar: T, gazonk: T => U): U = ???
  def fooOpt[T, U](bar: Option[T], gazonk: T => U): U = ???
  def fooEither[T, U](bar: Either[T, U], gazonk: T => U): U = ???

  foo("Hello", s => s + " world")
  fooOpt(Option("Hello"), s => s + " world")
  fooEither(Left("Hello"), s => s + " world")

(don’t know if they work in dotty)

@jxtps Yes, they all work in Dotty :).

2 Likes

Here’s another similar one:

trait Transaction[T]

def inTransaction[T](body: Transaction[T] => T): T = ???

inTransaction { tx => 42 }
1 Like

When the compiler can’t infer a type argument, instead of using Nothing, it would be great if it could preserve it as “unknown.”

1 Like

@zygfryd With Dotty, you’ll at least get T to be inferred to Any, doing better requires solving the same problem I described in this comment

@nafg This is already the case in Dotty, see the talk and slides I linked to in my original post.

1 Like

In Monocle, we have issues around type inference for overloaded methods. Here is an example: https://github.com/julien-truffaut/Monocle/blob/master/example/src/test/scala/monocle/ComposeIssueExample.scala

It would dramatically improve the API, if overloaded methods had the same type inference support than normal methods.

Inference of inner functions types. I know it’s impossible in general, but it would be nice to have at least for cases like below.

def foo(x: Int, y: Int) = {
    val z = 5
    def inner(a, b) = a + b + z
    inner(x, y)
}
2 Likes

Implicit resolution and currying often leads to inference errors (or unintuitive behavior, at least).

sealed trait Do[A] {
  def something(a: A): Unit
}

case class Thing[T: Do](t: T)

case class SomeThing(a: Unit)
object SomeThing {
  implicit val doSomeThing: Do[SomeThing] = new Do[SomeThing] {
    def something(a: SomeThing): Unit = ()
  }
}

val someThings = List(SomeThing(()), SomeThing(()), SomeThing(()))

someThings.map(a => Thing(a)) // Compiles
someThings.map(Thing(_))      // Compiles
someThings.map(Thing)         // Does not compile
someThings.map(Thing.apply)   // Does not compile

I would expect all (or none) of the above to compile.

3 Likes

@julien-truffaut Looks like your example works in Dotty! Feel free to send a PR with a test case if you want to make sure we don’t accidentally regress on that one :).

@nau Besides the difficulty of inferring things correctly here, what you’re proposing is a language change and would have to go through the SIP process.

@tdidriksen All your examples compile with Dotty, except someThings.map(Thing) because Thing here is interpreted as being the companion object Thing of the case class, which makes sense to me.

6 Likes