Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala invariant generic type parameter ignored by method parameter type depending whether argument is literal expression versus variable

Summary

If I pass a literal expression as the argument to a function, shouldn't that be the same as first evaluating that same literal expression, then binding a variable to the value returned from that evaluation, and then passing the variable name as the same argument to the same function? And if that literal expression returns the wrong type for the function's parameter, then skipping the step of assigning the value to an intermediary variable whose type Scala infers from the return value of the expression should not make passing that incompatible type to the function possible where before it raised a type-mismatch error, should it? Yet is that not what the following example shows?

Example

Here I try to get a function parameter of type Array[Super] to accept a value of Array[Sub]. Declare the following in the Scala repl. Note the type of the function's single parameter:

class Super
class Sub extends Super
def wantsSuperArray(a: Array[Super]) { println(a.size) }

Then construct an instance of Sub

scala> val s = new Sub
s: Sub = Sub@2c9fa2fb

Create an Array[Sub]:

scala> val subArray = Array(s)
subArray: Array[Sub] = Array(Sub@2c9fa2fb)

The following would indicate that the generic Array is invariant in its element type, and that an Array[Sub] is not an Array[Super] even though a Sub is a Super:

scala> wantsSuperArray(subArray)
<console>:13: error: type mismatch;
 found   : Array[Sub]
 required: Array[Super]
Note: Sub <: Super, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Super`. (SLS 3.2.10)
              wantsSuperArray(subArray)
                              ^

No surprises so far.

Surprising Observation

We just saw that wantsSuperArray() will not take as an argument type an Array[Sub]. So why does the following not produce the same type-mismatch error message as above?

scala> wantsSuperArray(Array(new Sub))
1

Likewise, why raises this no errors?

scala> wantsSuperArray(Array(s))
1

Consider that the compiler treats the three variants consistently with the repl, namely refusing to compile and giving the same type-mismatch error for the first, and compiling the second and third.

An Additional Detail

If we expressly parameterize the Array as follows then the error message appears again:

scala> wantsSuperArray(Array[Sub](new Sub))
<console>:11: error: type mismatch;
 found   : Array[Sub]
 required: Array[Super]
Note: Sub <: Super, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Super`. (SLS 3.2.10)
              wantsSuperArray(Array[Sub](new Sub))
                                        ^

So, apparently when there is no intermediate variable involved, Scala can see what type wantsSuperArray wants and is doing some kind of conversion, perhaps casting from Array[Sub] to Array[Super]. Still, that seems like a gotcha, as I still think that the choice of whether or not to use an intermediate variable should not cause such a difference in a program's operation, and this particular situation seems to be performing a cast instead of raising an error the programmer would be expecting based on the invariance of the generic Array type parameter.

Questions

If I believe that in calling wantsSuperArray() as defined above, passing a literal expression ought to be the same as passing the name of a variable holding the value resulting from evaluating that same expression, as show above, then what am I misunderstanding?

  • How can I improve my understanding of what I am observing and complaining about here?

  • Where in the Scala documentation can I read about the phenomenon I am observing here so as to be able to understand it and never be surprised by it again?

like image 230
Adam Mackler Avatar asked Mar 24 '23 09:03

Adam Mackler


1 Answers

This is because scala performs type inference base on the expected type of wantsSuperArray parameter. So even though Array(new Sub) taken alone would be infered as an expression of type Array[Sub], the compiler sees that you are in the context where a value of type Array[Super] is expected, and thus when calling Array.apply (which is generic) it tries to use Super as the type parameter (instead of Sub), which correctly types (Array.apply takes a vararg list of parameters of type T, here T = Super and you pass an instance of Sub, which is a sub-type of Super, which is sound).


Here is a relevant extract from the scala specification, chapter 6.1: Expression Typing, chapter 6.6: Function applications and chapter 6.26.4: Local Type Inference (emphasis mine):

The typing of expressions is often relative to some expected type (1) (which might be undefined). When we write “expression e is expected to conform to type T ”, we mean: the expected type of e is T , and the type of expression e must conform to T .

...

An application f (e1, ..., em) applies the function f to the argument expressions e1, ..., em. If f has a method type (p1:T1, ..., pn:Tn)U, the type of each argument expression ei is typed with the corresponding parameter type Ti as expected type (2). Let Si be type type of argument ei (i in 1, ..., m). If f is a polymorphic method, local type inference is used to determine type arguments for f (3) .

...

If f is a polymorphic method it is applicable if local type inference can determine type arguments so that the instantiated method is applicable (4)*.

...

Local type inference infers type arguments to be passed to expressions of polymorphic type. Say e is of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T and no explicit type parameters are given ... If the expression e appears as a value without being applied to value arguments, the type arguments are inferred by solving a constraint system which relates the expression’s type T with the expected type pt (5)

Points (3) and (4) explain how in the expression Array(new Sub), type inference flows from new Sub to Array.apply to yield the type Array[Sub]. This is the "simple" case that you apparently had no problem with. If you just take this rule, Array(new Sub) should be typed as Array[Sub]. And indeed, it is what happens when it is typed in isolation (by example in val subArray = Array(new Sub), subArray does have the type Array[Sub]).

But points (1), (2) and (5) together also say that in wantsSuperArray(Array(new Sub)), the expected type of the parameter to wantsSuperArray (which is Array[Super]) is passed to the expression Array(new Sub) (because it is an expression of polymorphic type where the type parameter is not explictly given). Thus the expression Array(new Sub) is evaluated as the expression Array[Super](new Sub). In other words, it is typed as Array[Super].

like image 99
Régis Jean-Gilles Avatar answered Mar 26 '23 23:03

Régis Jean-Gilles