Wandering Thoughts archives

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.

Go2RequireMinimalContracts written at 22:13:08; Add Comment

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.)

GitViewFileOverTimeWish written at 01:27:06; Add Comment

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.)

GoInterfacesVsGenerics written at 21:36:27; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.