Reputation: 9761
Defining the following structure:
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[A](head: A, tail: List[A]) extends List[A]
and the following function (please note that the second line which is the type check error spot is also an algorithmic mistake for short-circuiting the result, but nonetheless it came up and i wonder why type checking is not spotting it)
def foldRight[A, B] (list: List[A], z: B)(f: (A,B) => B ): B = list match {
case Nil => z
case Cons(0, _) => z // problematic line
case Cons(s, xs) => f(s, foldRight(xs, z) (f))
}
I know scala also suffer from Type Erasure, but i am surprised that in this case, this can't be detected at compilation time ? Cons
is Cons[A]
in this case, and z
is clearly of type B
?
Any reason why such code actually compile ?
Sounds like there is a lengthy explanation here https://gist.github.com/jkpl/5279ee05cca8cc1ec452fc26ace5b68b, but Long read. If someone could make it simpler :)
Upvotes: 0
Views: 206
Reputation: 51658
Why these or those decisions about language design were made is opinion based.
In Scala spec it's written:
8.3.2 Type parameter inference for constructor patterns
Assume a constructor pattern 𝐶(𝑝1,…,𝑝𝑛) where class 𝐶 has type parameters 𝑎1,…,𝑎𝑛. These type parameters are inferred in the same way as for the typed pattern (_: 𝐶[𝑎1,…,𝑎𝑛]).
So for pattern Cons(h, t)
type parameter A
in Cons[A](h, t)
is inferred as if the pattern were _: Cons[A]
(which becomes _: Cons[_]
at runtime because of erasure).
There is example there in the spec how types are inferred for
class Term[A]
class Number(val n: Int) extends Term[Int]
def f[B](t: Term[B]): B = t match {
case y: Number => y.n
}
It's explained there why for the pattern y: Number
type parameter B
(new type parameter B
) is inferred Int
.
Similarly, in our case for the pattern Cons(0, _)
(as if it were _: Cons[A]
) A
is inferred Int
.
After typer
phase (scalacOptions ++= Seq("-Xprint:typer", "-Xprint-types")
) the code becomes
def foldRight[A, B](list: App.List[A], z: B)(f: (A, B) => B): B = list{App.List[A]} match {
case App.this{App.type}.Nil{App.Nil.type} => z{B}
case (head: A, tail: App.List[A]): App.Cons[?A1](0{Int(0)}, _{App.List[A]}){App.Cons[?A1]} => z{B}
case (head: A, tail: App.List[A]): App.Cons[?A2]((s @ _{A}){A}, (xs @ _{App.List[A]}){App.List[A]}){App.Cons[?A2]} => f.apply{(v1: A, v2: B): B}(s{A}, App.this{App.type}.foldRight{[A, B](list: App.List[A], z: B)(f: (A, B) => B): B}[A, B]{(list: App.List[A], z: B)(f: (A, B) => B): B}(xs{App.List[A]}, z{B}){(f: (A, B) => B): B}(f{(A, B) => B}){B}){B}
}{B}
and after erasure
(scalacOptions += "-Xprint:erasure"
) it becomes
def foldRight(list: App$List, z: Object, f: Function2): Object = {
<synthetic> var rc9: Boolean = false;
<synthetic> <stable> var x2: App$Cons = (null: App$Cons);
{
case <synthetic> val x1: App$List = list;
case11(){
if (App$Nil.==(x1))
matchEnd10(z)
else
case12()
};
case12(){
if (x1.$isInstanceOf[App$Cons]())
{
rc9 = true;
x2 = (x1.$asInstanceOf[App$Cons](): App$Cons);
{
<synthetic> val p3: Object = x2.head();
if (scala.Int.box(0).==(p3))
matchEnd10(z)
else
case13()
}
}
else
case13()
};
case13(){
if (rc9)
{
val s: Object = x2.head();
val xs: App$List = x2.tail();
matchEnd10(f.apply(s, App.this.foldRight(xs, z, f)))
}
else
case14()
};
case14(){
matchEnd10(throw new MatchError(x1))
};
matchEnd10(x: Object){
x
}
}
};
Actually, code similar to yours compiles even in Haskell if we add the information that a type parameter a
belongs to type classes Eq
and Num
to the context of function signature
data List a = Nil | Cons a (List a)
foldRight :: (Eq a, Num a) => List a -> b -> (a -> b -> b) -> b
foldRight list z f = case list of
Nil -> z
Cons 0 _ -> z
Cons s xs -> f s (foldRight xs z f)
Type classes are not first-class citizens in Scala. So Scala can't request similar thing about something like Num
(in Scala everything can be compared with ==
, so Eq
is "automatical").
Upvotes: 3
Reputation: 27356
Cons
isCons[A]
in this case
This is not correct, or at very least it is confusing. There are two types A
here: the type parameter in Cons[A]
and the type parameter in foldRight[A,B]
. There is nothing to say that they are the same type. You could equally well define
case class Cons[Z](head: Z, tail: List[Z]) extends List[Z]
So on the problematic line
case Cons(0, _) => z // problematic line
the compiler treats this as Cons[Int](0, _)
because the type of head
is Int
. The compiler is promiscuous in this case and allows this match
in all cases even though it can only succeed if foldLeft
is called with a List[Int]
.
Upvotes: 0