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

Did you mean this perhaps?

Sorry, yes. I should have double-checked his example instead of relying on my memory. Apologies.

@david-sledge Looks like your example already works in Dotty.

@smarter: The following code doesn’t work:

@main def app = {
  
  
  object Nil {
    override def toString = "Nil"
  }
  
  final case class NonEmptyList[+A](head: A, tail: List[A])
  
  type List[+A] = Nil.type | NonEmptyList[A]
  
  inline def [A, B >: A](ls: List[A]) :: (elem: B): List[B] = NonEmptyList(elem, ls) 

  val x : List[Int] = Nil
  
  val xs : List[Int] =  NonEmptyList(1, NonEmptyList(2, Nil))
  
  val ys : List[Int] = 1 :: 2 :: Nil
// Error:
// Found:    (2 : Int)
// Required: List[object Nil]
  
  println(xs)
}

Looks like an inference problem. Wdyt ?

I think you need to make that:

inline def [A, B >: A](elem: B) :: (ls: List[A]): List[B] = NonEmptyList(elem, ls) 
1 Like

Works. Thank you!

This was working prior to 0.24:

trait P[M: Ordering, C] {
  def apply(s: (M, C)*): Seq[(M, C)]
  def sort(x: Seq[(M, C)]) = apply(x.sortBy((s, _) => s): _*)
                                             ^
                 cannot infer type; expected type <?> is not fully defined
}

Can you open an issue for that? Also I suspect it can be minimized further (the use of a lambda whose expected type comes from a repeated parameter is probably the important part here)

6 posts were split to a new topic: Scala 2 type inference questions

This thread is specifically about improving Dotty’s type inference. For Scala 2 issues, I suggest opening an issue on github.com/scala/bug/issues

3 Likes

The following shapeless-style derivation of a polymorphic sequence operation for Option doesn’t infer a useful type in Scala 3.0.0-M1. The type is checkable but on failure the expected type is not useful.


object Blah {

  trait Options[T <: Tuple] {
    type Out <: Tuple
    def apply(t: T): Option[Out]
  }

  object Options {

    type Aux[T <: Tuple, O <: Tuple] = Options[T] { type Out = O }

    implicit def single[H]: Aux[Option[H] *: EmptyTuple, H *: EmptyTuple] =
      new Options[Option[H] *: EmptyTuple] {
        type Out = H *: EmptyTuple
        def apply(t: Option[H] *: EmptyTuple): Option[H *: EmptyTuple] =
          t.head.map(_ *: EmptyTuple)
      }

    implicit def multiple[H, T <: Tuple](
      implicit ev: Options[T]
    ): Aux[Option[H] *: T, H *: ev.Out] =
      new Options[Option[H] *: T] {
        type Out = H *: ev.Out
        def apply(t: Option[H] *: T) =
          (t.head.flatMap(h => ev(t.tail).map(t => h *: t)))
      }

  }

  extension [T <: Tuple](t: T)(using ev: Options[T]) {
    def sequence: Option[ev.Out] = ev(t)
  }

  // If we annotate this with an incorrect type it fails to typecheck and gives the inferred
  // type as Option[?1.Out] rather than the expected fully-expanded type.
  val x: Option[(Int, String, Boolean, Double)] =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence

}

Is there a better way to do this?

Maybe the pretty-printer could try harder to get rid of these path-dependent things (bug report welcome), but if we avoid the Aux pattern and just use type parameters the problem goes away too:

object Blah {
  trait Options[T <: Tuple, Out] {
    def apply(t: T): Option[Out]
  }
  // one can always define `type Foo[T <: Tuple] = Options[T, _]`
  // to hide the parameter.

  object Options {
    implicit def single[H]: Options[Option[H] *: EmptyTuple, H *: EmptyTuple] =
      new Options[Option[H] *: EmptyTuple, H *: EmptyTuple] {
        def apply(t: Option[H] *: EmptyTuple): Option[H *: EmptyTuple] =
          t.head.map(_ *: EmptyTuple)
      }

    implicit def multiple[H, T <: Tuple, Tail <: Tuple](
      implicit ev: Options[T, Tail]
    ): Options[Option[H] *: T, H *: Tail] =
      new Options[Option[H] *: T, H *: Tail] {
        def apply(t: Option[H] *: T) =
          (t.head.flatMap(h => ev(t.tail).map(t => h *: t)))
      }
  }

  extension [T <: Tuple, Out](t: T)(using ev: Options[T, Out]) {
    def sequence: Option[Out] = ev(t)
  }

  // OK:
  val x: Option[(Int, String, Boolean, Double)] =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence 

  // error: Found:    Option[Int *: String *: Boolean *: Double *: EmptyTuple]
  //        Required: String
  val y: String =
    (Option(1), Option("x"), Option(true), Option.empty[Double]).sequence 
}
1 Like

This is a pretty interesting gotcha I just stumbled upon: https://scastie.scala-lang.org/8r2pKMoXRyCk1wjUYeccHQ

Inlined:

def isTrue(b: Boolean): Boolean = b
val notTrue = !isTrue(_) // Missing parameter type\n\nI could not infer the type of the parameter _$1
2 Likes

I believe that is !((b: Boolean) => b), and since Function1 has no ! method, it gets lost. The error message is deceiving. I expect you intended it to expand like this:

val notTrue = (b: Boolean) => !isTrue(b)

Which compiles fine.

Actually, the error message suggests the expansion is as intended, but then the type inference still fails:


error: missing parameter type for expanded function ((<x$1: error>) => isTrue(x$1).unary_$bang)

Equivalent case without prefix operator:

> def m(i: Int): Int = i
def m(i: Int): Int

> val isFinite = m(_).isFinite
^

error: missing parameter type for expanded function ((<x$1: error>) => m(x$1).isFinite)
2 Likes

Probably moot since we will get Enums in Dotty, but thgis is one that frequently pops up for me and I find it annoying:

trait Error
case class SomethingBadHappened(msg: String) extends Error
case object UnexpectedError extends Error
//plus a few more 

def someIO: IO[String, Thing]

def newIO: IO[Error, Thing] = someIO.leftMap(SomethingBadHappened(_)) //expected Error but got SomethingBadHappened

def newIO: IO[Error, Thing] = someIO.leftMap(SomethingBadHappened(_):Error) //compiles fine

also may be missing something with some language design principle, but this seems like a failure of the compiler to me

3 Likes

Uncommenting line 6 works, but would be nice if compiler could infer these instead of Nothing?

1 Like

I wish I could write compose as

  def compose[G[_]: Functor]: Functor[F[G[_]]] =
    Functor.make[F[G[_]]](
      [A, B] => f => fga => map(fga)(Functor[G].map(_)(f))
    )

That’s not type inference specific though, just syntax. Same for value level functions. This won’t work as well.

def compose[A, B, C](f: A => B, g: B => C): A => C = f(g(_))