Skip to content

Instantly share code, notes, and snippets.

@andybalholm
Last active October 26, 2018 17:36
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 andybalholm/8165da83c10a48e56590c96542e93ff2 to your computer and use it in GitHub Desktop.
Save andybalholm/8165da83c10a48e56590c96542e93ff2 to your computer and use it in GitHub Desktop.
Enumerated and structural contracts

(This is a refinement of Burak Serdar's proposal at https://gist.github.com/bserdar/8f583d6e8df2bbec145912b66a8874b3)

Enumerated Contracts

The simplest way to specify a contract is to provide a list of the types that fulfill it:

contract byteSequence string, []byte
contract unsigned uint, uint8, uint16, uint32, uint64, uintptr
contract signed int, int8, int16, int32, int64

For any of the types listed that do not have methods, the contract will also be fulfilled by named types derived from it. For example, time.Duration fulfills the signed contract.

In addition to types, an enumerated contract can also contain other contracts:

contract integer signed, unsigned
contract orderedPrimitive integer, float32, float64, string

Structural Contracts

A structural contract (so called because it operates on the principle of structural typing) is a lot like an interface, but it can also include fields:

contract reader {
	io.Reader
}

contract lesser {
	Less(b lesser) bool
}

contract linkedListNode {
	Next *linkedListNode
}

In the body of a structural contract, contract names may be used as if they were types. The most common case of this is when a contract contains a reference to itself, like the lesser and linkedListNode contracts above. In that case, the type that fulfills the contract must reference itself in the same way. For example, the following myNode type would fulfill linkedListNode:

type myNode struct {
	Val string
	Next *myNode
}

It is also possible for contracts to reference each other, to define a more complex relationship between types:

contract Node {
	Edges() []Edge
}

contract Edge {
	Nodes() (from, to Node)
}

func ShortestPath(type N Node, E Edge)(src, dst N) []E

When a set of type parameters involves contracts that reference each other, the types that are supplied must fulfill the contracts in a compatible way. Each contract must be consistently fulfilled by the same type. For example, the Edges method of the type supplied for N must return a slice of the type supplied for E. You can't use a Node type from one type of graph, and an Edge type from another.

(Note that in this proposal, contracts are per type, rather than covering the whole list of type parameters.)

The Built-in Contract comparable

There is one built-in contract, with the predeclared identifier comparable. It is fulfilled by types that support == and !=, and can be used as map keys.

(In my first draft, I allowed using operators in structural contracts. But I realized that the only one that adds any expressive power is ==, because the other operators are limited to a specific set of primitive types (plus named types based on them). So they can be specified by simply enumerating the types that support the operator.

And the contract for supporting == should probably be sort of a special case anyway, since it should also allow for the use of != and using the type as a map key.)

Contracts in Type Switches

A type switch can be used with a type parameter in the switch line, and contracts or individual types in the case lines. This allows writing generic functions that operate on types that may fulfill any one of several contracts:

contract ordered orderedPrimitive, lesser

func Min(type T ordered) (a, b T) T {
	switch T.(type) {
	case lesser:
		if a.Less(b) {
			return a
		}
		return b
	case orderedPrimitive:
		if a < b {
			return a
		}
		return b
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment