A gen
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: gen
, assure
and export
.
A gen
block body may contain
- some
assure
lines 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,assure
lines should be placed at the very beginning of agen
block. - some general Go code. Such as type declarations, function declarations, ...
- an
export
final line to determine what should be exported. (Like the return statement in functions.) Agen
block can hastype
,func
andconst
exports (outputs).
All of the three are optional.
If a gen
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
,const
andfunc
can 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.
The generic definition:
package lib
import "fmt"
// this generic has not any constraints for type T.
gen 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()
}
NOTICE: there is an alternative modified version on how to use a gen
.
package convert
gen 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)...)
}
If a gen
call is following the assure
keyword, then the effect is all the assure
lines in the called gen
block are embedded in the calling gen
block.
A gen
call is like an import
for general code. To use the exported types/funcs/consts of a gen
block, the gen
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"
gen 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
}
For a single export output gen
, 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)...)
}
The generic definition:
package lib2
import "fmt"
// this generic has not any constraints for type T.
gen 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()
}
Sometimes, a gen
may export too many items to make a generic call look verbose.
An alternative design is to introduce a crate
element.
A crate
is an instance of a gen
definition and it can be viewed as a mini imported package.
For example, the following is a modification on the first example shown above by using crate
instead.
The modiified generic definition:
package lib
import "fmt"
// this generic has not any constraints for type T.
gen List[T type] {
type Node struct { // the type name must be an exported identifier
Element T
Next *list
}
func Push(n *node, e T) *node { // the func name must be an exported identifier
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()
}
}
The modified using the above modiifed generic definition.
package main
import "lib"
crate IntList = lib.List[int]
crate StringList = lib.List[string]
func main() {
var intList *IntList.Node
intList = IntList.Push(intList, 123)
intList = IntList.Push(intList, 456)
intList = IntList.Push(intList, 789)
intList.Dump()
var strList *StringList.Node
strList = StringList.Push(strList, "abc")
strList = StringList.Push(strList, "mno")
strList = StringList.Push(strList, "xyz")
strList.Dump()
}
// We can also do these:
type IntListNode = IntList.Node
func IntListPush = IntList.Push
type StringListNode = StringList.Node
func StringListPush = StringList.Push
A very inmature example:
gen 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.
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.
[Feb. 15, 2019] Update: I have some better ideas to unify custom and built-in generics now. Please read the v2 version and this solution for details.
For a gen
G
with prototype gen [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 ()
.
Maybe it would be good to introduce an expr
element, so that we can define operator rewritten as
expr (a T) + (b T) = AddT(a, b)
Maybe it is more consistent if we can define nil
, iota
, true
and false
as generics.
gen nil[T type] [var] {
var t T
export t
}
This example needs the generics can export var
s.
The current type-swithc syntax is some bizzard. Maybe
switch v := v.(type) {
case int:
case nil:
default:
}
can be reformed as:
switch T, v := v.(type), v.(var); T {
case int:
case nil:
default:
}
or:
switch T, v := v.(type, var); T {
case int:
case nil:
default:
}
The reform needs runtime generic support so that a type can used as a super type value.
Any criticisms are welcome.