Many people are riledup
that Dart’s type system
proudly
features covariant generics and so “is unsound”. The
basic problem with covariant generics is that it lets you take
a bowl of apples and call it a bowl of fruit. That’s fine if you’re
just going to eat from the bowl and you like all fruits, but what
if you decide to contribute a banana to the fruit bowl? Someone
taking from the bowl expecting an apple might choke. And someone
else could call the same bowl a bowl of things then put a pencil in
it, just to spite you.

… because they are unsound?

We all seem to agree that it’s bad for an apple eater to choke
on a banana or a fruit eater to choke on a pencil. (Wittgenstein
and Turing similarly agreed that it’s bad for a bridge to fall
and kill people.) For example, Java seems to agree: it detects at
run time when such choking is about to occur and throws an
exception instead, so no immediate harm is done. There is less
agreement as to whether a static type system must prevent such
choking before run time. One
argument for early prevention is that it’s the whole point of
having a static type system—why even bother with a static type
system if it’s unsound?

But plenty of static type systems are unsound yet respectable.
For example, many dependent type systems are unsound, either
because their
term languages are blatantly non-terminating or because they
include the Type:Type rule. (I faintly recall that
Conor McBride calls Type:Type “radical
impredicativity”.) So it doesn’t seem that unsoundness per se
should rile people up about Dart in particular.

Perhaps it’s time to distinguish
different sorts of unsoundness: a type system can be unsound
because it fails normalization or because it fails subject
reduction. In the former case, a program (or the type checker)
might promise an apple but never produce one; in the latter case,
the program might promise an apple then produce a banana. The
unsoundness of a dependent type system tends to be of the former
sort, whereas the unsoundness of covariant generics seems
to be of the latter sort. And the former sort of unsoundness seems
more benign than the latter sort (and more deserving of the term
“radical impredicativity”). So, by calling for subject reduction
but not normalization, it seems we can drive a wedge between the
acceptable unsoundness of many dependent type systems and the
unacceptable unsoundness of Dart.

But it is very easy to turn a failure of subject reduction
detected at run time into a failure of normalization: just go into
an infinite loop whenever choking is about to occur! In Java, we
can write catch (Exception e) { for (;;) {} }. (In
math, we can restore subject reduction by letting an uncaught
exception take any type, as is standard.) Is that really better?
Does this move really answer the critics? I don’t feel so. For one
thing, this move makes it harder, not easier, to write better
programs: I want my program to stop and tell me
when it’s choking.

Just as it’s only
useful for type systems to allow a program if it’s something
good that a programmer might write, it’s only useful for type
systems to disallow a program if it’s something bad that a
programmer might write. In other words, both incompleteness and
unsoundness “may
appear to be a steep price to pay” yet “work well in practice”.
Does Type:Type abet many bad programs that people
produce? Perhaps not. Do covariant generics abet many bad programs
that people produce? Perhaps not. Are untyped bowls and naive set
theory terribly dangerous? Perhaps not. (Proof rules and
programming facilities that are unsound may even encourage useful
abstraction and reduce the proliferation of bugs by
copy-and-paste.)

… because they are meaningless?

To summarize so far, I don’t think we should dislike covariant
generics for its unsoundness in general or for its failure of
subject reduction in particular. Why do covariant generics bother
me, then? I think it’s because they dash my hope for types to have
crisp meanings.

When I write down an expression, whether it’s a term or a type,
I want it to refer to something else, whether in my mind
or in the world. When I write that a certain term has a certain
type (say, alice:person or 2+3:nat), I
want to write about some entity, to mean that it
has some property. That is not subject reduction, but to have a
subject and a predicate in the first place, which is prior to
subject reduction. I would be dissatisfied if there is no subject
and predicate to be found outside the syntax and operational
semantics of a formal language. After all, my desire to assert
that 2+3:nat is not fueled by any interest in the
curly symbols 2 and 3 or the binary node
label +. In short,
I want to denote.

Of course, it’s not entirely a formal language’s fault if I
can’t use it to denote, if it’s hard to program in. Syntax only
goes so far, and cognition must carry the rest of the way. But it
does get easier to denote and program if how expressions combine
inside the language matches how their referents tend to
interact outside the language. For example, FORTRAN made it easier
to denote numbers than in assembly language: having written
2+3 to denote a number, and knowing that numbers have
absolute values, I can write abs(2+3) to denote the
absolute value of the number denoted by 2+3. Numbers
are outside FORTRAN, yet FORTRAN matches my knowledge that numbers
have absolute values. In contrast, if you can hardly put a banana
in a bowl of apples in reality but you can easily put(banana,
bowl:Bowl<Apple>) in formality, or vice versa, then I
have trouble seeing what banana and bowlmean.

This match I want is a kind of modular reasoning. Now, the word
“modular”
has been used to mean many things, but what I mean here is
this. To combine two objects inside a language, I might need to
worry about whether their types match, whether they live on the
same machine, whether they use the same calling convention, etc. To
combine two objects outside a language, I might need to worry about
how big they are, whose kitchen it is, and whether fruit flies
would come. Ideally, my worries inside the language should match my
worries outside the language. Ideally, a combination should make
sense inside a language just in case the referents of the combined
expressions are compatible outside the language. If the language
implementation could keep track of my worries and discharge them as
early as possible for me, then so much the better, but that’s
secondary and orthogonal—I’d be plenty happy if I can just talk
about worrisome things without also worrying about the language at
the same time.

I feel it’s hopeless to match a language with covariant generics
against things we want to talk about. Why? I don’t have proof. For
all I can show, the worries of a Dart programmer inside and outside
the language will match perfectly, just as Java exceptions
precisely simulate the material effects of putting a pencil in a
bowl of apples. In the end, my feeling boils down to the
unreasonable and unusual effectiveness of
mathematics and logic.

Not only might this effectiveness be fallible, but it’s also
possible that my hopelessness is misplaced. I said I want
expressions to match their referents more closely so that denoting
and programming becomes easier. But maybe the match we have today
is the best we can do because the world has too many shades of gray
to be modeled in the black-and-white terms provided by mathematics
and logic. Maybe the world simply refuses to provide crisp
referents for any expression to mean. Maybe, when lexical
references slip and uncaught exceptions fire, it’s just collateral
damage from the fact that life is messy. After all, it is one thing
for a bridge engineer to leave gravity unquantified because
calculus is hard; it is another thing for a bridge architect to
leave beauty unquantified because psychology is hard. To avoid
variance rules just because they “fly in the face of programmer
intuition” feels like the former to me, but what do I know?

… because they are not even unsound?

Well, I do my best. I do my best to understand whether and how
reference and truth can help programming. I do my best to enlist
syntax and pragmatics. I’m not yet ready to
give up just because so many people are confused by noun-noun
compounds in English (what’s an “apple bowl”?) and by variance
rules in Java today. To me, the notion of being sound or unsound
only makes sense if expressions have meanings outside the language.
I do my best so that Bowl<Apple> means
something, so that when I claim “it is easy for tools to
provide a sound type analysis if they choose” I know what I
mean by “sound”. To not even worry about what expressions mean is
not even unsound.