Skip to content

Instantly share code, notes, and snippets.

@dotaheor
Last active February 17, 2019 02:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dotaheor/4b7496dba1939ce3f91a0f8cfccef927 to your computer and use it in GitHub Desktop.
Save dotaheor/4b7496dba1939ce3f91a0f8cfccef927 to your computer and use it in GitHub Desktop.
A Go generic syntax proposal which combine contract definition and code implementation tegother, so that generic code looks fimilar with general Go code.

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 a gen 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.) A gen block can has type, func and const 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 and func 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.

Some use case examples

Example 1

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.

Example 2

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

Example 3: generic call generic

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
}

Example 4: using single export generic

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

Example 5: another signle export output example

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()
}

More thoughts

Alternative design to the export keyword.

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

Need c-alike #if macros? Are there are better alternatives?

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.

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.

Very immature ideas to unify custom and built-in generics

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

About type literal representation

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

About operator rewritten

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)

About built-in constants and variables

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

Reform the type-swtich syntax

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.

@dotaheor
Copy link
Author

Any criticisms are welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment