Type parameter unexpected behavior (at least to me)

Example on scastie:

case class Agg[T] private (
  acc: List[T]
) {
  def add(data: T): Agg[T] =
    copy(acc = data :: this.acc)
}

object Agg {
  def create[T](data: T): Agg[T] =
    Agg(acc = List(data))
}

val agg = Agg
  .create("first")
  .add("second")
  .add(10)

Hi there, in this example agg inferred type is Agg[String | Int], I was expecting a compiler error on add(10)once T is string in my mind add[T]should be equivalent to add(data: String).

What I miss here?

If you split it up like:

case class Agg[T] private (acc: List[T]) :
  def add(data: T): Agg[T] = copy(acc = data :: this.acc)

object Agg :
  def create[T](data: T): Agg[T] = Agg(acc = List(data))

val agg = Agg
  .create("first")
  .add("second")
  .add(10)

val agg1 = Agg.create("first")
val agg2 = agg1.add("second")
val agg3 = agg2.add(10)

you get the compiler error on val agg3 you expected i suppose:

Found:    (10 : Int)
Required: String

It seems that the type inference can work over the whole expression at once. Interesting, i did not know this.

2 Likes

Me neither, it continues weird to me. It looks like a really complex feature on compiler

Without being hindered with much knowledge of the internals of the compiler, i would assume finding the most specific type T for an expression like Agg.create[T]("first": T).add("second": T).add(10: T) is fairly common.

Yes, AFAIK, that is how it works in Scala 3, it uses the whole expression to infer types.

That fixes the usual complaint of Scala 2 for inferring the types of multiple-parameter lists functions. The common example being foldLeft:

val result = data.foldLeft(Nil) { case (acc, x) => ... }

Where acc was inferred to be Nil.type and thus the result could only be Nill.type.

Nonetheless, it now introduced new complaints like this one.
Or similar situations where folks expect the compiler to rather reject code instead of inferring a valid type.


BTW, this reminded me that a few weeks ago, someone had a similar question: https://stackoverflow.com/questions/79876779/unexplained-scala-compiler-behavior-3-7-4
But, in this case, they would like an even more aggressive type inference algorithm.

This just shows that it is impossible to make type inference to always do what everyone wants; it has to have trade-offs, not only for practical reasons but also between the spectrum of strictness and flexibility.
And then we add into the mix union types, and subtyping, and this becomes even more complicated.

5 Likes

@BalmungSan I just have posted on that topic too, looks like that he was expecting some behavior that I was expecting. I mean, not infer based over the full expression. Furthermore, I agree with him, current behavior sounds like a bug to me.
In my point of view, if the user wants a wider type, he/she must put the type explicitly, I mean:

val agg: Agg[Int | String] = Agg.create(“first”).add(“second”).add(10)

My approach for foldLeft case is always:

val result = data.foldLeft(List.empty[T]) { case (acc, x) => ... }

Well, that would be the Scala 2 behavior, which, as I said, was heavily criticized for having to specify type parameters.

Also, for the record, what the StackOverflow OP wants is consistent behavior regardless of how the expression is written. And, I think the only real way to accomplish that is by having full global type inference, but I could be wrong.

I agree that the behaviour is confusing and frustrating.
But I am just saying that it is not a bug; it is working as expected by the definition of the algorithm. Furthermore, I am saying that no matter what specification you have for the algorithm, someone will find a situation where that behaviour would be inconvenient.

Sure, I agree in principle, but again, that is a slippery slope.
First, that was the usual solution for the Scala 2 behavior, and while I didn’t have an issue with doing List.empty[Foo] over Nil, a lot of people did.
Second, and more importantly, the code you shared would still be incorrect in that version, it would rather need to be: val agg = Agg.create[String | Int](“first”).add(“second”).add(10), because you need the wider type at the start of the chain, not at the end.
Third, I could just exaggerate to the absurd, and say that just never let the compiler infer types, and just type everything yourself.


Anyways, again, I am not trying to say anyone is wrong here, nor defend the current behavior.
I am just pointing out why it is the way it is, and why it is impossible to get it “right”.

But, if you ask me, yes, I kind of prefer the Scala 2 variant.
And I would not be against a petition to change the Scala 3 version… but that would never happen, it would likely break existing code and cause more confusion.

2 Likes

Thank you guys for responses, I really appreciate the enlightenments.

Type inference is in the FAQ. (Thanks @BalmungSan for helping there.)

The linked SO is also about why does it matter if the expression is split up? The example is about the Scala 2 stumbling block.

When Seth Tisue linked to that SO from a ticket in 2013, it was already hoary enough for him to say:

I think this is just another version of this old chestnut

which is to say, it was already ancient lore at that time.

2 Likes