Skip to content

Instantly share code, notes, and snippets.

@tooolbox
Last active August 24, 2023 23:27
Show Gist options
  • Save tooolbox/6bde6925a1a8c78cb593b2c11e977b07 to your computer and use it in GitHub Desktop.
Save tooolbox/6bde6925a1a8c78cb593b2c11e977b07 to your computer and use it in GitHub Desktop.
Go 2 Contracts Feedback - May 2020

Towards Clarity: Syntax Changes for Contracts in Go

There are two main parts to the Contracts Draft Design:

  • Type Parameters
  • Contracts

Type Parameters are mostly independent of Contracts; Contracts builds on Type Parameters. The two concepts are well thought-out and many corner cases have been looked at.

These are some specific points that I think are done well by the current generics draft proposal, not in any particular order:

  • Type parameters (generics!) in functions, structs, etc.
  • Ability to express relationships between types through contracts
  • Ability to constrain contract types by methods, and/or by a list of existing types, including aggregate types
  • Pointer methods in contracts
  • Embedded contracts
  • Self-referential types in contracts
  • Export rules (capitalization) for contracts are the same as other constructs
  • Explicit type parameters at call sites, inferred where possible
  • "Unboxed" generics, for those who want to avoid any runtime penalty
  • Ability to defer the decision of compile-time/runtime implementation
  • Instantiation of type-parameterized functions, structs, etc.
  • No changes to the Reflect package
  • No operator overloading (barring Ordered and Comparable, etc. as proposed)
  • Type conversions
  • Focus on the burden of complexity being on the writer of generic code, rather than the consumer
  • Design that does not encourage widespread proliferation
  • Good set of omissions.
  • Absolutely acceptable zero value solution.
  • Probably sufficient orthogonality with interfaces.
  • Examples of how to implement many of the most common use cases for generics (map, reduce, set, linked list, etc.)

The primary issue with the Contracts proposal is the syntax; it does not feel like Go code. It is difficult to read and confusing to a Go developer.

This document contains several proposals to change the syntax of Type Parameters and Contracts, to improve their readability. Each proposal is incorporated into the next, so the examples will show an evolution of syntax. (Note that, in spite of this, each proposal can stand on its own, and can be broken out if desired.) At the end there will be a thorough comparisons with the existing draft's examples.

Methods and Types in Contracts

I propose an alternate syntax for specifying methods in contracts:

// Existing syntax
contract stringer(T) {
    T String() string
}

// Method-call syntax
contract stringer(T) {
    T.String() string
}

This syntax is more clear regarding the relationship of String() to T and the nature of what T is, a type that is the receiver of the String() method.

A similar alternative exists for specifying a list of types:

// Existing syntax
contract Float(T) {
    T float32, float64
}

// Type-assertion syntax
contract Float(T) {
    T.(float32, float64)
}

This syntax is familiar to anyone who has done an ordinary type assertion in Go; T must be of one of the given types.

An example with both methods and types:

// Existing syntax
contract StringableSignedInteger(T) {
    T int, int8, int16, int32, int64
    T String() string
}

// Proposed syntax
contract StringableSignedInteger(T) {
    T.(int, int8, int16, int32, int64)
    T.String() string
}

An example with multiple types in the contract:

// Existing syntax
contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

// Proposed syntax
contract G(Node, Edge) {
    Node.Edges() []Edge
    Edge.Nodes() (from Node, to Node)
}

By adding the . to method & type specifications in contracts, the relationship is clarified and there is better visual separation of the different elements.

Empty Contract

I propose the concept of an empty contract which is written as:

contract{}

The empty contract is satisfied by any type. The amount of types involved in an empty contract is unbounded.

The empty contract is used for a type-parameterized declaration that otherwise does not use a contract. Here are some examples:

// Existing syntax
func Print(type T)(s []T) { ... }
func Print2(type T1, T2)(s1 []T1, s2 []T2) { ... }

type Lockable(type T) struct {
	T
	mu sync.Mutex
}

type Vector(type Element) []Element

// Proposed syntax
func Print(type T contract{})(s []T) { ... }
func Print2(type T1, T2 contract{})(s1 []T1, s2 []T2) { ... }

type Lockable(type T contract{}) struct {
	T
	mu sync.Mutex
}

type Vector(type Element contract{}) []Element

This mirrors the concept of the empty interface which is familiar to Go developers. More importantly, by always using a contract with type parameters, the concepts involved are simplified and the syntax is more consistent.

Type Parameter Syntax for Mixed-Empty Contracts

The existing type parameter syntax is as follows:

type t1, t2, ...tN con?

In other words, a comma-separated list of type names with an optional contract specified at the end.

However, there are scenarios where some type parameters belong to a contract, and some belong to the empty contract:

func MapAndPrint(type E, M stringer(M))(s []E, f(E) M) []string {
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = f(v).String()
	}
	return r
}

The updated syntax then is:

type t1, t2, ...tN con?(t1, t2, ...tn)?

To improve this, and in alignment with the empty contract, I propose an alternative syntax when defining type parameters in this scenario:

func MapAndPrint(type E contract{}, M stringer)(s []E, f(E) M) []string {
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = f(v).String()
	}
	return r
}

The exact syntax is:

type t1, t2, t3, ...tN contract, t1, t2, t3, ....tN contract

Type parameters must list at most one regular contract, optionally the empty contract, and at least one of either. The layout and syntax is similar to the shorthand for multiple function arguments or struct fields and should seem natural to Go programmers.

Explicit Contract Parameter Matching

When a contract is used to validate type parameters, the amount and sequence must match, but the names are irrelevant. I propose an additional, alternative method of declaring type parameters, whereby contract-matching uses the type names, and ignores the amount and sequence of values.

// Normal syntax
type t1, t2, t3, ...tN contract, t1, t2, t3, ....tN contract

// Alternative syntax
type name1? contract.t1, name2? contract.t2, ...nameN? contract.tN

In other words, name/type pairs.

  • All types must be drawn from the same contract, or from the empty contract.
  • The types must match how they are defined in their contract.
  • There is no requirement to list every type specified by a contract.
  • The type can stand by itself as an identifier, similar to anonymous struct embedding.
  • If the signature contains an empty contract, no types may be "anonymous".

An example of this would be:

contract equal(T) {
	T.Equal(T) bool
}

// Explicitly named contract type
func Index(type X equal.T)(s []X, e X) int { ... }

// Embedded contract type
func Index(type equal.T)(s []T, e T) int { ... }

contract stringer(T) {
	T.String() string
}

// ILLEGAL!
func MapAndPrint(type E contract{}, stringer.T)(s []E, f(E) M) []string { ... }

// This is fine
func MapAndPrint(type E contract{}, M stringer.T)(s []E, f(E) M) []string { ... }

Although it could make individual type parameters more verbose, this style allows you to select specific arguments of the contract to use for validation. A good example of this use case is in Roger Peppe's generic mgo exploration. The syntax of the contract is outdated, but I think the point can't be missed:

contract SessionContract(
	s Session,
	db Database,
	c Collection,
	q Query,
	it Iter,
) {
	Session(Collection)(s)
	Collection(Query)(db)
	Query(Iter)(query)
	Iter(it)
}

// Normal syntax
func PrintBobDetails(
	type Session, _, _, _, _ SessionContract,
)(session Session) {
	iter := session.DB("mydb").C("people").Find(bson.M{
		"name": "bob",
	})
	... etc
}

// Name-based matching
func PrintBobDetails(
	type SessionContract.Session,
)(session Session) {
	iter := session.DB("mydb").C("people").Find(bson.M{
		"name": "bob",
	})
	... etc
}

In addition to a contract with a large number of type parameters, this style can help with general code maintenance, because changing the number or sequence of type parameters will not break code that uses that contract. As an analogy, the existing way of declaring type parameters is like using unnamed fields in a struct literal; this is the named variation.

Non-Parenthetical Delimiters

The delimiters to use for type parameters are perhaps the most hotly contested aspect of the contracts draft.

The contracts draft has specific callouts for the team's thoughts on F<T> and F[T] and F«T». To be fair, they also touch upon Lots of irritating silly parentheses, and the 4 kinds of parsing ambiguity introduced by using parentheses for type argument clauses.

It's true, there are downsides for any delimiter you can think of. However, using parentheses will have the greatest downsides, because it will harm the users of Go code by making it difficult to read. The F<T> form, although potentially difficult to implement, would be superior simply because the complexity is kept in the parser and not thrust upon the developer. Go users may be distantly proud of the tooling's simplicity, including the parser, but they don't study the parser for their day job—they read and write Go code. It needs to be extremely readable, and while feedback is mixed, the general consensus seems to be that the additional parentheses make the code harder to read.

Key points:

  1. Using F<T> is most familiar to developers as it is common in other languages. If this can be solved, it would be for the best.
  2. Using F«T» is not terrible. The characters can be written on a Mac with Alt + \ and Alt + Shift + \. Perhaps the compiler could equally accept F<<T>>, and for consistency gofmt could transform F<<T>> into F«T» in cases where there is clearly no bit-shifting occurring. Also, generally speaking, a lot of generic code (I would say well-written generic code) can have the type parameters inferred, and won't need these delimiters, so the burden of typing unusual characters generally falls on the writer of generic code, not the consumer (as mentioned in the draft).
  3. Failing the above, F|T| is a fallback, simply because they're not parentheses.
  4. Also, F<|T|> is a little ugly, but it's not F<T> and it's easier to see the start & end points than F|T|, so it's a candidate.

I won't insist on a particular delimiter, but I will argue, plead, and cajole that we not proceed until we have something better than parentheses worked out.

Here is how the Graph example looks with F«T»:

contract G«Node, Edge» {
	Node.Edges() []Edge
	Edge.Nodes() (from Node, to Node)
}

type Graph«type Node, Edge G» struct { ... }
func New«type Node, Edge G»(nodes []Node) *Graph«Node, Edge» { ... }
func (g *Graph«Node, Edge») ShortestPath(from, to Node) []Edge { ... }

Something I would also support is angle brackets before types, suggested by Peter McKenzie. In that case I think the type keyword would be omitted from the type parameter definition clause:

type <T contract{}>List []T
func <K, V contract{}>Keys(m map[K]V) []K

var ints <int>List
// keys := <int, string>Keys(map[int]string{1: "one", 2: "two"})
keys := Keys(map[int]string{1: "one", 2: "two"}) // type-inferred

type <T Equal>Set []T
func (s <T>Set) Find(x T) int {
  ...
}

The above is radical in its own way. <T>F is less readable than F<T> at first glance but more readable than F(T) because the parts are clearly delineated.

For the sake of making a selection, the rest of this document will be in the style of F«T».

Rejected: Solo Contracts

The following is a proposal that I talked myself out of.

There are some contracts that have a single type, which is specified in the name of the contract itself. The nested type then seems extraneous. Contracts could have a "solo" syntax:

// Existing syntax
contract StringableSignedInteger«T» {
	T.(int, int8, int16, int32, int64)
	T.String() string
}

// Solo syntax
contract StringableSignedInteger {
	int, int8, int16, int32, int64
	String() string
}

In combination with explicit, anonymous type parameters, you could achieve this effect:

contract setter {
	Set(string) error
}

func Strings«type setter»(s []string) ([]setter, error) {
	ret := make([]T, len(s))
	for i, v := range s {
		if err := ret[i].Set(v); err != nil {
			return nil, err
		}
	}
	return ret, nil
}

One could argue that this is more readable; it's similar to using an interface in that the contract and the type are unified. There's nothing extraneous.

However, I do not propose this for two reasons:

  1. Since it's innately a different syntax for a contract, it may throw off expectations of what a contract should look like, and it might then be confused with an interface.
  2. It creates inconsitent syntax when specifying a list of types as a constraint on a contract. I considered and discarded _.(int, int8...) and similar things.
  3. It creates an ambiguity in the type parameter listing: which form is being used, named or unnamed? Without Solo Contract syntax, even anonymous type params can be identified by their dot; I have this feeling it must makes things too loose.

The normal syntax is not particularly burdensome:

contract setter«T» {
	T.Set(string) error
}

func Strings«type set setter»(s []string) ([]set, error) {
	ret := make([]T, len(s))
	for i, v := range s {
		if err := ret[i].Set(v); err != nil {
			return nil, err
		}
	}
	return ret, nil
}

Code Comparison

What follows is a conversion of the code samples from the contracts proposal, to use the syntax given above, to get a moderately wide sampling of how it will look. Note that another set of delimiters could be used instead of F«T», such as F<T>.

The main point of this entire batch of changes is to improve the readability of generic code in the Go language, and I believe the below examples do show that's been done. It no longer looks like odd and surprising function calls strewn throughout, it looks like something different and unique, and yet familiar enough to easily grasp the intent.

Design

Type parameters

// Print prints the elements of a slice.
// It should be possible to call this with any slice value.
func Print(s []T) { // Just an example, not the suggested syntax.
    for _, v := range s {
        fmt.Println(v)
    }
}
func Print«type T contract{}»(s []T) {
    // same as above
}
    Print«int»([]int{1, 2, 3})
    Print([]int{1, 2, 3}) // inferred

Type contracts

// This function is INVALID.
func Stringify«type T contract{}»(s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String()) // INVALID
    }
    return ret
}

Contract introduction

contract stringer«T» {
    T.String() string
}

Using a contract

func Stringify«type Str stringer»(s []Str) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String()) // now valid
    }
    return ret
}

Multiple type parameters

func Print2«type T1, T2 contract{}»(s1 []T1, s2 []T2) { ... }
func Print2Same«type T1 contract{}»(s1 []T1, s2 []T1) { ... }
contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Parameterized types

type Vector«type Element contract{}» []Element
var v Vector«int»
func (v *Vector«Element») Push(x Element) { *v = append(*v, x) }
// This is OK.
type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

// This type is INVALID.
type P«type Element1, Element2 contract{}» struct {
    F *P«Element2, Element1» // INVALID; must be «Element1, Element2»
}
type StringableVector«type Str stringer» []T

func (s StringableVector«Str») String() string {
    var sb strings.Builder
    sb.WriteString("[")
    for i, v := range s {
        if i > 0 {
            sb.WriteString(", ")
        }
        sb.WriteString(v.String())
    }
    sb.WriteString("]")
    return sb.String()
}
type Lockable«type T contract{}» struct {
    T
    mu sync.Mutex
}

func (l *Lockable«T») Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

Parameterized type aliases

type VectorInt = Vector«int»

Contract embedding

contract PrintStringer«X» {
    stringer«X»
    X.Print()
}

This is equivalent to

contract PrintStringer«X» {
    X.String() string
    X.Print()
}

Using types that refer to themselves in contracts

package compare

// The equal contract describes types that have an Equal method with
// an argument of the same type as the receiver type.
contract equal«T» {
    T.Equal«T» bool
}

// Index returns the index of e in s, or -1.
func Index«type T equal»(s []T, e T) int {
    for i, v := range s {
        // Both e and v are type T, so it's OK to call e.Equal(v).
        if e.Equal(v) {
            return i
        }
    }
    return -1
}
import "compare"

type EqualInt int

// The Equal method lets EqualInt satisfy the compare.equal contract.
func (a EqualInt) Equal(b EqualInt) bool { return a == b }

func Index(s []EqualInt, e EqualInt) int {
    // return compare.Index«EqualInt»(s, e)
    return compare.Index(s, e) // inferred
}

Mutually referencing type parameters

package graph

contract G«Node, Edge» {
    Node.Edges() []Edge
    Edge.Nodes() (from Node, to Node)
}

type Graph«type G.Node, G.Edge» struct { ... }
func New«type G.Node, G.Edge»(nodes []Node) *Graph«Node, Edge» { ... }
func (g *Graph«Node, Edge») ShortestPath(from, to Node) []Edge { ... }
type Vertex struct { ... }
func (v *Vertex) Edges() []*FromTo { ... }
type FromTo struct { ... }
func (ft *FromTo) Nodes() (*Vertex, *Vertex) { ... }
var g = graph.New«*Vertex, *FromTo»([]*Vertex{ ... })
type NodeInterface interface { Edges() []EdgeInterface }
type EdgeInterface interface { Nodes() (NodeInterface, NodeInterface) }

Passing parameters to a contract

func MapAndPrint«type E contract{}, M stringer»(s []E, f(E) M) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = f(v).String()
    }
    return r
}

Values of type parameters are not boxed

package from

contract setter«T» {
    T.Set(string) error
}

func Strings«type T setter»(s []string) ([]T, error) {
    ret := make([]T, len(s))
    for i, v := range s {
        if err := ret[i].Set(v); err != nil {
            return nil, err
        }
    }
    return ret, nil
}
type Settable int

func (p *Settable) Set(s string) (err error) {
    *p, err = strconv.Atoi(s)
    return err
}

func F() {
    // The type of nums is []Settable.
    nums, err := from.Strings«Settable»([]string{"1", "2"})
    if err != nil { ... }
    // Settable can be converted directly to int.
    // This will set first to 1.
    first := int(nums[0])
    ...
}
package pair

type Pair«type carT, cdrT contract{}» struct {
    f1 carT
    f2 cdrT
}

Function argument type inference

    Print«int»([]int{1, 2, 3})
    s1 := []int{1, 2, 3}
    Print(s1)
package transform

func Slice«type From, To contract{}»(s []From, f func(From) To) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}
    strs := transform.Slice([]int{1, 2, 3}, strconv.Itoa)
package pair

func New«type T contract{}»(f1, f2 T) *Pair(T) { ... }
type Pair«type T contract{}» struct { f1, f2 T }
var V = Pair{1, 2} // inferred as Pair«int»{1, 2}

Instantiating a function

// PrintInts will be type func([]int).
var PrintInts = Print«int»

Type assertions and switches

contract reader«T» {
    T.Read([]byte) (int, error)
}

func ReadByte«type T reader»(r T) (byte, error) {
    if br, ok := r.(io.ByteReader); ok {
        return br.ReadByte()
    }
    var b [1]byte
    _, err := r.Read(b[:])
    return b[0], err
}

Instantiating types in type literals

No parsing ambiguity:

x1 := []T(v1)   // type conversion
x2 := []T(v2){} // ([]T)(v2){} - compiler error?
x3 := []T«v3»   // type instantiation
x4 := []T«v4»{} // typed-instantiated slice literal

Using parameterized types as unnamed function parameter types

No parsing ambiguity:

var f func(x(T)) // equivalent to func(x T)
var g func(x«T»)

Embedding a parameterized type in a struct

No parsing ambiguity:

type S1«type T contract{}» struct {
    f T
}

// Instantiated type `S1«int»` embedded
type S2 struct {
    S1«int»
}

// Single field named `S1` of type `(int)`
type S3 struct {
    S1(int)
}

Embedding a parameterized interface type in an interface

No parsing ambiguity:

type I1«type T contract{}» interface {
    M«T»
}

// Embed instantiated type `I1«int»` into `I2`
type I2 interface {
    I1«int»
}

// `I3` has single method named `I1` that takes an argument of type `int`
type I3 interface {
    I1(int)
}

Pointer methods

contract setter«T» {
    T.Set(string)
}

func Init«type T setter»(s string) T {
    var r T
    r.Set(s)
    return r
}

type MyInt int

func (p *MyInt) Set(s string) {
    v, err := strconv.Atoi(s)
    if err != nil {
        log.Fatal("Init failed", err)
    }
    *p = MyInt(v)
}

// INVALID
// MyInt does not have a Set method, only *MyInt has one.
var Init1 = Init«MyInt»("1")

// DOES NOT WORK
// r in Init is type *MyInt with value nil,
// so the Set method does a nil pointer deference.
var Init2 = Init«*MyInt»("2")
contract setter«T» {
    *T.Set(string)
}

Pointer or value methods

func LookupAsString«type T stringer»(m map[int]T, k int) string {
    return m[k].String() // Note: calls method on value of type T
}

type MyInt int
func (p *MyInt) String() { return strconv.Itoa(int(*p)) }
func F(m map[int]MyInt) string {
    return LookupAsString«MyInt»(m, 0)
}

Operators

// This function is INVALID.
func Smallest«type T contract{}»(s []T) T {
    r := s[0] // panics if slice is empty
    for _, v := range s[1:] {
        if v < r { // INVALID
            r = v
        }
    }
    return r
}

Types in contracts

contract SignedInteger«T» {
    T.(int, int8, int16, int32, int64)
}
contract Ordered«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string)
}
func Smallest«type T Ordered»(s []T) T {
    r := s[0] // panics if slice is empty
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Conjunction and disjunction in contracts

// PrintStringer1 and PrintStringer2 are equivalent.
contract PrintStringer1«T» {
    T.String() string
    T.Print()
}

contract PrintStringer2«T» {
    T.String() string; T.Print()
}
contract Float«T» {
    T.(float32, float64)
}
contract IOCloser«S» {
    S.Read([]byte) (int, error), // note trailing comma
        S.Write([]byte) (int, error)
    S.Close() error
}
contract unsatisfiable«T» {
    T.(int)
    T.(uint)
}

Both types and methods in contracts

contract StringableSignedInteger«T» {
    T.(int, int8, int16, int32, int64)
    T.String() string
}
type MyInt int
func (mi MyInt) String() string {
    return fmt.Sprintf("MyInt(%d)", mi)
}

Aggregate types in contracts

contract byteseq«T» {
    T.(string, []byte)
}
type MyByte byte
type MyByteAlias = byte
func Join«type T byteseq»(a []T, sep T) (ret T) {
    if len(a) == 0 {
        // Use the result parameter as a zero value;
        // see discussion of zero value below.
        return ret
    }
    if len(a) == 1 {
        return T(append([]byte(nil), a[0]...))
    }
    n := len(sep) * (len(a) - 1)
    for i := 0; i < len(a); i++ {
        n += len(a[i]) // len works for both string and []byte
    }

    b := make([]byte, n)
    bp := copy(b, a[0])
    for _, s := range a[1:] {
        bp += copy(b[bp:], sep)
        bp += copy(b[bp:], s)
    }
    return T(b)
}

Aggregates of type parameters in contracts

contract Slice«S, Element» {
    S.([]Element)
}
func Map«type S, Element Slice»(s S, f func(Element) Element) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}

type MySlice []int

func DoubleMySlice(s MySlice) MySlice {
    v := Map«MySlice, int»(s, func(e int) int { return 2 * e })
    // Here v has type MySlice, not type []int.
    return v
}
type M«type T contract{}» []T

contract C«T» {
    T.M(T)   // T must implement the method M with an argument of type T
    T.(M«T») // T must be the type M(T)
}

Comparable types in contracts

func Index«type T comparable»(s []T, x T) int {
    for i, v := range s {
        if v == x {
            return i
        }
    }
    return -1
}

Type conversions

contract integer«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr)
}

contract integer2«T1, T2» {
    T1.(integer)
    T2.(integer)
}

func Convert«type To, From integer2»(from From) To {
    to := To(from)
    if From(to) != from {
        panic("conversion out of range")
    }
    return to
}

Untyped constants

contract integer«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr)
}

func Add10«type T integer»(s []T) {
    for i, v := range s {
        s[i] = v + 10 // OK: 10 can convert to any integer type
    }
}

// This function is INVALID.
func Add1024«type T integer»(s []T) {
    for i, v := range s {
        s[i] = v + 1024 // INVALID: 1024 not permitted by int8/uint8
    }
}
The zero value
type Optional«type T contract{}» struct {
    p *T
}

func (o Optional«T») Val() T {
    if o.p != nil {
        return *o.p
    }
    var zero T
    return zero
}
Lots of irritating silly parentheses
    F«int, float64»(x, y)(s)
Defined aggregate types
func Map«type Element contract{}»(s []Element, f func(Element) Element) []Element {
    r := make([]Element, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}
type MySlice []int

func DoubleMySlice(s MySlice) MySlice {
    s2 := Map(s, func(e int) int { return 2 * e })
    // Here s2 is type []int, not type MySlice.
    return MySlice(s2)
}
Identifying the matched predeclared type
contract Float«F» {
    F.(float32, float64)
}

func NewtonSqrt«type F Float»(v F) F {
    var iterations int
    switch v.(type) {
    case float32:
        iterations = 4
    case float64:
        iterations = 5
    default:
        panic(fmt.Sprintf("unexpected type %T", v))
    }
    // Code omitted.
}

type MyFloat float32

var G = NewtonSqrt(MyFloat(64))

Sort

type orderedSlice«type Elem Ordered» []Elem

func (s orderedSlice«Elem») Len() int           { return len(s) }
func (s orderedSlice«Elem») Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice«Elem») Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// OrderedSlice sorts the slice s in ascending order.
// The elements of s must be ordered using the < operator.
func OrderedSlice«type Elem Ordered»(s []Elem) {
    sort.Sort(orderedSlice«Elem»(s))
}
    sort.OrderedSlice«int32»([]int32{3, 5, 2})
    sort.OrderedSlice([]string{"a", "c", "b"}) // type argument inferred
type sliceFn«type Elem» struct {
    s []Elem
    f func(Elem, Elem) bool
}

func (s sliceFn«Elem») Len() int           { return len(s.s) }
func (s sliceFn«Elem») Less(i, j int) bool { return s.f(s.s[i], s.s[j]) }
func (s sliceFn«Elem») Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

// SliceFn sorts the slice s according to the function f.
func SliceFn«type Elem»(s []Elem, f func(Elem, Elem) bool) {
    Sort(sliceFn«Elem»{s, f})
}
    var s []*Person
    // ...
    sort.SliceFn(s, func(p1, p2 *Person) bool { return p1.Name < p2.Name })

Map keys

package maps

// Keys returns the keys of the map m.
// Note that map keys (here called type K) must be comparable.
func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}
    k := maps.Keys(map[int]int{1:2, 2:4}) // sets k to []int{1, 2} (or {2, 1})

Map/Reduce/Filter

// Package slices implements various slice algorithms.
package slices

// Map turns a []from to a []to using a mapping function.
func Map«type from, to contract{}»(sl []from, f func(from) to) []to {
    r := make([]to, len(sl))
    for i, v := range sl {
        r[i] = f(v)
    }
    return r
}

// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce«type val, rdc contract{}»(s []val, initializer rdc, f func(rdc, val) rdc) rdc {
    r := initializer
    for _, v := range s {
        r = f(r, v)
    }
    return r
}

// Filter filters values from a slice using a filter function.
func Filter«type val contract{}»(s []val, f func(val) bool) []val {
    var r []val
    for _, v := range s {
        if f(v) {
            r = append(r, v)
        }
    }
    return r
}
    s := []int{1, 2, 3}
    floats := slices.Map(s, func(i int) float64 { return float64(i) })
    sum := slices.Reduce(s, 0, func(i, j int) int { return i + j })
    evens := slices.Filter(s, func(i int) bool { return i%2 == 0 })

Sets

// Package set implements sets of any type.
package set

type Set«type Elem comparable» map[Elem]struct{}

func Make«type Elem comparable»() Set(Elem) {
    return make(Set«Elem»)
}

func (s Set«Elem») Add(v Elem) {
    s[v] = struct{}{}
}

func (s Set«Elem») Delete(v Elem) {
    delete(s, v)
}

func (s Set«Elem») Contains(v Elem) bool {
    _, ok := s[v]
    return ok
}

func (s Set«Elem») Len() int {
    return len(s)
}

func (s Set«Elem») Iterate(f func(Elem)) {
    for v := range s {
        f(v)
    }
}
    s := set.Make«int»()
    s.Add(1)
    if s.Contains(2) { panic("unexpected 2") }

Channels

package chans

import "runtime"

// Ranger returns a Sender and a Receiver. The Receiver provides a
// Next method to retrieve values. The Sender provides a Send method
// to send values and a Close method to stop sending values. The Next
// method indicates when the Sender has been closed, and the Send
// method indicates when the Receiver has been freed.
//
// This is a convenient way to exit a goroutine sending values when
// the receiver stops reading them.
func Ranger«type T contract{}»() (*Sender«T», *Receiver«T») {
    c := make(chan T)
    d := make(chan bool)
    s := &Sender«T»{values: c, done: d}
    r := &Receiver«T»{values: c, done: d}
    runtime.SetFinalizer(r, r.finalize)
    return s, r
}

// A sender is used to send values to a Receiver.
type Sender«type T contract{}» struct {
    values chan<- T
    done <-chan bool
}

// Send sends a value to the receiver. It returns whether any more
// values may be sent; if it returns false the value was not sent.
func (s *Sender«T») Send(v T) bool {
    select {
    case s.values <- v:
        return true
    case <-s.done:
        return false
    }
}

// Close tells the receiver that no more values will arrive.
// After Close is called, the Sender may no longer be used.
func (s *Sender«T») Close() {
    close(s.values)
}

// A Receiver receives values from a Sender.
type Receiver«type T contract{}» struct {
    values <-chan T
    done chan<- bool
}

// Next returns the next value from the channel. The bool result
// indicates whether the value is valid, or whether the Sender has
// been closed and no more values will be received.
func (r *Receiver«T») Next() (T, bool) {
    v, ok := <-r.values
    return v, ok
}

// finalize is a finalizer for the receiver.
func (r *Receiver«T») finalize() {
    close(r.done)
}

Containers

// Package orderedmap provides an ordered map, implemented as a binary tree.
package orderedmap

import "chans"

// Map is an ordered map.
type Map«type K, V contract{}» struct {
    root    *node«K, V»
    compare func(K, K) int
}

// node is the type of a node in the binary tree.
type node«type K, V contract{}» struct {
    key         K
    val         V
    left, right *node«K, V»
}

// New returns a new map.
func New«type K, V contract{}»(compare func(K, K) int) *Map«K, V» {
    return &Map«K, V»{compare: compare}
}

// find looks up key in the map, and returns either a pointer
// to the node holding key, or a pointer to the location where
// such a node would go.
func (m *Map«K, V») find(key K) **node«K, V» {
    pn := &m.root
    for *pn != nil {
        switch cmp := m.compare(key, (*pn).key); {
        case cmp < 0:
            pn = &(*pn).left
        case cmp > 0:
            pn = &(*pn).right
        default:
            return pn
        }
    }
    return pn
}

// Insert inserts a new key/value into the map.
// If the key is already present, the value is replaced.
// Returns true if this is a new key, false if already present.
func (m *Map«K, V») Insert(key K, val V) bool {
    pn := m.find(key)
    if *pn != nil {
        (*pn).val = val
        return false
    }
    *pn = &node«K, V»{key: key, val: val}
    return true
}

// Find returns the value associated with a key, or zero if not present.
// The found result reports whether the key was found.
func (m *Map«K, V») Find(key K) (V, bool) {
    pn := m.find(key)
    if *pn == nil {
        var zero V // see the discussion of zero values, above
        return zero, false
    }
    return (*pn).val, true
}

// keyValue is a pair of key and value used when iterating.
type keyValue«type K, V contract{}» struct {
    key K
    val V
}

// InOrder returns an iterator that does an in-order traversal of the map.
func (m *Map«K, V») InOrder() *Iterator«K, V» {
    sender, receiver := chans.Ranger(keyValue«K, V»)()
    var f func(*node«K, V») bool
    f = func(n *node«K, V») bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue«K, V»{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// Iterator is used to iterate over the map.
type Iterator«type K, V contract{}» struct {
    r *chans.Receiver(keyValue«K, V»)
}

// Next returns the next key and value pair, and a boolean indicating
// whether they are valid or whether we have reached the end.
func (it *Iterator«K, V») Next() (K, V, bool) {
    keyval, ok := it.r.Next()
    if !ok {
        var zerok K
        var zerov V
        return zerok, zerov, false
    }
    return keyval.key, keyval.val, true
}

This is what it looks like to use this package:

import "container/orderedmap"

var m = orderedmap.New«string, string»(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Append

func Add(s, t []byte) []byte
package slices

// Append adds values to the end of a slice, returning a new slice.
func Append«type T contract{}»(s []T, t ...T) []T {
    lens := len(s)
    tot := lens + len(t)
    if tot <= cap(s) {
        s = s[:tot]
    } else {
        news := make([]T, tot, tot + tot/2)
        copy(news, s)
        s = news
    }
    copy(s[lens:tot], t)
    return s
}
// Copy copies values from t to s, stopping when either slice is
// full, returning the number of values copied.
func Copy«type T contract{}»(s, t []T) int {
    i := 0
    for ; i < len(s) && i < len(t); i++ {
        s[i] = t[i]
    }
    return i
}
    s := slices.Append([]int{1, 2, 3}, 4, 5, 6)
    slices.Copy(s[3:], []int{7, 8, 9})

Metrics

package metrics

import "sync"

type Metric1«type T comparable» struct {
    mu sync.Mutex
    m  map[T]int
}

func (m *Metric1«T») Add(v T) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[T]int)
    }
    m[v]++
}

contract cmp2«T1, T2» {
    T1.(comparable)
    T2.(comparable)
}

type key2«type T1, T2 cmp2» struct {
    f1 T1
    f2 T2
}

type Metric2«type T1, T2 cmp2» struct {
    mu sync.Mutex
    m  map[key2«T1, T2»]int
}

func (m *Metric2«T1, T2») Add(v1 T1, v2 T2) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[key2«T1, T2»]int)
    }
    m[key«T1, T2»{v1, v2}]++
}

contract cmp3«T1, T2, T3» {
    T1.(comparable)
    T2.(comparable)
    T3.(comparable)
}

type key3«type T1, T2, T3 cmp3» struct {
    f1 T1
    f2 T2
    f3 T3
}

type Metric3«type T1, T2, T3 cmp3» struct {
    mu sync.Mutex
    m  map[key3«T1, T2, T3»]int
}

func (m *Metric3«T1, T2, T3») Add(v1 T1, v2 T2, v3 T3) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[key3]int)
    }
    m[key«T1, T2, T3»{v1, v2, v3}]++
}

// Repeat for the maximum number of permitted arguments.

Using this package looks like this:

import "metrics"

var m = metrics.Metric2«string, int»{}

func F(s string, i int) {
    m.Add(s, i) // this call is type checked at compile time
}

List transform

package list

// List is a linked list.
type List«type T contract{}» struct {
    head, tail *element«T»
}

// An element is an entry in a linked list.
type element«type T contract{}» struct {
    next *element«T»
    val  T
}

// Push pushes an element to the end of the list.
func (lst *List«T») Push(v T) {
    if lst.tail == nil {
        lst.head = &element«T»{val: v}
        lst.tail = lst.head
    } else {
        lst.tail.next = &element«T»{val: v }
        lst.tail = lst.tail.next
    }
}

// Iterator ranges over a list.
type Iterator«type T contract{}» struct {
    next **element«T»
}

// Range returns an Iterator starting at the head of the list.
func (lst *List«T») Range() *Iterator«T» {
    return Iterator«T»{next: &lst.head}
}

// Next advances the iterator.
// It returns whether there are more elements.
func (it *Iterator«T») Next() bool {
    if *it.next == nil {
        return false
    }
    it.next = &(*it.next).next
    return true
}

// Val returns the value of the current element.
// The bool result reports whether the value is valid.
func (it *Iterator«T») Val() (T, bool) {
    if *it.next == nil {
        var zero T
        return zero, false
    }
    return (*it.next).val, true
}

// Transform runs a transform function on a list returning a new list.
func Transform«type T1, T2 contract{}»(lst *List«T1», f func(T1) T2) *List«T2» {
    ret := &List«T2»{}
    it := lst.Range()
    for {
        if v, ok := it.Val(); ok {
            ret.Push(f(v))
        }
        it.Next()
    }
    return ret
}

Context

// Key is a key that can be used with Context.Value.
// Rather than calling Context.Value directly, use Key.Load.
//
// The zero value of Key is not ready for use; use NewKey.
type Key«type V contract{}» struct {
    name string
}

// NewKey returns a key used to store values of type V in a Context.
// Every Key returned is unique, even if the name is reused.
func NewKey«type V contract{}»(name string) *Key {
    return &Key«V»{name: name}
}

// WithValue returns a new context with v associated with k.
func (k *Key«V») WithValue(parent Context, v V) Context {
    return WithValue(parent, k, v)
}

// Value loads the value associated with k from ctx and reports
//whether it was successful.
func (k *Key«V») Value(ctx Context) (V, bool) {
    v, present := ctx.Value(k).(V)
    return v.(V), present
}

// String returns the name and expected value type.
func (k *Key«V») String() string {
    var v V
    return fmt.Sprintf("%s(%T)", k.name, v)
}
var ServerContextKey = context.NewKey«*Server»("http_server")

    // used as:
    ctx := ServerContextKey.WithValue(ctx, srv)
    s, present := ServerContextKey.Value(ctx)

Dot product

// Numeric is a contract that matches any numeric type.
// It would likely be in a contracts package in the standard library.
contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Absolute difference

// NumericAbs matches numeric types with an Abs method.
contract NumericAbs«T» {
    T.(Numeric)
    T.Abs() T
}

// AbsDifference computes the absolute value of the difference of
// a and b, where the absolute value is determined by the Abs method.
func AbsDifference«type T NumericAbs»(a, b T) T {
    d := a - b
    return d.Abs()
}
// OrderedNumeric matches numeric types that support the < operator.
contract OrderedNumeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64)
}

// Complex matches the two complex types, which do not have a < operator.
contract Complex«T» {
    T.(complex64, complex128)
}

// OrderedAbs is a helper type that defines an Abs method for
// ordered numeric types.
type OrderedAbs«type T OrderedNumeric» T

func (a OrderedAbs«T») Abs() T {
    if a < 0 {
        return -a
    }
    return a
}

// ComplexAbs is a helper type that defines an Abs method for
// complex types.
type ComplexAbs«type T Complex» T

func (a ComplexAbs«T») Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}
func OrderedAbsDifference«type T OrderedNumeric»(a, b T) T {
    return T(AbsDifference(OrderedAbs«T»(a), OrderedAbs«T»(b)))
}

func ComplexAbsDifference«type T Complex»(a, b T) T {
    return T(AbsDifference(ComplexAbs«T»(a), ComplexAbs«T»(b)))
}
// This function is INVALID.
func GeneralAbsDifference«type T Numeric»(a, b T) T {
    switch a.(type) {
    case int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64:
        return OrderedAbsDifference(a, b) // INVALID
    case complex64, complex128:
        return ComplexAbsDifference(a, b) // INVALID
    }
}

oOo

@caio-northfleet-neon
Copy link

Hi, on the non-parenthetical delimiters, has anyone ever suggested some paradigm shift approach in the way computer languages are "understood"?

Here my point is that the only things available for developers today are text characters... we have nice IDEs that use colors, fonts, etc. to differentiate language elements, which helps a lot code readability), but at the end the source code is a sequence of characters that a compiler translates into machine instructions. Can't just a standardization of colors and fonts solve the case? So no matter if there are parenthesis in there that might be difficult to read, if they are green and bold we can probably see the difference.

That's kind of reading a book, how can we make the reader that a person is walking and not running? That's kind of easy, we can use words that describe what we want readers to compile in their heads... but why after reading The Lord of the Rings and later watching the movie we feel so weird? What we compiled reading the book was mixed with our memory, creating a lot of other things not there in the words written into that book. Sometimes we get frustrated because internally we had a much better "version" of the things, sometimes we get excited because the movie "version" added a lot of other things we didn't imagined when reading the book (not to mention the book was turned into a script in the process).

I am not saying a language should be a screenplay with visual and sound effects, songs, colors, etc., but small enhancements in how we turn words and grammar elements into a printed collection of pages are interesting! Things beyond a black ink on a white paper... take children books as an example, when the main character is excited, it shouts, and that is visually represented with big letters in the page. When we get older our books are just as programming languages... 300 pages of black ink, same font, over and over and over. For our imagination it full plate, we don't want everything to be there... but sometimes watching a movie makes us much more emotional as we see people crying, suffering, dying, etc.

Anyway, I was just sitting here looking for the Go 2 status and ended up reading on this gist, and felt like proposing a more childish and colorful coding experience. At the end of the month we get our salaries by coding lines and lines of text, but at the end of each day we hope to be artists, craftsmen, inventors, isn't it?!

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