Hi there
I am aware that the proposal to add generics has been accepted, so the discussion of whether or not Go will get generics is answered. For better or for worse.
I worked on a different approach since a few years, not very intensive, and in spite of this fact, I now want to tell you about:Binding interface types on import
See in: https://github.com/leiser1960/importbinding/blob/master/README.md
I did not speek up yet, because it simply was not ready for
discussion in my eyes.
Why do I think the accepted proposal is not good enough?
Technically it seem sound and consistent to me and has all the
bells and whistles you would expect. And all the Features. And
close enough to the way other languages define generics.
So what do i dislike?
Hmmm... Let me answer another question first:
What do I love about Go?
Its simplicity. And Simplicity is
Complicated
And that is IMHO the problem of the accepted generics proposal,
the lack of simplicity.
To explain this, let me start with a language design question:
What is needed for adding "monomorphic generic types" to Go?
Go already has a polymorphic generic type mechanism: "interface types".
So you have to start from this (as the accepted proposal does) and add to things:
- A way to define type parameters.
- A way to bind the type parameters to concrete types at
compile time
The accepted proposal does so by:
1. Adding positional type parameters to type and function definitions
2. Adding a syntax for binding concrete types to the type parameters on use of the types and functions.
With the proper brackets it is readable. And of course there are more goodies in the propsal such as:
- An extension to the descriptive power of "interface types".
- type inference rules to eliminate the need of the additional type parameters in certain cases.
Both are great for functional libraries such as "sort.go", but
does not help for container types such as "container/list.go".
And the proposal is long because any generic type mechanism is
complicated to implement and tricky to integrate in the language.
This is the obviously complicated part.
But the proposal lacks the simplicity and elegance of for example:
- the way Go integrates inheriting method from a typ by simply omitting the attribute name in a struct definition
- the way Go defines the types of numerical constant expressions for the purpose of type inference
incredibly simple to use and explain. No need for an additional
syntactic element at all.
And completely different from the way other languages treat this.
The accepted proposal more or less follows the syntactic path taken since Ada, over C++ and Java and all the other languages.
It this good enough?
Can we do better?
I think so. But How? Remember we need to do two things:
- A way to define type parameters.
- A way to bind the type parameters to concrete types at
compile time
Is suggest doing this on the package level, not the individual types or functions:
1. Use named interface types as implicit type parameters of a package
2. Bind the named types on import
With the first I simply mean any declaration of the form:
type ElementType interface {}
e.g. The type Locker in
sync.go.
You do not find such a declaration in "containter/list.go"
. But it would be simple task to add such a line and replace all
existing occurences of "interface{}" by "ElementType". Not
changing the behaviour at all.
As a go proverb says: interface
nothing says nothing
But after we named it "ElementType" we can get a hold of it and bind it in the package we intend to use e.G. a "list of strings":
import "container/list" type ElementType string
augmenting the existing "import" clause by this type binding. We say:
We bind (the binding) type "string" to (the bound type) "list.ElementType".
We bind the type string to the type list.ElementType in the
import of package list.
It is as simple as this.
But what does this mean?
It means two things:
1. binding type must implement the bound type.
2. the bound type is treated as if defined by:
type BoundType BindingType
in the imported package.
1. is trivial in our example, because interface{} is implemented by any time.
2. is trivial in our example, because "string" is a standard
type.
The general case is not so trivial, think of the binding type
being defined locally and private to the package.
I tried to give a semantic description in my definition in Binding
interface types on import but it is not as elaborate as the
accepted proposal.
The semantics is defined by a set of program transformations back
to a Go 1 program.
Is this simplicty?
I think so, ok not as simple as inheritance, because I have to
add a syntactic element: The type binding on Import.
But definitely a simpler than the accepted proposal.
Last features. If you need two different bindings in one package, say a list of string and a list of int you may use:
import "container/list" intlist type ElementType = int
import "container/list" stringlist type ElementType = string
And then simply pick either the package name: "intlist" or "stringlist" e.g.
myintlist := intlist.New()
mystringlist := stringlist.New()
This makes myintlist.Value of type int, whereas
mystringlist.Value is of type string.
And if you think of the type "Map" in "sync.go": You need a way to bind more than one type on a single import: sync.Key and sync.Value:
import "sync.go" type Key string; Map mySyncdata
But now we have all of the syntax by example. Simple to use.
You may not bind all "named interface types" on the import, the
above does not bind the Type "Locker", so the "Cond"
type remains polymorphic.
The definition of a generic package is go 1 compatible.
You only have to "name" the parameter types for use by type
binding imports. Which improves the documentation as well.
Not convinced? Lets move from Go 1 polymorphic generics to Go 2
monomorphic use, again using the "container/list.go" example.
It requires two sorts of changes:
1. Add the required type binding to the import clause, see
above
2. delete the type casts needed for accessing the "Value" Attribute of the "Element" struct:
if "" = (string)mystringlist.Value {
has to be changed to:
if "" = mystringlist.Value {
Try the same with the accepted proposal.
But a last word:
So behind the scenes there is more to it:
- there is an additional restriction for "named interface
types" necessary in order to allow them to be used as type
parameters.
- there are new semantic hurdles because of the type parameters being on the package level.
- I did not describe any "type inference rules" yet, but it should be possible to add this for typical functional libraries such as "sort.go".
- type bindings my even be transitive, if the binding type is itself a exported interface type.
So the a full fledged proposal may end up as long as the accepted
proposal. That is the complicated part anyway.
But it is this the kind of simplicity I want to see when
talking about monomorphic generic types. Perhaps someone can do
even better?
Yours Martin Leiser