2019-04-22
Go 2 Generics: The usefulness of requiring minimal contracts
I was recently reading Ole Bulbuk's Why Go Contracts Are A Bad Idea In The Light Of A Changing Go Community (via). One of the things that it suggests is that people will write generics contracts in a very brute force fashion by copying and pasting as much of their existing function bodies into the contract's body as possible (the article's author provides some examples of how people might do this). As much as the idea makes me cringe, I have to admit that I can see how and why it might happen; as Ole Bulbuk notes, it's the easiest way for a pragmatic programmer to work. However, I believe that it's possible to avoid this, and to do so in a way that is beneficial to Go and Go programmers in general. To do so, we will need both a carrot and a stick.
The carrot is a program similar to gofmt
which rewrites contracts
into the accepted canonical minimal form; possibly it should even
be part of what 'gofmt -s
' does in a Go 2 with generics. Since
contracts are so flexible and thus so variable, I feel that rewriting
them into a canonical form is generally useful for much the same
reasons that gofmt
is useful. You don't have to use the canonical
form of a contract, but contracts in canonical form will likely be
easier to read (if only because everyone will be familiar with it)
and easier to compare with each other. Such rewriting is a bit more
extreme than gofmt
does, since we are going from syntax to semantics
and then back to a canonical syntax for the semantics, but I believe
it's likely to be possible.
(I think it would be a significant danger sign for contracts if this
is not possible or if the community strongly disagrees about what the
canonical form for a particular type restriction should be. If we cannot
write and accept a gofmt
for contracts, something is wrong.)
The stick is that Go 2 should make it a compile time error to include
statements in a contract that are not syntactically necessary and that
do not add any additional restriction to what types the contract will
accept. If you throw in restrict-nothing statements that are copied
from a function body and insist that they stay, your contract does not
compile. If you want your contract to compile, you run the contract
minimizer program and it fixes the problem for you by taking them out.
I feel that this is in the same spirit as requiring all imports to
be used (and then providing goimports
). In general, future people,
including your future self, should not have wonder if some statement in
a contract was intended to create some type restriction but accidentally
didn't, and you didn't notice because your current implementation of the
generic code didn't actually require it. Things in contracts should
either be meaningful or not present at all.
To be clear here, this is not the same as a contract element that is not used in the current implementation. Those always should be legal, because you always should be able to write a contract that is more strict and more limited than you actually need today. Such a more restrictive contract is like a limited Go interface; it preserves your flexibility to change things later. This is purely about an element of the contract that does not add some extra constraint on the types that the contract accepts.
(You can pretty much always relax the restrictions of an existing contract without breaking API compatibility, because the new looser version will still accept all of the types it used to. Tightening the restrictions is not necessarily API compatible, because the new, more restricted contract may not accept some existing types that people are currently using it with.)
PS: I believe that there should be a gofmt
for contracts even if
their eventual form is less clever than the first draft proposal,
unless the eventual form of contracts is so restricted that there
is already only one way to express any particular type restriction.
2019-04-10
A Git tool that I'd like and how I probably use Git differently from most people
For a long time now, I've wished for what has generally seemed
to me like a fairly straightforward and obvious Git tool. What I
want is a convenient way to page through all of the different
versions of a file over time, going 'forward' and 'backward' through
them. Basically this would be the whole file version of 'git log
-p FILE
', although it couldn't have the same interface.
(I know that the history may not be linear. There are various ways to cope with this, depending on how sophisticated an interface you're presenting.)
When I first started wanting this, it felt so obvious that I couldn't believe it didn't already exist. Going through past versions of a file was something that I wanted to do all the time when I was digging through repositories, and I didn't get why no one else had created this. Now, though, I think that my unusual desire for this is one of the signs that I use Git repositories differently from most people, because I'm coming at them as a sysadmin instead of as a developer. Or, to put it another way, I'm reading code as an outsider instead of an insider.
When you're an insider to code, when you work on the code in the
repository you're reading, you have enough context to readily
understand diffs and so 'git log -p
' and
similar diff-based formats (such as 'git show
' of a commit) are
perfectly good for letting you understand what the code did in the
past. But I almost never have that familiarity with a Git repo I'm
investigating. I barely know the current version of the file, the
one I can read in full in the repo; I completely lack the contextual
knowledge to mentally apply a diff and read out the previous behavior
of the code. To understand the previous behavior of the code, I
need to read the full previous code. So I wind up wanting a convenient
way to get that previous version of a file and to easily navigate
through versions.
(There are a surprising number of circumstances where understanding something about the current version of a piece of code requires me to look at what it used to do.)
I rather suspect that most people using Git are developers instead of people spelunking the depths of unfamiliar codebases. Developers likely don't have much use for viewing full versions of a file over time (or at least it's not a common need), so it's probably not surprising that there doesn't seem to be a tool for this (or at least not an easily found one).
(Github has something that comes close to this, with the 'view blame prior to this change' feature in its blame view of a particular file. But this is not quite the same thing, although it is handy for my sorts of investigations.)
2019-04-08
An example of a situation where Go interfaces can't substitute for generics
I recently read Why Go Contracts Are A Bad Idea In The Light Of A Changing Go Community (via). I have some views on this, but today I want to divert from them to touch on one thing I saw in the article (and that I believe I've seen elsewhere).
In the article, the author cites the stringer
contract example
from the draft proposal:
func Stringify(type T stringer)(s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret } contract stringer(x T) { var s string = x.String() }
The author then says:
All that contracts are good for is ensuring properties of types. In this particular case it could (and should) be done simpler with the Stringer interface.
There are two ways to read this (and I don't know which one the
author intended, so I am using their article as a springboard). The
first way is that the contract is a roundabout way of saying that
the type T
must satisfy the Stringer interface, and we should
be able to express this type restriction directly. I don't entirely
argue with this, but I also don't think Go has any particularly
compact and clear way of doing this now. Perhaps there should be a
special syntax for it in a world with generics, although that depends
on how many contracts will be basically specifying required method
functions versus other requirements on types (such as comparability
or addibility).
The other way of reading this is to say that our Stringify()
example as a whole should be rewritten to use interfaces and not
generics. Unfortunately this isn't possible; you can't write a
function that behaves the same way using interfaces. This is
because a non-generic function using interfaces must have the
type signature:
func Stringify(s []Stringer) (ret []string)
The problem with this type signature is the famous and long
standing
Go issue that you cannot cast an array of some type to an array of
some interface, even if the type satisfies the interface. The power
of the generic version of Stringify
is that it can work on your
existing array of elements of some type; you do not have to manually
create an array of those elements turned into interfaces.
The larger problem is that creating an interface value for every
existing value is not free (even beyond the cost of a new array to
hold them all). At a minimum it churns memory and makes extra work
for the garbage collector. If you're starting with concrete values
that are not pointers, you'll hit other efficiency issues as well when your Stringify
calls the String()
receiver method on your type.
The attraction of generics in this situation is not merely being
able to implement generic algorithms in a way that is free of the
sort of flailing around with interfaces that we see in the current
sort
package. It is also that
the actual implementation should be efficient, ideally as efficient
as a version written for the specific type you're using. By their
nature, interfaces cannot deliver this level of efficiency; they
always involve an extra layer of interface values and indirection.
(Even if you don't care about the efficiency, the need to transform
your slice of T
elements to a slice of Stringer
interface values
requires you to write some boring and thus annoying code.)