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

I stumbled on something when working with Serializable. Scastie link as requested.

This might be more of a type/implicit mismatch problem although intuitively (to me) both lines should work.

@arkban The problem is that scala.Serializable is its own type that extends java.io.Serializable, all serializable Scala classes extend scala.Serializable but Java classes don’t. Thankfully, this problem will go away in Scala 2.13 because scala.Serializable is now just a type alias for java.io.Serializable (same for Cloneable).

3 Likes

@smarter OMG that’s great news. I’ve had awful situations before due to scala.Serializable not just being a type alias.

Not sure if it applies, but given

sealed trait Color
object Color {
  case object Red extends Color
  case object Blue extends Color
}

def fun(n: Int) = n match {
  case 42 => Color.Blue
  case _ => Color.Red
}

fun is inferred as

fun: (n: Int)Product with Serializable with Color

It would be nice if it were inferred as

fun: (n: Int)Color

instead.

1 Like

@gabro The problem is that Color does not subtype Product nor Serializable, while (Color.Blue.type | Color.Red.type) does, a piece of information that widening the latter should arguably not lose.

Maybe we could make sealed traits and classes implicitly extend all the superclasses that are shared by all the subclasses of the sealed trait/class in question? I’m not sure whether this would create problems with linearization.

@gabro That one is tricky since it would mean inferring a less precise type in a somewhat arbitrary way. Instead you can make Color extends Product with Serializable which I concede isn’t very nice either. Alternatively if you use Dotty enums, then the type of the value Color.Blue would just be Color, which sidesteps the problem.

5 Likes

Is it ever a problem when that is inferred instead of just Color?

Maybe I should leave this suggestion to Olivier :-), but it seems to be there are quite a few asInstanceOf calls in https://github.com/OlivierBlanvillain/monadic-html/blob/master/monadic-rx/shared/src/main/scala/mhtml/rx.scala that could ideally be avoided. I also recall Olivier saying he got this to build on dotty a while back with minimal changes (in the associated gitter channel).

Not sure if this counts as type inference, but this is a fun one:

Welcome to Scala 2.12.6-20180516-151519-unknown (OpenJDK 64-Bit Server VM, Java 1.8.0_172).
Type in expressions for evaluation. Or try :help.

scala> trait F[A]
defined trait F

scala> implicit def f[A, C <: Iterable[A]]: F[C] = ???
f: [A, C <: Iterable[A]]=> F[C]

scala> implicitly[F[List[String]]]
<console>:14: error: could not find implicit value for parameter e: F[List[String]]
       implicitly[F[List[String]]]
                 ^

scala> implicit def f[A, C <: Iterable[A]]: F[C with Iterable[A]] = ???
f: [A, C <: Iterable[A]]=> F[C with Iterable[A]]

scala> implicitly[F[List[String]]]
scala.NotImplementedError: an implementation is missing
  at scala.Predef$.$qmark$qmark$qmark(Predef.scala:284)
  at .f(<console>:13)
  ... 28 elided

Only happens if F is invariant on A.

@jcracknell Looks like your example already works in Dotty too!

1 Like

Sorry, my message above should have been @smarter - need to be more careful when replying by email.

First, I want to thank Dotty for adopting Eq. This prevents the compilation of the following:

scala> 1 == false
1 |1 == false
  |^^^^^^^^^^
  |Values of types Int and Boolean cannot be compared with == or !=

scala> 2 == Option(2)
1 |2 == Option(2)
  |^^^^^^^^^^^^^^
  |Values of types Int and Option[Int] cannot be compared with == or !=

scala> Right(3)  == "something"
1 |Right(3)  == "something"
  |^^^^^^^^^^^^^^^^^^^^^^^^
  |Values of types Right[Nothing, Int] and String cannot be compared with == or !=

while allowing equality test for Right and Left:

scala> Right(1) == Left(2)
val res0: Boolean = false

I would like this notion to be extended to type inference as well. So, two types A and B that cannot be compared safely are also not mixed together.

def foo = if (true) 1 else false

def bar = 1 match {
  case 1 => 2
  case 2 => Option(2)
}

def baz = List(Right(3), "something")

Currently they all seem to compile. I would like them to not.

There might be situation where you do want to mix Right(3) and "something". That can require type ascription the same way == does:

scala> (Right(3): java.io.Serializable) == "something"
val res1: Boolean = false
1 Like

I am glad you like Eq in Dotty!

def foo = if (true) 1 else false

A more elaborate version of this could easily happen if we write a universal interpreter with Any as the representation type, or if we interface with dynamic languages. So I don’t think we want to exclude this.

Also, the only sane way to exclude this would be to completely remove Any and AnyRef as top types. But that would fly in the face of fundamental assumptions underlying Scala’s type system and virtually all existing code.

Error type in Cats EitherT.fromEither(Right(a)) stacked with other EitherTs that have the same error type is inferred as EitherT[F, Nothing, A]. Would be nice to have the error type inferred when specifying only Right() value but using it in a context.

@smarter any chance we could make type inference automatically pick up the fact that functions with default arguments can be eta-expanded to multiple arities? e.g. in the following case, I would like the compiler to be able to figure out the foo can be eta-expanded into an Int => Int by using the default value for the second parameter

@ def foo(x: Int, y: Int = 2): Int = x + y
defined function foo

@ Seq(1, 2, 3).map(foo)
cmd15.sc:1: type mismatch;
 found   : (Int, Int) => Int
 required: Int => ?
val res15 = Seq(1, 2, 3).map(foo)
                             ^
Compilation Failed
6 Likes

@lihaoyi There’s a related discussion at https://github.com/lampepfl/dotty/issues/4275 you might want to contribute to. Such a change would have to be done carefully while considering all possible interactions (e.g. what do you do if the expected type is (Int => Int) & ((Int, Int) => Int) ?)

@aksharp Could you write down a complete example illustrating the issue ?

This is in Dotty

scala> if (true) 1 else 2f
val res0: Float = 1.0

scala> if (true) Some(1) else Some(2f)
val res1: Some[Int | Float] = Some(1)

There is an inconsistency. Either the first case should have been inferred to Int | Float or the second one to Option[Float]

6 Likes

@ramnivas Currently in dotty, we alwaus widen unions at the top-level (that is, not inside a type parameter), because keeping unions tends to break existing code, it’s possible that we 'll change this behavior but doing so without breaking too much code is tricky, see https://github.com/lampepfl/dotty/pull/2330#issuecomment-298233273 for the last discussion we had on this subject. Your example also involves conversions between numeric types which is another can of worms, the current rules are explained at http://dotty.epfl.ch/docs/reference/dropped/weak-conformance.html, but may also need to be debated further (and probably go through the SIP process too?)

Starting dotty REPL...
scala> List(1L, 1F)                                                             
val res0: List[Double] = List(1.0, 1.0)
scala> List[Long|Float](1L, 1F)                                                 
1 |List[Long|Float](1L, 1F)
  |                 ^^
  |                 found:    Double(1.0)
  |                 required: Long | Float
  |                 
1 |List[Long|Float](1L, 1F)
  |                     ^^
  |                     found:    Double(1.0)
  |                     required: Long | Float
7 Likes