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: videoslides)
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!
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)
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:
@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 :).
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.
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")
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.
@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.