A generic can be viewed as a compile-time code generation function call.
It can has type and const inputs, and can has type, func and const outputs.
There are three new keywords are used in the propsoal: generic, assure and export.
A generic block body may contain
- some
assurelines which are used to constraint types. They have no effects on generic code executions. The purpose of these lines are to define inputs (types and consts) constraints/contracts. In practice,assurelines should be placed at the very beginning of agenericblock. - some general Go code. Such as type declarations, function declarations, ...
- an
exportfinal line to determine what should be exported. (Like the return statement in functions.) Agenericblock can hastype,funcandconstexports (outputs).
All of the three are optional.
If a generic block exports nothing, then it is a pure type constraint/contract definition. Please view potentially useful type constraints/contracts for such cases.
Some considerations for the design:
- it is hard to describe the input constraints in the generic prototype in a brief way. So in the prototype, only
type,constandfunccan be used to simply describe the overall functionality of a generic definition. - in the Go 2 generic draft, contract and code implementation are defined seperated in multiple blocks. This may be flexible so that multiple code implementation blocks can use the same one contract defintion. However, this also makes the generic definitions scatter in many places, which is not good for readibility. My propsoal doesn't forbit, but not recommend, the seperations.
- it gives people a crowding feeling and has bad readibilities by adding a generic parameter part to a function declaration or a type declaration. This way is similar to the generic designs in other languages, such as C++/Java/Rust. IMHO, such way looks very ugly and has a really bad readbility. On the contract, by seperating the generic parameter part and the traditional type/function declaration part, we can continue coding in a fimilar clean and readable way.
Some use case examples
Example 1
The generic definition:
package lib
import "fmt"
// this generic has not any constraints for type T.
generic List[T type] [type, func] {
type node struct {
Element T
Next *list
}
func push(n *node, e T) *node {
newNode := &node{Element: e}
if node == nil {
return newNode
}
node.Next = newNode
return node
}
func (n *node) Dump() {
fmt.Print("Dump result: ")
for n != nil {
fmt.Print(n.Element)
if n.Next != nil {
fmt.Print(", ")
}
n = n.Next
}
fmt.Println()
}
export node, push
}
An example using the above generic definition.
package main
import "lib"
type IntListNode, func PushIntListNode = lib.List[int]
type StringListNode, func PushStringListNode = lib.List[string]
func main() {
var intList *IntListNode
intList = PushIntListNode(intList, 123)
intList = PushIntListNode(intList, 456)
intList = PushIntListNode(intList, 789)
intList.Dump()
var strList *StringListNode
strList = PushStringListNode(strList, "abc")
strList = PushStringListNode(strList, "mno")
strList = PushStringListNode(strList, "xyz")
strList.Dump()
}
Example 2
package convert
generic ConvertSlice[Slice, NewElement type] [func] {
type OldElement = element(Slice)
asssure convertable[OldElement, NewElement]
func Convert(x Slice) []NewElement {
if x == nil {
return nil
}
y := make([]NewElement, 0, len(x))
for i := range x {
y = append(y, NewElement(x[i]))
}
return y
}
export Convert
}
Using the above generic:
package main
import "convert"
func stringSlice2InterfaceSlice = convert.ConvertSlice[[]string, interfacce{}]
func main() {
words := []string{"hello", "bye"}
fmt.Println(stringSlice2InterfaceSlice(words)...)
}
An exported function can also be assigned to local function variable:
package main
import "convert"
func main() {
words := []string{"hello", "bye"}
var stringSlice2InterfaceSlice = convert.ConvertSlice[[]string, interfacce{}]
fmt.Println(stringSlice2InterfaceSlice(words)...)
}
Example 3: generic call generic
If a generic call is following the assure keyword, then the effect is all the assure lines in the called generic block are embedded in the calling generic block.
A generic call is like an import for general code. To use the exported types/funcs/consts of a generic block, the generic block must be called in an assure line. (This is just a design detail, it can also be designed to not require this.)
An example:
package foo
import "lib"
import "convert"
generic SliceToList[Slice, NewElement type] [func, type, func] {
assure convert.ConvertSlice[Slice, NewElement]
assure lib.List[NewElement] // the line must be present to make the following line valid
type node, func push = lib.List[NewElement]
func slice2list (s Slice) *node {
var list *node
for i := range s {
list = push(list, NewElement(s[i]))
}
return list
}
export slice2list, node, push
}
Example 4: using single export generic
For a single export output generic, we can use it in one line instead of two.
package main
import "convert"
func main() {
words := []string{"hello", "bye"}
fmt.Println(convert.ConvertSlice[[]string, interfacce{}](words)...)
}
Through type deduction, it can be simplified as
package main
import "convert"
func main() {
words := []string{"hello", "bye"}
fmt.Println(convert.ConvertSlice(words)...)
}
Example 5: another signle export output example
The generic definition:
package lib2
import "fmt"
// this generic has not any constraints for type T.
generic List[T type] type {
type node struct {
Element T
Next *list
}
func (n *node) Push(e T) *node {
newNode := &node{Element: e}
if node == nil {
return newNode
}
node.Next = newNode
return node
}
func (n *node) Dump() {
fmt.Print("Dump result: ")
for n != nil {
fmt.Print(n.Element)
if n.Next != nil {
fmt.Print(", ")
}
n = n.Next
}
fmt.Println()
}
export node
}
An example using the above generic definition.
package main
import "lib2"
func main() {
var intList *lib2.List[int]
intList = intList.Push(123)
intList = intList.Push(456)
intList = intList.Push(789)
intList.Dump()
var strList *lib2.List[string]
strList = intList.Push("abc")
strList = intList.Push("mno")
strList = intList.Push("xyz")
strList.Dump()
}
More thoughts
It would be great if custom generic and built-in generic can be unified.
For a generic G with prototype generic [A, B, C, D], maybe we can call it with this form G[A][B][C]D, so that the like builtin map generic can be unified with custom generics. But I doubt it is worth making this unification.
It is very hard to find a unification form to also include built-in array/slice/chan/pointer generics. We can view the call syntax of the four built-in generics as special generic syntax.
So it look unifying custom generic and built-in generic is an imposible task. However, I think using [] in generic calls is much better than using ().
Need c-alike #if macros? Are there are better alternatives?
A very inmature example:
generic G[T type] func {
when sameKind[T, string]
func f() {
....
}
end
when sameKind[T, []byte] && equalsTo[element[(T), byte]
func f() {
....
}
end
export f
}
Maybe, it is not a good idea to support a generic solution which supports all possibilities.
The identifer of a declared function can also be used a function type to be passed to generic calls.
When we declare a function, we in fact bind a constant function value to an identifier. We can also use the identifier to represent the type of the function.
Some thoughts on runtime generic
Currently, the most bizarre syntax in Go is the type switch syntax. It looks types are used as values in the syntax.
It would be great to generialize the v.(type) syntax to make it look not that bizarre.
For example, for non-interface types, make v.(type) to represent the type of value v
and use it in variable declarations. But this has a problem, if the type of v is non-exported,
then we can use the non-exported type in other packages, which is not good.
It would be great to store a type in a variable-like thing. Currently, when we declare a function, we in fact declare a constant function value. Maybe we can do this for types too. For example, when we declare a type, we in fact declare a constant type. And we can create anonymous types at run time too.
func F() {}
var f = func() {}
type T int
var t = type int
// then we can pass t in a generic call to do runtime generic.
Maybe a subset of the compile-time generic would cover most generic use cases and are easy to be implemented at run time.
Any criticisms are welcome.