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

This was working in 2.x:

val a = List(Byte.MinValue, Byte.MaxValue, Short.MinValue, Short.MaxValue, Int.MinValue, Int.MaxValue)
val b: List[Int] = a
                   ^
                   Found:    (a : List[AnyVal])
                   Required: List[Int]

That one is documented in the Scala 3 Reference.

2 Likes

At the bottom of the page, it mentions “the one rule to rule them all.” (Not a direct quote.)

Internally, they call it “harmonization” of constants written in source.

Also those are constants:

scala> val xs: List[Int] = List(Byte.MinValue, Int.MinValue)
val xs: List[Int] = List(-128, -2147483648)
1 Like

I often find myself in the following situation, where the compiler is not able to infer the correct type of a partial function. Now, i am not knowledgeable enough to judge if this is behaviour as expected, but it would certainly be nice if inference would work. The code

/* From Akka (simplified): */
object Actor :
  type Receive = PartialFunction[Any, Unit]

trait Actor :
  type Receive = Actor.Receive
  def receive: Receive


/* Different use cases: */
object MyActor :
  case class Text(value: String)
  case class Number(value: Int)
  def common: Actor.Receive = { case Text(msg) => println(s"msg=$msg") }

class MyActor1 extends Actor :
  import MyActor.*
  def receive = { case Number(nr) => println(s"nr=$nr") }

class MyActor2 extends Actor :
  import MyActor.*
  def receive = common orElse { case Number(nr) => println(s"nr=$nr") }

class MyActor3 extends Actor :
  import MyActor.*
  def receive = { case Number(nr) => println(s"nr=$nr") } orElse common

class MyActor4 extends Actor :
  import MyActor.*
  def receive = ({ case Number(nr) => println(s"nr=$nr")}: Receive) orElse common

Examples MyActor1 and MyActor2 work as expected. At MyActor3 the compiler complains:

Missing parameter type

I could not infer the type of the parameter x$1 of expanded function:
x$1 => 
  x$1 match 
    {
      case Number(nr) => 
        println(s"nr={nr}")
    }.
Not found: nr

so you need to explicitly tell it to have the type Receive as in MyActor4. However, this seems unnecessary clutter to me. Or not?

Anonymous class inference in Scala 3 is really great on Scala.js to avoid having facades invented names in your code as described in this video by @sjrd.
However, with nested types using js.UndefOr the compiler can’t infer types anymore, and you need to add the trait name to make it compile. Here a minimal example using scala-cli:

//> using platform "scala-js"
//> using scala "3.2.1"

import scala.scalajs.js

trait Bar extends js.Object:
  var string: js.UndefOr[String] = js.undefined

trait Foo extends js.Object:
  var bar: js.UndefOr[Bar] = js.undefined

def f(foo: Foo) = new:
  var bar = new:
    string = "hello"

It fails to compile with:

[error] ./example.scala:14:5: Not found: string
[error]     string = "hello"
[error]     ^^^^^^

And compiles if I change new: with new Bar:

def f(foo: Foo) = new:
  var bar = new Bar:
    string = "hello"

Here is some code that compiles in scala2, but due to inference differences does not compile in scala3

import play.api.libs.ws._

//Class with overloaded methods
trait CustomResponse {
  def body: String
  def body[T: BodyReadable]: T
}

//copied from specs2
class MustExpectable[T] {
  def must_==(other: =>Any): Any = ???
  def must_===(other: =>T): Any = ???
}
trait MustExpectations {
  import scala.language.implicitConversions
  implicit def theValue[T](t: => T): MustExpectable[T] = ???
} 

object Test extends MustExpectations {

  val r: CustomResponse = ???

  //explictly calling the implicit conversion works and `body:String` is inferred
  theValue(r.body).must_===("")
  //if not, `body[T: BodyReadable]: T` is inferred 
  r.body.must_===("")
}

scala3 vs scala2

I know I can make this code compile by bringing the BodyReadable[String] into scope, but I would like to avoid that since it will require changes in a lot of files (see play PR where I faced this problem WIP: Scala3 remaining modules by jtjeferreira · Pull Request #11551 · playframework/playframework · GitHub)

Is this a bug in scala3 inference? Can it be improved?

PS: I also raised this topic in Discord where I was referred to Guillaume Martres—Scala 3, Type Inference and You! - YouTube and this thread

Just an addendum:

import annotation.*

trait StandaloneWSResponse

@implicitNotFound("Cannot find an instance of StandaloneWSResponse to ${R}. Define a BodyReadable[${R}].")
class BodyReadable[+R](val transform: StandaloneWSResponse => R)

and as noted on discord, x compiles, y does not:

  val x = r.body
  x.must_===("")
  val y: Any { def must_===(s: String): Any } = r.body

The implication is that inferring T for the conversion (badly) constrains overload resolution of body.

There are a few open tickets on overloading intersecting with inference and default args, that unholy triumvirate. For example. Maybe it’s just the way errors are handled during overload resolution.

A case I hit a lot is that the typechecker cannot reason with equations of the form

F[A] =:= G[B]

Here is a (contrived) failing example:

enum Expr[T] {
  case Maybe[A](value: Option[A]) extends Expr[Option[A]]
}

def go[F[_], A](expr: Expr[F[A]]): Unit =
  expr match {
    case _: Expr.Maybe[a] =>
      summon[F[A] =:= Option[a]] // Cannot prove that F[A] =:= Option[a].
  }

Working around these cases adds a lot of accidental complexity. Here’s a technique I use for coping with these cases, which involves manually managing type equalities:

// helper type to go into myproject.util package
sealed trait Masked[F[_], A] {
  type X
  val value: F[X] // hiding that value is of type F[A]
  def ev: X =:= A // gives a way of manually recovering F[A] from F[X]

  def visit[R](f: [T] => (F[T], T =:= A) => R): R =
    f[X](value, ev)
}

object Masked {
  def apply[F[_], A](fa: F[A]): Masked[F, A] =
    new Masked[F, A] {
      override type X = A
      override val value: F[X] = fa
      override def ev: X =:= A = summon
    }
}


enum Expr[T] {
  case Maybe[A](value: Option[A]) extends Expr[Option[A]]

  def mask: Masked[Expr, T] =
    Masked(this)
}

def go[F[_], A](expr: Expr[F[A]]): Unit =
  expr.mask.visit([T] => (e: Expr[T], ev: T =:= F[A]) => {
    e match {
      case _: Expr.Maybe[a] =>
        val ev1: F[A] =:= Option[a] =
          ev.flip andThen summon[T =:= Option[a]] // OK, but deriving manually
    }
  })

js.UndefOr[T] in Scala 3 is equivalent to T | Unit, so it seems that your suggestion is equivalent to allowing the following to compile:

trait Foo:
  def elem: Int

def foo: Foo | Unit = new:
  def elem = 1

I think this could be confusing since syntactically, there’s no reason why this should expand to new Foo as opposed to new Unit. Semantically we could exclude Unit because it’s final, so it shouldn’t be too hard to implement, but you might want to open a separate thread to check if people think this is controversial or not.

Hmm, based on how GADT reasoning is implemented in the compiler this seems tricky to support, since we constrain type symbols, not arbitrary types like F[A]. The following work around seems to work:

enum Expr[T] {
  case Maybe[A](value: Option[A]) extends Expr[Option[A]]
}

def go[F[_], A](expr: Expr[F[A]]): Unit =
  def go2[T](expr: Expr[T])(using F[A] =:= T): Unit =
    expr match {
      case _: Expr.Maybe[a] =>
        summon[F[A] =:= Option[a]] // OK.
    }
  go2(expr)

I’ve added a comment to No implicit argument of type F[T] was found (works in Scala 2) · Issue #15210 · lampepfl/dotty · GitHub based on your reproduction and @som-snytt’s comment.

The following codes compiles without the overload construct method:
https://scastie.scala-lang.org/ZyMfxeh7TMaMYTelMGDrcA

class Test(map: Map[Seq[?], String => Int]) {
  def this(seqs: Seq[?]*) = this(seqs.map(seq => (seq, (_: String) => 0)).toMap)
}


// this line compiles if remove the `def this(seqs: Seq[?]*) = ...`
val test = Test(Map((Seq(1, 2), s => s.toInt))) 


with error message:

Missing parameter type

I could not infer the type of the parameter s.
None of the overloaded alternatives of method apply in object Test with types
 (seqs: Seq[?]*): Playground.Test
 (map: Map[Seq[?], String => Int]): Playground.Test
match arguments (Map[K, Any])

where:    K is a type variable with constraint >: Seq[Int]
1 Like

Could it be possible to infer the literal type in code like this?:

class Sample[T <: Tuple](t: T)
object Sample:
  def apply[T <: Tuple](t: T)(using ev: Tuple.Union[T] <:< String with Singleton) = new Sample(t)
val sample = Sample(("a", "b"): ("a", "b"))

It would be great to be able to type just:

class Sample[T <: Tuple](t: T)
object Sample:
  def apply[T <: Tuple](t: T)(using ev: Tuple.Union[T] <:< String with Singleton) = new Sample(t)
val sample = Sample(("a", "b"))

Could it be possible to infer the literal type in code like this?:

See SIP-48 - Precise Type Modifier by soronpo · Pull Request #48 · scala/improvement-proposals · GitHub

Thank you for pointing this out. Interesting proposal.

So I just experienced a type inference issue unique to Scala 3 when dealing with custom function interfaces.

In pekko we had defined our own types of functions, i.e.

/**
 * A Function interface. Used to create first-class-functions is Java.
 * `Serializable` is needed to be able to grab line number for Java 8 lambdas.
 * Supports throwing `Exception` in the apply, which the `java.util.function.Function` counterpart does not.
 */
@nowarn("msg=@SerialVersionUID has no effect")
@SerialVersionUID(1L)
@FunctionalInterface
trait Function[-T, +R] extends java.io.Serializable {
  @throws(classOf[Exception])
  def apply(param: T): R
}

from incubator-pekko/Function.scala at 4ac0f00a477873965ee7d52e16faefb1de91fe3a · apache/incubator-pekko · GitHub

When trying to call the following function

def mapConcat[T](f: function.Function[Out, _ <: java.lang.Iterable[T]]): javadsl.Source[T, Mat] =
  new Source(delegate.mapConcat(elem => Util.immutableSeq(f.apply(elem))))

from incubator-pekko/Source.scala at 07df6071928f6f12dafb01c78d792fa26cd5b077 · apache/incubator-pekko · GitHub

using lambda syntax that returns a java.util.List i.e.

streamingPullResult
  .mapConcat((response: StreamingPullResponse) => response.getReceivedMessagesList)

You get the following error

[error] -- [E007] Type Mismatch Error: /Users/mdedetrich/github/incubator-pekko-connectors/google-cloud-pub-sub-grpc/src/main/scala/org/apache/pekko/stream/connectors/googlecloud/pubsub/grpc/javadsl/GooglePubSub.scala:75:21 
[error] 75 |          .mapConcat((response: StreamingPullResponse) => response.getReceivedMessagesList)
[error]    |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |         Found:    com.google.pubsub.v1.StreamingPullResponse =>
[error]    |           java.util.List[com.google.pubsub.v1.ReceivedMessage]
[error]    |         Required: org.apache.pekko.japi.function.Function[
[error]    |           com.google.pubsub.v1.StreamingPullResponse, ? <: Iterable[T]]
[error]    |
[error]    |         where:    T is a type variable with constraint 
[error]    |
[error]    | longer explanation available when compiling with `-explain`

Manually specifying the pekko.japi.function.Function function type, i.e.

streamingPullResult
  .mapConcat(
    ((response: StreamingPullResponse) =>
          response.getReceivedMessagesList): pekko.japi.function.Function[StreamingPullResponse,
      java.util.List[ReceivedMessage]])

Solves the issue

On Scala 2.12/2.13 this workaround is not necessary. Also see enable scala3 build for more google connectors by pjfanning · Pull Request #165 · apache/incubator-pekko-connectors · GitHub

I don’t have any code where that would be useful, but the compiler fails in cases where “duck inference” could succeed:

sealed abstract class Animal
case class Duck(quack: String) extends C
case class NonDuck(nonQuack: String) extends C

def foo[A <: Animal](f: A => String) = "works"

foo(x => x.quack) // error: value quack is not a member of Animal

foo[Duck](x => x.quack) // works

Of course this is dangerous, what should we do with extension methods ?
Should adding a Animal.quack in scope change the inference ?
Probably yes, but that’s a bit weird ?

I also found this signature change works, but I guess it doesn’t make sense for a java dsl:

def mapConcat[F[X] <: java.lang.Iterable[X], T](f: function.Function[Out, F[T]]): Source[T, Mat] =

I also opened Type inference fails with SAM and wildcard subtyping, works in 2.13 · Issue #18096 · lampepfl/dotty · GitHub with a minimisation

2 Likes

So I found another issue when adding Scala 3 support to pekko-persistence-jdbc i.e. Add Scala 3 support by mdedetrich · Pull Request #44 · apache/incubator-pekko-persistence-jdbc · GitHub. This is in regards to Scala 3’s cyclic import detection. I have noticed this before and although in some cases there are workarounds i.e.
https://github.com/mdedetrich/incubator-pekko-persistence-jdbc/blob/8e9b0129fe448b3601db1386f3cbaabf499ebda6/core/src/main/scala/org/apache/pekko/persistence/jdbc/state/scaladsl/JdbcDurableStateStore.scala#L50 complains with

[error] -- [E046] Cyclic Error: /Users/mdedetrich/github/incubator-pekko-persistence-jdbc/core/src/main/scala/org/apache/pekko/persistence/jdbc/state/scaladsl/JdbcDurableStateStore.scala:50:1 
[error] 50 |@ApiMayChange
[error]    | ^
[error]    | Cyclic reference involving val <import>
[error]    |
[error]    | longer explanation available when compiling with `-explain`

And this can be fixed by just changing it to a FQCN import i.e.

@pekko.annotation.ApiMayChange

There is another case of E046 at https://github.com/mdedetrich/incubator-pekko-persistence-jdbc/blob/8e9b0129fe448b3601db1386f3cbaabf499ebda6/core/src/main/scala/org/apache/pekko/persistence/jdbc/state/scaladsl/DurableStateSequenceActor.scala#L118 which I have no idea how to change (in fact it doesn’t look cyclic at all).

The current implementation of the cyclic import detection either seems to be brittle (in the case of `@ApiMayChange annotation, doesn’t seem like it can handle nesting well) or its a case of a spurious error due to other compilation problems (that branch isn’t fully compiling yet)?

No idea what’s going on, but that’s a bug and not a request for type inference enhancement, so it should go to Issues · lampepfl/dotty · GitHub