14

Why does a language like Scala, with a very strong static type system, allow the following constructions:

 scala> List(1, List(1,2))
 res0: List[Any] = List(1, List(1, 2))

The same thing works if you replace List with Array. I learned functional programming in OCaml, which would reject the same code at compile-time:

# [1; [1;2]; 3];;
Characters 4-9:
  [1; [1;2]; 3];;
      ^^^^^
Error: This expression has type 'a list
       but an expression was expected of type int

So why does Scala allow this to compile?

alifirat
  • 2,899
  • 1
  • 17
  • 33
  • 1
    Some additional links that may be useful for you: [Scala types hierarchy](http://www.scala-lang.org/old/node/128), [Heterogenous lists](https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#heterogenous-lists) from shapeless. – Aivean Aug 20 '15 at 09:41
  • 2
    Scala's type system only looks "strong" when you squint at it. Read about [Null](http://www.scala-lang.org/api/current/#scala.Null) and [heap pollution](https://en.wikipedia.org/wiki/Heap_pollution). – Chris Martin Aug 20 '15 at 09:46
  • 1
    @ChrisMartin where does either of the linked articles support your assertion? An explicit `Null` type (as well as the `Nothing` type, and primitive types fitting into the type hierarchy at all) is a clear improvement over Java. And it's much harder to pollute your heap when using Scala, because you don't need to work against the compiler as often - exactly because Scala's type system is richer than Java's. – Silly Freak Aug 20 '15 at 11:28
  • 1
    @SillyFreak "Strong" has many definitions (some of which Scala does satisfy) but "better than Java" is not one of them. – Chris Martin Aug 20 '15 at 11:32
  • 1
    @ChrisMartin there being a Null type doesn't make a type system weak, and heap pollution is IMO not a Scala problem, it's a Java one. As - in my eyes - you were only arguing against Java, I simply pointed out that you didn't address any weakness of Scala's type system. My original question stands: "where does either of the linked articles support your assertion?" – Silly Freak Aug 20 '15 at 11:41
  • @SillyFreak Links were not intended as citations. – Chris Martin Aug 20 '15 at 18:55
  • @ChrisMartin The intended guarantee in Scala/Java is that code that compiles without unchecked warnings and which doesn't use casts won't fail with ClassCastException. – Blaisorblade Oct 12 '16 at 17:14

2 Answers2

19

tl;dr

Long story short, OCaml and Scala use two different classes of type systems: the former has structural typing, the latter has nominal typing, so they behave differently when it comes to type inference algorithms.


Full discussion

If you allow nominal subtyping in your type system, that's pretty much what you get.

When analyzing the List, the Scala compiler computes the type as the LUB (least upper bound) of all types that the list contains. In this case, the LUB of Int and List is Any. Other cases would have a more sensible result:

@ List(Some(1), None)
res0: List[Option[Int]] = List(Some(1), None)

The LUB of Some[Int] and None is Option[Int], which is usually what you expect. It would be "weird" for the user if this failed with:

expected List[Some[Int]] but got List[Option[Int]]

OCaml uses structural subtyping, so its type system works differently when it comes to type inference. As @gsg pointed out in the comments, OCaml specifically doesn't unify the types like Scala, but requires an explicit upcast.

In Scala, the compiler unifies the types when performing type inference (due to nominal subtyping.)

Of course you can get much better errors with explicit type annotations:

@ val x: List[Int] = List(1, List(1, 2))
Compilation Failed
Main.scala:53: type mismatch;
 found   : List[Any]
 required: List[Int]
}.apply
  ^

You can get warnings whenever the compiler infers Any - which is usually a bad sign - using the -Ywarn-infer-any flag. Here's an example with the scala REPL:

scala -Ywarn-infer-any
Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_51).
Type in expressions to have them evaluated.
Type :help for more information.

scala> List(1, List(1, 2))
<console>:11: warning: a type was inferred to be `Any`; this may indicate a programming error.
       List(1, List(1, 2))
            ^
res0: List[Any] = List(1, List(1, 2))
Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
  • Thank you for the explanations, it's more clear about for me about these situations. – alifirat Aug 20 '15 at 09:51
  • 2
    OCaml supports subtyping, but avoids the inference problem by requiring explicit downcasts. – gsg Aug 20 '15 at 10:52
  • @gsg oh thanks, that's interesting. That definitely proves I don't know OCaml :) – Gabriele Petronella Aug 20 '15 at 10:53
  • @gsg I did my homework and tweaked the answer a little. Thanks for the heads up – Gabriele Petronella Aug 20 '15 at 11:04
  • 1
    Strictly speaking, OCaml doesn't allow any downcasts at all. It only allows upcasting, but even upcasting must be explicit. The main difference, is that Scala uses "local type inference" in comparison with real type inference systems, used in Haskell and OCaml, based on Hindley-Milner type system. The OP example, is undecidable in HM type system. That's the reason why Scala sticks with local type inference. – ivg Aug 20 '15 at 14:01
  • @gsg it's an upcast, not a downcast. –  Aug 20 '15 at 14:20
  • Beware: "upcasting must be explicit" is orthogonal to nominal or structural subtyping. – Blaisorblade Oct 12 '16 at 17:08
  • @Blaisorblade interesting point. So I guess this means you can compute the LUB of a structural type too? – Gabriele Petronella Oct 12 '16 at 17:19
  • @GabrielePetronella It probably depends on other details, but in principle yes and it should be typically easier. In fact, Scalac *cannot* compile exact LUBs in a few situations, they must be approximated. To fix that, Dotty added union types like `A | B`, the exact LUB of `A` and `B`. – Blaisorblade Oct 12 '16 at 18:48
8

Since Scala allows implicit subtyping, it is able to infer the "correct" type for such expressions with mixed contents. Scala correctly infers that your list is of type List[Any], meaning anything can occur within it.

Since Ocaml does not support implicit subtyping without explicit downcast; it is not able to automatically widen the type for mixed lists.

Most often, if you end up with type Any or AnyRef, you have messed something, but it can also be the right thing in some situations. It is up to the programmer to decide whether a more stringent type is required.

ziggystar
  • 28,410
  • 9
  • 72
  • 124
  • Thank you for the explanations, it's more clear about for me about these situations. – alifirat Aug 20 '15 at 09:51
  • Another type that gets often inferred when you accidentally mixed some types that you didn't want to, is `java.lang.Serializable`. – Jörg W Mittag Aug 20 '15 at 10:28
  • 1
    again, OCaml supports subtyping. That's the reason for having `O` in its name. It's just prefers explicit over implicit. – ivg Aug 20 '15 at 10:57