Skip to content

Instantly share code, notes, and snippets.

@dddent
Last active June 24, 2020 03:03
Show Gist options
  • Save dddent/6e4a7490f80cb427c0e910ebca5c3468 to your computer and use it in GitHub Desktop.
Save dddent/6e4a7490f80cb427c0e910ebca5c3468 to your computer and use it in GitHub Desktop.

Problems With Using Parentheses for Type Argument Lists

Changes to the syntax of type parameters have been suggested multiple times by now. And while there may be a point to make about readability, function declarations using type parameters as described in the proposal will stay unambiguous. However, ambiguity arises when it comes to the proposed way of using type arguments, as call expressions may look the same, despite having different meanings depending on whether their arguments are types or values.

While it seems to be consensus in the community that using different brackets for type parameters and arguments by now, the following points have not been mentioned yet and further support the change.

Drawbacks when reading code

Assume we have a generic function that doubles an addable type:

contract addable(t T) {
	var a T = t + t
}

func doubleIt(type T addable)(n T) T {
	return n + n
}

Encountering a statement like var a = doubleIt(amount), without knowing about "amount", would not be clear about what is being assigned to a. It might either be a function of type func(amount)amount or a value of the same type as amount, depending on whether amount is a type or a value.

To the reader, it is not clear from the statement itself, whether amount is a type or a value, as the signature of doubleIt would allow for both here. This almost reminds of function overloading, which was intentionally left out of the go spec:

Method dispatch is simplified if it doesn't need to do type matching as well. Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice. Matching only by name and requiring consistency in the types was a major simplifying decision in Go's type system. (from the go faq)

Drawbacks when writing code

When writing code, the syntax prevents the developer to clearly state the intention to the compiler. This might lead to problems like the following (using the same doubleIt function from above):

type Amount int

func foo() Amount {
	var amount Amount = 5
	a := doubleIt(Amount) // no error
	return a + 5 // error
}

While the compiler would catch an error here, it would not be where the actual mistake was made, which would be where type Amount was passed as an argument, instead of value amount. If the developer was able to clearly state whether the argument should be a type or a value argument, the compiler could catch the error where the mistake occurred. More complex examples can be constructed, where the compiler would not catch the mistake at all (see further examples at the bottom).

This gets even more problematic considering that types and variables may have the same name:

type amount int

func foo() amount {
	var amount amount = 5
	a := doubleIt(amount) // no error
	return a(7) // error
}

Again, the error should have been encountered in the assignment, like it would have if the value was used as a type anywhere else. E.g.:

type amount int

func foo() amount {
	var amount amount = 5
	var b amount = 7 // error is correctly encountered here
	return amount + b
}

All these problems can be prevented by using a different syntax for type arguments to make intentions clearer to reader and compiler alike. While admittedly most alternatives would not win any beauty contests, they would at least get rid of the ambiguities that come with using the same for both argument lists. While the actual choice might be subject to taste more than anything, I would suggest using angle brackets, as they are widely used as type arguments in other languages such as Typescript, C++ or C# and are also not used in go yet (at least for argument lists), making type arguments easy to distinguish from other uses of brackets (like slice / map index expressions or function arguments).

Using angle brackets, all problems from the examples above would result in correct errors:

type Amount int

func foo() Amount {
	var amount Amount = 5
	a := doubleIt(Amount) // "type Amount is not an expression" error
	return a + 5
}
type amount int

func foo() amount {
	var amount amount = 5
	a := doubleIt<amount> // "amount is not a type" error
	return a(7)
}

Point Against Using Square Brackets

Using square brackets, as suggested in alanfos writeup Proposed changes to the Go draft generics design in the light of feedback received would still lead to some ambiguities. Certainly not as severe as the ones mentioned above, there would still be expressions that look the same, despite meaning different things:

var sl = make([](func (int) int), 0)

func generic[type T](a T) T {
	return a
}

type Index int
var index = 0

func theyLookTheSame() {
	a := sl[index](5)
	b := generic[Index](5)
}

While these examples are certainly not too severe, especially as the compiler would recognize mistakes right away, they might still be confusing to someone reading the code, as both "sl" and "generic", without knowing about "index" / "Index", might be slices or generic functions. Using angle brackets here would make them clearly distinguishable to the reader, without the need to look up their definitions.

Further Examples

A more in depth example, in which the compiler would not catch the mistake at all:

contract smallerZeroComparable (t T) {
	t < 0
}

func nilSmallerZero(type T smallerZeroComparable)(a T) *T {
	if a < 0 {
		return nil
	}
	return &a
}

type Amount int

func whereTheMistakeHappens() {
	var amount Amount = -1
	a := nilSmallerZero(Amount)
	if a == nil {
		fmt.Printf("%d is smaller zero", amount)
	} else {
		fmt.Printf("%d is not smaller zero", amount)
	}
}

In this case the function would print "-1 is not smaller zero", as it points to the function nilSmallerZero(Amount), and not the result of nilSmallerZero called with amount as its argument. This is a mistake that could be easily prevented at compile time by allowing the developer to state whether the intention is to pass type or actual parameters.

@ianlancetaylor
Copy link

Thanks for the thoughts.

Angle brackets are problematic because they introduce (at least) two ambiguous parses.

    v := a<b>c // currently parses as (a<b) > c

    v := List<List<int>> // final >> parses as right shift operator

@jsipprell
Copy link

Perhaps require the type keyword when calling something that takes a generic as well as when declaring it?

e.g.

a := doubleIt(type Amount)
return a(Amount(7))

@antoniomo
Copy link

antoniomo commented Aug 1, 2019

While the draft clearly explains why F<T>, F[T] and non-ASCII (unable to type it here) F<<T>> where discarded, feels like F{T} would be more human readable than the sometimes three in a row set of (), while not complicating the parser with unbounded lookahead as you can't open a block in those circumstances.

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