Hello!
Let’s have such an example in Scala 3.7.2
extension [T](t: T) def m(v: T): T = v
val a = 5.m("str")
Why the type of a is inferred to Int | String? (IntelliJ thinks, it’s an Int :/)
The same with implicit class, but in Scala 2 works “as intended”, I mean: how to require the same type of v parameter as the one of t value?
The way you’ve written it, you’re asking the compiler to find a T that makes whatever you write work. And 5 is an Int | String, and so is "str", so the compiler succeeded!
If you want instead to ask whether the two types are the same, you can do something like
extension [T](t: T)
def m[M](m: M)(using ev: M =:= T): T = ev(m)
which produces
scala> 5.m("str")
-- [E172] Type Error: ----------------------------------------------------------
1 |5.m("str")
| ^
| Cannot prove that String =:= Int.
1 error found
Or, alternatively, if it’s very short, you can inline it:
extension [T](t: T)
inline def im[M >: T](m: M): T = inline m match
case tm: T => tm
which gives a less-friendly error like
scala> "eel".im(5)
-- Error: ----------------------------------------------------------------------
1 |"eel".im(5)
|^^^^^^^^^^^
|cannot reduce inline match with
| scrutinee: 5 : (5 : Int)
| patterns : case tm @ _:String
|-----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$8:2
2 | inline def im[M >: T](m: M): T = inline m match
| ^
3 | case tm: T => tm
-----------------------------------------------------------------------------
1 error found
or you can add case _ => compiletime.error("Type mismatch") if you just want it to give that single error.
scala> 5.mi("eel")
-- Error: ----------------------------------------------------------------------
1 |5.mi("eel")
|^^^^^^^^^^^
|Type mismatch
1 error found
Either way, the key concept here is to ask the compiler to independently bind the two types to type variables, and then require that they be the same.
4 Likes
Thanks, I understand. Why implicit classes work the same? And may somebody give me the ref, when it was changed? I read many Scala 2 to 3 migration tutorials, blogposts etc. and I never met the change of implicit class resolution. Or, is it strongly connected with union types inference?
While extension methods mostly replace implicit classes, they’re not the same thing. And of course, union types simply didn’t exist in Scala 2, so this wasn’t possible then.
So I think the most honest answer is, “It’s just different”. You’ve got something of an edge case here – most tutorials probably didn’t even think to mention it.
(The usage of =:= that @Ichoran mentions was entirely possible in Scala 2, but it probably wasn’t necessary for the implicit-class idiom that I assume you’re translating from, because of the different semantics.)
1 Like
With one exception, extension types have the exact same behavior as if you rewrote
extension [E](e: E) def foo...
as
def foo[E](e: E)...
and called it with foo(e)... instead of e.foo.... In fact, you are permitted to call extension methods that way, because that is exactly how they are implemented!
Because of this, it works according to the type resolution mechanisms for individual methods, rather than the two-stage implicit class / method on that class mechanism. However, because multiple type parameter blocks are now allowed, and the rules for those aren’t exactly the same as when they are in a single block, you may not be able to recover the identical Scala 2 behavior. It will be very close, though.
The one exception is that Scala 3 will dispatch off the type of the receiver (first argument) instead of shadowing the method name, which makes it act like it belongs to the class it’s extending. Originally it didn’t work that way, but that made extension methods extremely brittle for the same reason that it would be extremely brittle if different classes couldn’t have the same method names–import too much (or someone changes something) and now everything is broken!
Therefore, it was changed to act less like a method (ordinary methods still shadow each other) and more like a method on the class that is its first argument.
2 Likes
Thanks for your replies. I understand how extensions are desugared. I was just confused, why such code compiles in Scala 3 
implicit class X[T](t: T) {
def m(v: T): T = v
}
5.m("str")
It seems the compiler treat extension methods and curried functions differently. And also, generics inferences are not done from left to right.
extension[T](x: T)
def m(y: T) = y
def foo[T](x: T)(y: T) = y
5.m("q")
val x = 5
x.m("q")
foo(5)("q") // Does Type infer based on the whole expression?
foo.apply(5).apply("q")
val inter = foo(5)
inter("q") // only this line triggers a compilation error.
Interesting, it seems so when it comes to Union types (I suppose Union types provide a good, easy way to “widen” the type accurately?)
It does make some sense; when we have foo(5) we get the possible T = Int but there is no reason (at that moment) to widen it to Int | Any or ? >: Int or something like that… So the union at foo(5)("q") seems like a “sensible, useful special case decision”. I can appreciate it, even though it seems “irregular”. I wonder how it was back in Scala 2 (which had no union types)?
Anyway, it never ceases to amaze me how much everyone avoids providing type arguments like it’s the plague
I changed my frame of mind about this; I don’t rely too much on type inference (I know it’s undecidable and cannot read my mind; so the compiler has to make some “choices”), I provide the types usually.
1 Like
It varies for me, but it’s fascinating to look at some of the code I wrote a dozen years ago and realize that I used type ascriptions far less often then than I do now.
Type inference is a Sometimes Food…
1 Like