Re: The Myth Makers 1: Scala's "Type Types"
Posted: Dec 18, 2008 5:44 PM
> > This is a bit like saying you should stay away from
> cars with antilock braking systems because the internal
> workings of such systems are sophisticated!
> With due respect, and at the risk of stating a common and
> obvious fact, or of being perceived as an ally of Mr.
> Yegge, let me recall that a (static) type system should be
> seen as a syntactic discipline for enforcing levels of
> abstraction. A language with a complicated (or
> sophisticated, if you prefer) type system may validly be
> criticized for having more potential inconsistencies than
> one with a comparably simpler type system, and therefore
> more chances of undermining its raison d'être.
> Scala, having one of the most complex type systems of any
> language, promises much and has therefore much to defend.
> A language like Rhino, with an essentially non-existent
> static type system, promises little and defends little.
> To use your motoring metaphor, one might take a skeptical
> view of the promises of freedom and safety offered by an
> anti-lock braking system, with admittedly complex (or
> sophisticated) internal mechanisms, when one already knows
> how to lay tracks, build fences to keep them out of the
> reach of casual intruders, and run a steam locomotive on
> them. (Some readers will surely have noted that a braking
> system is a runtime check, while a type system is more
> akin to the department of transportation that promises
> that licensed drivers will not crash their cars.)
I think metaphors are getting mixed here. What I imagined Martin was referring to metaphorically as being sophisticated on the inside is the Scala compiler, not the type system. The Scala compiler is indeed sophisticated, and writing it is a complicated, difficult task. The specification is written very formally to facilitate multiple implementors of language tools such as compilers (or for example JetBrains' compiler front-end for the IntelliJ Scala plugin) to be able to create tools that faithfully and consistently parse valid Scala programs and reject invalid ones.
Scala's type system can be called complicated in the sense of having many different interconnected parts, but to what extent is is complicated in the sense of not easy to analyze or understand? That kind of complexity is in the eye of the beholder, but is also influenced by the way in which people try and learn about the type system. Martin's point I think is that the Scala language specification was not at all intended as a good way to learn about Scala's type system. It's goal was something quite different. The goal of the book, by contrast, was to make it as easy as possible to learn Scala including its type system.
For what it's worth, my experience learning Scala's type system was:
- plain old (nominal) types: easy, same as Java
- type parameterization: also easy, same as Java, but with square brackets instead of angle brackets. I felt square brackets looked a lot nicer, and probably was therefore easier to read, too.
- function types: functions in Scala have nominal types like anything else, with a type parameter for the result type and one each for the parameter types. But there's also a shorthand syntax. Instead of
Function1[String, Int], for example, you can say
String => Int. The latter is actually easier to read, but when combined with an assignment such as "
val a: String => Int = something I find it can sometimes be non-obvious at first glance how to parse it.
- traits: traits were quite easy for me to understand and use. They are exactly like classes except they can't (currently) take constructor parameters and the meaning of
super is different inside them. Mixing in multiple traits is straightforward, as it is like implementing multiple interfaces in Java, except traits can have concrete method implementations and real fields. Self types inside traits was something new, but made sense and was quite easy to grok, and are quite powerful.
- singleton objects: these were also quite easy to get, as they are really just a shorthand for the Gang of Four singleton pattern, and can also be seen a more object-oriented way to do statics. The type of a singleton object is its name followed by ".type". Almost never need to use that, but it is pretty straightforward and easy to understand. The
SingletonObject.type refers to a synthetically generated class name for the singleton object, which has dollar signs in its name in bytecodes.
- variance: As I mentioned earlier, I found nonvariance and contravariance quite intuitive, but contravariance to be counterintuitive. That latter one took a bit of study, but with a bit of study it made sense. It wasn't that difficult. Variance also includes the concept of upper and lower bounds, which made sense to me. I found the concept of upper and lower bounds easy, but I still have trouble remembering which symbol goes to which:
>: means lower bounds;
<: means upper bounds. Maybe a good mnemonic is that the one that looks like a smiley face is on uppers.
- view bounds: I did find implicit parameters a bit mysterious at first, but once it I saw good examples of its use I realized what it was for and found it quite sensible. View bounds is a shorthand for a very powerful use of implicit parameters. Its symbol is
<%. So it looks like upper bounds except with a percent sign instead of a colon. It is similar conceptually to upper bounds too, except instead of a requirement that something is a subtype, it means something must be implicitly convertible to a type. I think this one can be tricky to learn and understand, so we worked hard on the example in the book to make it as easy to learn as possible. I have used it in ScalaTest and it was very helpful at making the library easier to use.
- existential types: this one's in there primarily for compatibility with Java. Even though one of the things it's supposed to do is enable expression of wildcard types, I found it pretty easy to understand. An existential type says that a type exists, but I don't know exactly what it is. I may know something about it, such that it is a subtype of some other type, and the way that is written is pretty readable. As I mentioned before, what I've found difficult is knowing how to write the Scala existential type that maps to particular a Java wildcard types. There's a shorthand for existential types that involves underscores, but I also found this pretty intuitive. The underscore is Scala is used for a number of different things, but generally stands for something that's left out. In this case, it's a type that's left out, and I found it quite easy to read.
- structural types: this one was also quite easy for me to understand. It is basically compile-time checked duck typing. The syntax is pretty obvious too. I've used it in ScalaTest, and it helped make the API much easier to write and use. Sometimes duck typing is exactly what's needed.
- higher-kinded types: I've never used it to my knowledge. It's syntax is so natural, though, that I may have used it without realizing that's what I was using. This one is not covered in the book. Martin is planning to reimplement Scala's collections library using higher-kinded types for the next release, and says it will eliminate code duplication in the collections library implementation.
- path-dependent types: this one I found a bit mysterious at first. I'm still not sure I appreciate fully its potential uses, but the mechanics of it is quite straightforward. A "path" is some way to get to an object at runtime, and the type at the end is a type defined inside that object's class. So a path dependent type to an inner class instance would be a different type for each different outer class. One place it is used in Scala is in the
Enumeration library class.
- abstract types: Scala classes can have type members as well as field and method members, and like fields and methods, they can be abstract. This actually all made sense to me, and seemed quite consistent. What is still not clear to me is how to explain when to use type parameters versus abstract type members. (The book is fairly silent on that subject.) There's some overlap between them, but there are differences also. One difference is you can't use variance with abstract types, just with type parameters. With more experience designing libraries I expect I'll discover more insights, but for now I generally reach first for type parameters.
- type inference: This was also quite easy. The compile will generally infer types where you think it can infer types. On rare occasions you may get a compiler error that clearly says it can't infer the type, and you just fill it in.
That's all I can think of that could be called "Scala's type system". As you can see there are a lot of pieces to it. But with a few exceptions I found most of the pieces to be quite easy to learn and understand. So the challenge in learning it is probably more in how many different pieces there are to learn than in the difficulty of the individual pieces themselves.