2018-11-16
Restisting the temptation to rely on Ubuntu for Django 1.11
One of the things that is on my mind is what to do about our Django web application as far as Python 3 goes. Right now it's Python 2, and even apart from people trying to get rid of Python 2 in general, the Django people have been quite explicit that Django 1.11 is the last version that will support Python 2 and that support for it will end in 2020 (probably 'at the start of 2020' in practice). Converting it to Python 3 is getting more and more urgent, but at the same time this is going to be a bunch of grinding work (I still haven't added any tests to it, for example).
The host that our Django web app runs on was recently upgraded to Ubuntu 18.04 LTS, so the other day I idly checked the version of Django that 18.04 packages; this turns out to be Django 1.11 (for both Python 2 and Python 3; Django 2.0 for Python 3 might just have missed Ubuntu's cutoff point, since it was only released at the end of 2017). Ubuntu 18.04 LTS will be supported for five years and Ubuntu never does the sort of major version updates that going from 1.11 to 2.x would be, so for a brief moment I had the great temptation to switch over to the Ubuntu 18.04 packaged version of Django 1.11 and then forgetting about the problem until 2022 or so.
Then I came to my senses, because Ubuntu barely fixes bugs and security issues at the best of times. To my surprise, Ubuntu actually has Django in their 'main' repo, which is theoretically fully supported, but in practice I don't really believe that Canonical will really be spending very much effort to keep Django 1.11 secure after the upstream Django developers drop support for it. No later than 2020, the Ubuntu 18.04 LTS version of Django 1.11 is very likely to become, effectively, abandonware. Unless we feel very confident that Django 1.11 will be completely secure at that point in our configuration, we should not keep running it (especially since a small portion of the application is exposed to the Internet).
(I wouldn't be surprised if Canonical backported at least some easy security fixes from 2.x to 1.11 after 2020. But I would be surprised to see them do any significant programming work for code that's significantly different between 1.11 and the current 2.x or for 1.11-specific issues.)
However much I'd like to ignore the issue for as long as possible or let myself believe that it can be someone else's issue, dealing with this is in my relatively immediate future. We just have to move our Django web app to Python 3 and Django 2.x, even though it's going to be at least a bit of a grind. Probably I should try to do it bit by bit, for example by spending even just an hour or a half hour a week adding a test or two to the current code.
(Part of why I feel so un-motivated is that we're going to have to invest a bunch of effort to wind up exactly where we are currently. The app works perfectly well as it is and we don't want anything that's in newer Django versions; we're upgrading purely to stay within the version coverage of security fixes. This is, sadly, a bunch of make-work.)
Go 2 Generics: Contracts are too clever
The biggest and most contentious thing in the Go 2 Draft Designs is its proposal for generics. Part of the proposal is a way to specify things about the types that generic functions can be used on, in the form of contracts. Let me quote from the overview:
[...] In general an implementation may need to constrain the possible types that can be used. For example, we might want to define a
Set(T)
, implemented as a list or map, in which case values of typeT
must be able to be compared for equality. To express that, the draft design introduces the idea of a named contract. A contract is like a function body illustrating the operations the type must support. For example, to declare that values of typeT
must be comparable:contract Equal(t T) { t == t }
This is a clever trick (among other things it basically avoids adding any new syntax to Go to define contracts), but my view is that it is in fact too clever. Using function bodies with operations in them to define the valid types is essentially prioritizing minimal additions to the language and the compiler over everything else. The result is easy for the compiler to use (it just runs types through contract bodies in a regular typechecking process) but provides generally bad to terrible ergonomics for everything else.
In practice, contracts will be consumed and used by far more than the compiler. People will read contracts to understand error messages about 'your type doesn't satisfy this contract', to understand what they need to do to create types that can be used with generic functions they're interested in, to see how to specify something for their own generic functions, to try to understand mistakes that they made in their own contracts, and to understand what a contract actually requires (as opposed to what the code comments claim it requires, because we all know about what happens to code comments eventually). IDE systems will read contracts so they can figure out what types can be suggested as type parameters for generic functions. Code analysis will read contracts for all sorts of reasons, including spotting unnecessarily requirements that the implementation doesn't actually need.
All of these parties and all of these purposes are badly served by contracts which require you to understand the language and its subtle implications at a deep level. For instance, take this contract:
contract Example(t T) { t.Fetch()[:5] t.AThing().Len() }
The implications of both lines are reasonably subtle; the first
featured in a quiz by Dave Cheney, and the
second requires certain sorts of return values and what the .Len()
method is on, as covered as part of addressable values in Go. I would maintain that neither are obvious
to a person (certainly they aren't to me without careful research),
and they might or might not be easily understood by code (certainly
they're the kind of corner cases that code could easily be wrong
on).
Part of the bad ergonomics of contracts here is that they look simple and straightforward while hiding subtle traps in the fine details. I've illustrated one version of that here, and the detailed draft design actually shows several other examples. I could go on with more examples (consider the wildly assorted type requirements of various arithmetic operators, for example).
Go contracts do not seem designed to be read by anything but the compiler. This is a mistake; computer languages are in large part about communicating with other people, including your future self, not with the computer. If we're in doubt, we should bias language design toward being clearly and easily read by people, not toward the compiler. Go generally has this bias, preferring clean communication over excessive cleverness. The current version of contracts seem quite at odds with this.
(This is especially striking because other parts of the Go 2 Draft Designs are about making Go clearer for people. The error handling proposal, for example, is all about making Go code read better by hiding repetitive patterns.)
PS: The Go team may have the view that only a few people will ever create generics and thus contracts, and mostly everyone else will read the excellent documentation that these very few people have carefully written. I think that the Go team is quite wrong here; I expect to see generics sprout like mushrooms across Go codebases, at least for the first few years. In general I would argue that if generics are successful, we can expect to see them widely used, which means that many generics and contracts will not be written or read by people who are deep experts with Go. A necessary consequence of this wide use will be that some amount of it will be with contracts and code created partly through superstition in various forms.