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