Skip to content

Instantly share code, notes, and snippets.

@hdu-hh
Last active August 1, 2019 21:32
Show Gist options
  • Save hdu-hh/78ca8d3a40bda996c91ce9c1f558337c to your computer and use it in GitHub Desktop.
Save hdu-hh/78ca8d3a40bda996c91ce9c1f558337c to your computer and use it in GitHub Desktop.
Golang generics proposal extended with contract outcomes and contract switches

The proposal extends the Go 2 contracts (generics) draft design in four ways:

The gist of it is that the use of contracts should be explicit. For this contracts should act as their own adapters. Fundamental and custom types can be coerced to behave in a common way. The proposed contract switch is essential for this. Contracts need to become real executable code with a few special rules.

Contract Outcomes

Contracts can declare their intended outcomes. They become real code for adapting types to this outcome:

contract WithLength(a T) int {
	return len(a)
}

Applying contract outcomes

Contract outcomes can be applied directly:

func Max(type T Less)(first T, other ...T) T {
	max := first
	for _, val := range other {
		if Less(max, val) {
			max = val
		}
	}
	return max
}

var x = Max(5, 3, 8)

Contracts with no declared outcomes cannot be applied.

As types can abide multiple contracts it is helpful to know which contract is applied where. Using the contract name for that purpose led to a design with contracts becoming their own adapters.

Contract Switches

Contract switches react on the semantics of their case statements.

contract Less(a,b T) bool {
	switch {
	case: return a.Less(b)
	case: return a < b
	}
}

Case statements with semantic errors are silently ignored. Syntax errors are treated as always. The first applicable case statement is taken.

A type is considered to abide a contract if there is a valid code path to an explicit return statement. Contracts without declared outcomes don't need the return statement though. This allows contracts to steer types to behave in the most suitable way if any.

Control Flow Constraints

In contracts the control flow must not change outside of contract switch case statements.

Customized Error Reporting

Contracts may customize error reporting for specific cases. A code path that ends in a panic() reports its message as error:

contract Hash(t T) uint {
	switch {
	case: t = []byte{}; panic("byte slice hashing is not supported yet")
	case: return t.Hash()
	case: return uint(t)
	}
}

Multiple Contracts per Generic Type

Conformance to multiple contracts is a common trait of types. Requiring extra contracts just to combine them is bothersome, so a syntax for declaring multiple contracts per generic type is proposed:

type HashMap(type K Equal*Hash, type V) struct {
	buckets [][]struct{key K; val V}
}

contract Equal(a, b T) bool {
	switch {
	case: return a.Equals(b)
	case: return a == b
	case: return string(a) == string(b)
	}
}

contract Hash(t T) uint {
	switch {
	case: return t.Hash()
	case: return uint(t)
	}
}
@hdu-hh
Copy link
Author

hdu-hh commented Jul 29, 2019

Many thanks for your positive feedback and the ideas!

First I'd like to make some design decisions more clear

  • Unifying generics to work the same for native and custom types and constants was one of my major goals
  • As types can abide multiple contracts at the same time it is good to know which contract is applied, so I'd like to make that explicit; using the contract name for that lead to contracts becoming their own adaptors
  • Another goal was to make contracts easy to read by using real golang-like code instead of a domain specific language just for for the one purpose
    • This helps to reduce the learning curve by allowing e.g. single stepping through the contract code
    • Identifying why a type does not abide a contract could be easily done by enabling error reporting for contract switch case statements, which is disabled by default

Having a way to ensure that the expressions/statements in each branch are equivalent would be very nice. But if this can only be accomplished by restricting the branches too tightly then the price is high, especially if we'd lose

  • the chance of prodding a type from multiple aspects
  • implementing simple algorithms
  • custom error reporting

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