Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Proposal for Go Range Types

Proposal: Go 2 Range Types

B.

Last updated: 15 march, 2019

Discussion at:

golang/go/issues/30428 (the proposal itself)
golang/go/issues/30613 (checked integer types)
golang.org/issue/29649 (integer range contraints)
golang.org/issue/28987 (enums as an extension to types)

Abstract

I propose these related changes to the type system in Go:

  • Add bounded range types with upper and lower bounds as in type Range range float64 [-5.0:5.0]
  • Add enumerated range types that can only have concrete values from a list as in type Enumerated range int {7, 10, -1, 22}
  • Add named range types that have enumerated names with values as in type EnumeratedWithNames range int {Foo = 7, Bar = 10, Baz = -1, Quux = 22}. These can be used like this: a := EnumeratedWithNames.Foo ; b := EnumeratedWithNames.Quux ;. Or even a := range int {Foo = 7, Bar = 10, Baz = -1, Quux = 22}.Foo, for consistency, although that is pretty useless.
  • The user of iota is allowed for named enumerated types like this: type Weekday range int { Monday = iota, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
  • The zero value of a bounded range type is the zero value of the underlying type if it is in range for that bounded range type, otherwise the zero value is its lower bound. For an enumerated range type the zero value is the value of the first member.
  • Extend the range clause to allow enumeration of range types.
  • Extend the built in len() function to allow calling it with range types.
  • Cause all assignments of constants of range type to be checked at compile time.
  • Cause all assignments to variables ranged type to cause a panic if a value out of bounds or not one of the enumerated values is assigned to a variable of a range type. The compiler may, if possible, check the range for variables at compile time in stead of causing a panic at run time, but it will be implementation defined for which variable assignments compile time checking is done or not.
  • Allow all assignments to variables of range type, enumerated, named, and bounded to use the result, ok := form in which case no exception will be raised if a value out of bounds or not one of the enumerated values would be assigned to result, but in stead the assignment does not happen and ok is set to false. Otherwise the assignment happens and ok is set to true.
  • Cause all calculations on values of numeric all range types not to overflow, if they exeed the range of the underlying type, but cause a panic, or in case a result, ok := form is used, the result will be set to the zero value of the type, and ok will be set to false.

Background

Go currently does not support enumerated types nor ranged types. The former is a widely requested feature, but can be considered a special case of the latter.

Enumerated types are widely availabe in many languages, Go is one of the few languages wich does not have this feature. Ranged types appear in several Wirth languages such as Pascal, Ada and Module-3. Both enumerated and ranged types are useful in application where constants and variables may only assume certain values or must be guaranteed to be in certain ranges. They are also useful as a general purpose mechanism to control integer overflow.

Proposal

I propose to add range types to Go language. Range types come in three flavours, namely bounded, enumerated and named. Range types are based on an underlying type that must have as underlying type or alias, a primitive numeric go type, that is not a complex type, namely one of int8, int16, int32, int64, int, uint8, uint16, uint32, uint64, uint, float32, float64. Enumerated range types and named range types may also have an underlying type that has an underlying type of or alias to string.

Bounded range types have inclusive upper and lower bounds, between which their values must stay or be equal to upon any assignment. They are defined as range <underlying_type> [<lower_bound>:<upper_bound>]. For bounded ranges, <underlying_type> as well as <lower_bound> and <upper_bound> cannot be a string. Either <lower_bound> and <upper_bound> may be omittted. If ommitted the <lower_bound> defaults to the lower bound of the underlying type, or, if the underlying type is a primitive numeric type, then it defaults to math.Min<type>. If ommitted the <upper_bound> defaults to the upper bound of the underlying type, or, if the underlying type is a primitive numeric type, then it defaults to math.Max<type>.

Enumerated range types that can only have concrete values from a list in their definition. They are defined as range <underlying_type> { <value>, ... }. For enumerated range types the <value> must be a constant expression, that is, one which the compiler can completely resolve the value of at compile time.

Named range types have enumerated names with values as from a list in their definition. They are defined as range <underlying_type> { <name> = <value>, ... }. For named range types the <name> must be a valid go identifier. The values of a name of a named range can be accessed as <range type>.<name>. Methods defined for the '' may not shadow these names. Names defined for a range follow the normal Go export rules.
If the first character of a <name> is upper case, then it is exported and can be used in other packages, otherwise, it can only be used within the current package. Here again, <value> must be a constant expression, that is, one which the compiler can completely resolve the value of at compile time. The constant expression may contain iota, after which the consecutive values of the enumerated types will be set just as if they were defined consecutively in a const block.

For assignments to a constant of a ranged type, the compiler checks at compile time that the assigned value respects either the bounds of the bounded range type, or is one of the enumerated values of an enumerated range type. The compiler emits a compile error if the value assigned to a constant can be proven to be not in range.

For assignments to variables of ranged type, the compiler emits a run time check that causes a recoverable panic 'range: out of range: allowed <bounds|values>', if the value of a variable of a range type would go out of range due to the assignment. If the assignment is to a package level variable, then compiler generates a synthetic init() function that performs the actual check, and from where the the panic is raised in case the value is out of range.

The compiler may, if possible, also check assignments to variables at compile time as for constants. It is implementation defined which variable assignments are checked at compile time.

The compiler performs the range check described in the previous paragraphs only at assignment. Any intermediate values during the calculation of the value of an expression that are out of range are not checked and do not cause a panic. For any calculations using the Go operators and built in functions, a ranged type acts as if it was a value or variable of it's underlying type.

For all assignments to ranged types, the result, ok := expression form will be made available, which will not cause a panic but in stead set ok is set to false and does not execute the assignment to result if the constraints of the ranged type would not be respected. Otherwise the assignment happens and ok is set to true. For assignments where the compiler is able to check at compile time that the assignment will always be in range, this form is equivalent to result := expression; ok := true.

For calculation involving values of ranged type with an underlying numerical type, the compiler emits a run time check that causes a recoverable panic 'range: overflows : allowed <bounds|values>', if during the calculation, the value of that calculation would overflow the underlying type of the range type. If the calculation happens in an assignment to a package level variable, then compiler generates a synthetic init() function that performs the actual overflow check, and from where the the panic is raised in case the value is out of range. However, in this case of overflow of the underlying type, if the result, ok := form is used, no panic will be caused, but in stead, the result will be set to the zero value of the type, and ok will be set to false.

Notwithstanding what is written above, if the implementation of the ,ok form is initially too complex, then this form might be omitted until a later version of the compiler, seeing that the range check panics and overflow panics are recoverable.

The zero value of a bounded range type is the zero value of the underlying type if it is in range for that bounded range type, otherwise the zero value is its lower bound. For an enumerated range type the zero value is the value of the first member.

The for i := range statement will be enhanced to allow iteration over ranged types. For bounded range types the iteration will start at the lower bound and be incremented by 1 until the value would become strictly higher than the upper bound. For enumerated and named types all values will be produces in order of definition.

The built in len() funcion will be enhanced to return either T(high - low) for variables or range type literals of type range T[low:high]. The built in len() funcion will return the number of range enumeration members for variables or range type literals of enumerated ranges.

Language Changes

The definitions in https://golang.org/ref/spec#Types add:

TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
	    SliceType | MapType | ChannelType | RangeType

A new section "Range Types" adds:

RangeType = "range" ElementType RangeBoundaries | RangeEnumeration
RangeBoundaries = "[" { RangeLowerBound } ":" { RangeUpperBound } "]"
RangeLowerBound = ConstantExpression
RangeUpperBound = ConstantExpression
RangeEnumeration = "{" [ RangeEnumerationList [ "," ] ] "}" .
RangeEnumerationList  = RangeEnumerationMember { "," RangeEnumerationList } .
RangeEnumerationMember = RangeEnumerationAnonymousMember | RangeEnumerationNamedMember .
RangeEnumerationAnonymousMember =  ConstantExpression .
RangeEnumerationNamedMember = RangeMemberName { "=" ConstantExpression } .
ElementType = Type .
RangeMemberName = identifier .

For RangeBoundaries, RangeLowerBound and RangeUpperBound, if present, must be integer or floating point constant expressions. Complex constants expressions are not allowed. RangeLowerBound must be lower than or equal to RangeUpperBound. The ElementType for range types must have an underlying type or alias, that is a primitive go type, that is, one of int8, int16, int32, int64, int, uint8, uint16, uint32, uint64, uint, float32, float64, or string. ElementType then is the underlying type of the ranged type. Underlying complex types, namely complex64 or complex128 are not allowed for range types.

The ConstantExpression that defines the value of a RangeEnumerationMember may contain iota. In this case, the value of following RangeEnumerationNamedMember will be defined as if the definitions were in a const block.

A new section Range Epressions adds:

RangeExpression = RangeType "." RangeMemberName | Type "." RangeMemberName

The Type in a RangeExpression must be a type defined as an enumerated range type, and the RangeMemberName must be present in the RangeEnumerationList of that enumerated range type.

At the end of the section https://golang.org/ref/spec#Iota add:

Within an enummerated range type declaration, iota also represents successive untyped integer constants. Its value is the index of the respective RangeEnumerationNamedMember in that constant declaration, starting at zero. This can be used to ensure the values of the enumerated type are consecutive as in:

type Month range int { January = 1 + iota, February, March, April, May, June, July, September, October, November, December }

type Weekday range int { Monday = iota, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }

The definition in https://golang.org/ref/spec#RangeClause is modified to read:

For statements with range clause

A "for" statement with a "range" clause iterates through all entries of an array, slice, string or map, values received on a channel, or values of a range type. For each entry it assigns iteration values to corresponding iteration variables if present and then executes the block.

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

The expression on the right in the "range" clause is called the range expression, which may be an array, pointer to an array, slice, string, map, channel permitting receive operations, or a range type literal. As with an assignment, if present the operands on the left must be addressable or map index expressions; they denote the iteration variables. If the range expression is a channel, at most one iteration variable is permitted, otherwise there may be up to two. If the last iteration variable is the blank identifier, the range clause is equivalent to the same clause without that identifier.

A point 6 is added to the list in https://golang.org/ref/spec#RangeClause:

  1. For range types, with an enumeration list, the successive values in the same order as in the enumeration's definition. For range types with boundaries, the range's lower bound, then that value incremented by 1 or 1.0, for all values smaller than or equal to the upper bound. If no values that are in range for the range type can be produced like this, then the loop is not executed.

In https://golang.org/ref/spec#Assignments a new subsection "Range Checks" adds:

For assignments to a constant of a ranged type, the compiler checks at compile time that the assigned value respects either the bounds of the bounded range type, or is one of the enumerated values of an enumerated range type. The compiler emits a compile error if the value assigned to a constant can be proven to be not in range.

For assignments to variables of ranged type, the compiler emits a run time check that causes a panic 'range: out of range: allowed <bounds|values>', if the value of a variable of a range type would go out of range due to the assignment. If the assignment is to a package level variable, then compiler generates a synthetic init() function that is executed before all other manually defined init() functions, that performs the actual check at run time, and from where the the panic is caused in case the value is out of range.

The compiler may, if possible, also check assignments to variables at compile time as for constants. It is implementation defined which variable assignments are checked at compile time.

The compiler performs the range check described in the previous paragraphs only at assignment. Any intermediate values during the calculation of the value of an expression that are out of range are not checked and do not cause a panic. For any calculations using the Go operators and built in functions, a ranged type acts as if it was a value or variable of it's underlying type.

For all assignments to ranged types, the result, ok := expression form is available, which will not cause a panic but in stead set ok is set to false and does not execute the assignment to result if the constraints of the ranged type would not be respected. Otherwise the assignment happens and ok is set to true. For assignments where the compiler is able to check at compile time that the assignment will always be in range, this form is equivalent to result := expression; ok := true.

Amend the section https://golang.org/ref/spec#The_zero_value to read:

The zero value

When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. The zero value of bounded range types is the zero value of the underlying type if that is in range for the bounded range type in question. Otherwise it is the lower bound of that bounded range type. The zero value of enumerated range types is the value of the first range member. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.

Amend the section https://golang.org/ref/spec#Length_and_capacity to read:

Length and capacity

The built-in functions len and cap take arguments of various types and return a result of type int. The implementation guarantees that the result always fits into an int.

Call      Argument type    Result

len(s)    string type        			string length in bytes
          [n]T, *[n]T        			array length (== n)
          []T                			slice length
          map[K]T            			map length (number of defined keys)
          chan T             			number of elements queued in channel buffer
	  range T[low:high]  			int(high - low)
	  range T{Name=Val,...} 		number of range enumeration members	  
	  type T range U[low:high] 		int(high - low)
	  type T range T{Name=Val,...} 		number of range enumeration members

cap(s)    [n]T, *[n]T      array length (== n)
          []T              slice capacity
          chan T           channel buffer capacity

The capacity of a slice is the number of elements for which there is space allocated in the underlying array. At any time the following relationship holds:

0 <= len(s) <= cap(s)

The length of a nil slice, map or channel is 0. The capacity of a nil slice or channel is 0.

The expression len(s) is constant if s is a string constant or a range constant. The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated. Otherwise, invocations of len and cap are not constant and s is evaluated.

const (
	c1 = imag(2i)                    // imag(2i) = 2.0 is a constant
	c2 = len([10]float64{2})         // [10]float64{2} contains no function calls
	c3 = len([10]float64{c1})        // [10]float64{c1} contains no function calls
	c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
	c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128

Library Changes

The reflect package will have to be updated to support the new range types.

Rationale

Enumerated types are widely supported in many languages, yet Go still lacks them. Bounded range typos are implemented in Wirth languages such as Ada because they are useful for critical software where certain values must be guaranteed to always be in a specific range. Furthermore, integer overflow currently wraps in Go language, however, in many cases, this is not desirable, and a panic or a check would be better on integer overflow.

This proposal adds both enumerated types, and bounded integers as a way to control the range of integers, including integer overflow, to go language, yet keeps these features easy to learn, general use, and Go like, and adds these features in a strictly backwards compatible way.

Compatibility

The syntaxes being introduced here were all previously invalid syntactically.

The implementation requires:

  • Language specification changes, detailed above.
  • Library changes, detailed above.
  • Compiler changes, in gofrontend and cmd/compile/internal/syntax.
  • Testing of compiler changes, library changes, and gofmt.

License

MIT License

Copyright (c) 2019 Beoran

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.