blob: cb9933250377419b3d0b57854a2af5d893276e7e [file] [log] [blame] [view]
# Compile-time Functions and First Class Types
This is a proposal for adding compile-time functions and first-class types to
Go, written by Bryan C. Mills in September, 2016.
This proposal will most likely not be adopted.
It is being presented as an example for what a complete generics proposal must
cover.
## Background
This is an alternative to an
[earlier proposal](https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md)
and subsequent drafts by Ian Lance Taylor.
Ian's earlier proposal notes:
> [T]he right way to implement such features [as template metaprogramming] in
> Go would be to add support in the go tool for writing Go code to generate Go
> code […] which is in turn compiled into the final program. This would mean
> that the metaprogramming language in Go is itself Go.
This proposal is an attempt to drive that observation to its natural conclusion,
by adding explicit support for running Go functions over types at compile-time.
It results in a variant of Go with deep support for parametric types and
functions, despite relatively little addition to the language.
This proposal is intended to be Go 1 compatible.
If any incompatibilities exist, they are likely to be due to the extension of
`TypeName` to include compile-time expressions and/or due to grammar conflicts
with the expanded definition of `Declaration`.
## Proposal
We introduce a new builtin type, `gotype`, which represents a type within Go
program during compilation, and a new token which creates a constant of type
`gotype` from a [`Type`](https://golang.org/ref/spec#Type).
We permit a subset of Go functions to be called at compile-time.
These functions can accept and return values of type `gotype`.
The `gotype` values returned from such functions can appear as types within the
program.
### Syntax
`const`, applied at the start of a
[`FunctionDecl`](https://golang.org/ref/spec#FunctionDecl), indicates a
declaration of a compile-time function.
ℹ There are many reasonable alternatives to the `const` token.
We could use a currently-invalid token (such as `::` or `<>`), a new builtin
name (such as `static`), or, in fact, nothing at all!
In the latter case, any `FunctionDecl` that meets the constraints of a
compile-time function would result in a function which could be called at
compile time.
The new builtin type `gotype` represents a Go type at compile-time.
`(type)`, used in place of a
[`FunctionBody`](https://golang.org/ref/spec#FunctionBody),
[`LiteralValue`](https://golang.org/ref/spec#LiteralValue), or the `Expression`
in a [`Conversion`](https://golang.org/ref/spec#Conversion), represents "the
type itself".
It produces a constant of type `gotype`.
The actual syntax for the `(type)` token could be a currently-invalid
token (such as `<>` or `{...}`), an existing keyword that cannot currently occur
in this position (such as an unparenthesized `type`), or a new builtin
non-keyword (such as `itself`, `typeof`, or `gotype`).
The `.(type)` syntax already supported in type switches can be used as a general
expression within compile-time functions.
It returns the `gotype` of the `interface` value to which it is applied.
In order to support defining methods on types within compile-time functions, we
expand the definition of a
[`Declaration`](https://golang.org/ref/spec#Declaration) to include
[`FunctionDecl`](https://golang.org/ref/spec#FunctionDecl) and
[`MethodDecl`](https://golang.org/ref/spec#MethodDecl), with the restriction
that method declarations must occur within the same scope as the corresponding
type declaration and before any values of that type are instantiated.
For uniformity, we also allow these declarations within ordinary
(non-compile-time) functions.
ℹ As a possible future extension, we could allow conditional method declarations
within a compile-time function before the first use of the type.
Expressions of type `gotype` can appear anywhere a
[`TypeName`](https://golang.org/ref/spec#TypeName) is valid, including in the
parameters or return-values of a compile-time function.
The compiler substitutes the concrete type returned by the function in place of
the `gotype` expression.
`gotype` parameters to a compile-time function may be used in the types of
subsequent parameters.
ℹ In this proposal, functions whose parameter types depend on other parameters
do not have a well-formed Go type, and hence cannot be passed or stored as
variables with `func` types.
It may be possible to add such
[dependent types](https://en.wikipedia.org/wiki/Dependent_type) in the
future, but for now it seems prudent to avoid that complexity.
### Semantics
Compile-time functions and expressions cannot depend upon values that are not
available at compile-time.
In particular, they cannot call non-compile-time functions or access variables
at package scope.
ℹ As a possible future extension, we could allow compile-time functions to read
and manipulate package variables.
This would enable more `init` functions to be evaluated at compile-time,
reducing the run-time cost of package initialization (and allowing Go programs
to load more quickly).
However, it would potentially remove the useful invariant that all
compile-time functions are "pure" functions.
The values returned from a call to a compile-time function are
[constants](https://golang.org/ref/spec#Constants) if expressions of the
corresponding types are otherwise valid as constants.
Arguments to compile-time functions can include:
* constants (including constants of type `gotype`)
* calls to other compile-time functions (even if they do not return constants)
* functions declared at package scope
* functions and variables declared locally within other compile-time functions
* [function literals](https://golang.org/ref/spec#Function_literals), provided
that the body of the literal does not refer to the local variables of a
run-time function or method
It is an error to write a compile-time expression that depends on a value not
available at compile-time.
Passed-in run-time functions cannot be called directly within the compile-time
function to which they are passed, but they can be called by other (run-time)
functions and/or methods declared within it.
⚠ Can / should we allow compile-time functions to accept and call other
compile-time functions passed as parameters?
Run-time functions and methods declared within compile-time functions may refer
to local variables of the compile-time function.
Calls to compile-time functions from within run-time functions are evaluated at
compile-time.
(This is a form of
[partial evaluation](https://en.wikipedia.org/wiki/Partial_evaluation).)
Calls to compile-time functions from other compile-time functions are evaluated
when the outer function is called.
⚠ Should we allow compile-time functions that do not involve `gotype` to be
called at run-time (with parameters computed at run-time)?
Expressions of type `gotype` can only be evaluated at compile-time.
ℹ There is an important distinction between "an expression of type `gotype`" and
"an expression _whose type is_ an expression of type `gotype`" (a.k.a. "an
expression whose type is _a_ `gotype`).
The former is always a compile-time expression; the latter is not.
If an expression's type is a `gotype` but the concrete type represented by
evaluating the `gotype` does not support an operation used in the expression, it
is a compile-time error.
If the expression occurs within a compile-time function and the expression is
not evaluated, there is no error.
`gotype` expressions within type and method declarations are only evaluated if
the block containing the type declaration is evaluated.
(This is analogous to the
[SFINAE rule in C++](https://en.cppreference.com/w/cpp/language/sfinae).)
A [`TypeSwitchStmt`](https://golang.org/ref/spec#TypeSwitchStmt) on an
expression or variable of type `gotype` switches on the concrete type
represented by that `gotype`, not the type `gotype` itself.
As a consequence, a `TypeSwitchStmt` on a `gotype` cannot bind an `identifier`
in the `TypeSwitchGuard`.
⚠ It would be useful in some cases to be able to convert between a `gotype` and
a `reflect.Type`, or to otherwise implement many of the `reflect.Type` methods
on the `gotype` type.
For example, in the `hashmap` example below, the `K` parameter could be
eliminated by changing the `hashfn` and `eqfn` parameters to `interface{}` and
reflecting over them to discover the key type.
Is that worth considering at this point?
#### Type names
The name of a type declared within a function is local to the function.
If a local type is returned (as a `gotype`), it becomes
an [unnamed type](https://golang.org/ref/spec#Types).
ℹ Local types introduce the possibility of unnamed types with methods.
Two unnamed types returned as `gotype` are
[identical](https://golang.org/ref/spec#Type_identity) if they were returned by
calls to the same function with the same parameters.
If the function itself was local to another compile-time function, this applies
to the parameters passed to the outermost function that returns the `gotype`.
### Examples
```go
// AsWriterTo returns reader if it implements io.WriterTo,
// or a wrapper that embeds reader and implements io.WriterTo otherwise.
const func AsWriterTo(reader gotype) gotype {
switch reader.(type) {
case io.WriterTo:
return reader
default:
type WriterTo struct {
reader
}
func (t *WriterTo) WriteTo(w io.Writer) (n int64, err error) {
return io.Copy(w, t.reader)
}
return WriterTo (type)
}
}
const func MakeWriterTo(reader gotype) func(reader) AsWriterTo(reader) {
switch reader.(type) {
case io.WriterTo:
return func(r reader) AsWriterTo(reader) {
return r
}
default:
return func(r reader) AsWriterTo(reader) {
return AsWriterTo(reader) { r }
}
}
}
```
```go
func redundantButOk(b *bytes.Buffer) io.WriterTo {
return MakeWriterTo(*bytes.Buffer)(b) // ok: takes the io.WriterTo case
}
func maybeUseful(r *io.LimitedReader) io.WriterTo {
return MakeWriterTo(*io.LimitedReader)(r) // ok: takes the default case
}
const func fused(reader gotype) (writerTo gotype, make func(reader) writerTo) {
writerTo = AsWriterTo(reader) // ok: gotype var in a compile-time function
return writerTo, MakeWriterTo(writerTo) // ok: call of compile-time function with compile-time var
}
// bad always produces a compile-time error.
func bad(i int) io.WriterTo {
return MakeWriterTo(int)(i) // error: 'int' does not implement 'io.Reader'
}
// hiddenError produces a compile-time error only if it is called.
const func hiddenError(i int) io.WriterTo {
return MakeWriterTo(int)(i) // error: 'int' does not implement 'io.Reader'
}
```
```go
const func List(T gotype) gotype {
type List struct {
element T
next *List
}
return List (type)
}
type ListInt List(int)
var v1 List(int)
var v2 List(float)
const func MyMap(T1, T2 gotype) gotype { return map[T1]T2 (type) }
const func MyChan(T3 gotype) gotype { return chan T3 (type) }
var v3 MyMap(int, string)
```
```go
package hashmap
const func bucket(K, V gotype) gotype {
type bucket struct {
next *bucket
key K
val V
}
return bucket (type)
}
const func Hashfn(K gotype) gotype { return func(K) uint (type) }
const func Eqfn(K gotype) gotype { return func(K, K) bool (type) }
const func Hashmap(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) gotype {
type Hashmap struct {
buckets []bucket(K, V)
entries int
}
func(p *Hashmap) Lookup (key K) (val V, found bool) {
h := hashfn(key) % len(p.buckets)
for b := p.buckets[h]; b != nil; b = b.next {
if eqfn(key, b.key) {
return b.val, true
}
}
}
func (p *Hashmap) Insert(key K, val V) (inserted bool) {
// Implementation omitted.
}
return Hashmap (type)
}
const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn) {
return func () *Hashmap(K, V, hashfn, eqfn) {
return &Hashmap(K, V, hashfn, eqfn) {
buckets: make([]bucket(K, V), 16),
entries: 0,
}
}
}
```
```go
package sample
import (
"fmt"
"hashmap"
"os"
)
func hashint(i int) uint {
return uint(i)
}
func eqint(i, j int) bool {
return i == j
}
var v = hashmap.New(int(type), string(type), hashint, eqint)
func Add(id int, name string) {
if !v.Insert(id, name) {
log.Fatal("duplicate id", id)
}
}
func Find(id int) string {
val, found := v.Lookup(id)
if !found {
log.Fatal("missing id", id)
}
return val
}
```
### Ambiguities
The extension of `TypeName` to include calls returning a `gotype` introduces
some minor ambiguities in the grammar which must be resolved, particularly when
the argument to the `gotype` function is a named constant rather than a `gotype`
or another call.
The following example illustrates an ambiguity in interface declarations.
If `Z` is a type, it declares a method named `Y` with an argument of type `Z`.
If `Z` is a constant, it embeds the interface type `Y(Z)` in `X`.
```go
interface X {
Y(Z)
}
```
The compiler can theoretically resolve the ambiguity itself based on the
definition of `Z`, but `go/parser` package currently does not do enough analysis
to determine the correct abstract syntax tree for this case.
To keep the parser relatively simple, we can resolve the ambiguity by requiring
parentheses for embedded types derived from expressions, which can never be
valid method declarations:
```go
interface X {
(Y(Z))
}
```
ℹ In the above example, `Z` must be a constant or the name of a type, not a
`gotype` literal: the `(type)` in `Z (type)` would already remove the
ambiguity.
ℹ This problem and the proposed resolution are analogous to the existing
ambiguity and workaround for composite literals within `if`, `switch`, and
`for` statements.
(Search for "parsing ambiguity" in the Go 1.7 spec.)
The following example illustrates an ambiguity in expressions.
If `Y` is a compile-time function that returns a `gotype`, the expression is a
[`Conversion`](https://golang.org/ref/spec#Conversion) of the expression `w` to
the type `Y(Z)`.
Otherwise, it is a [call](https://golang.org/ref/spec#Calls) to the function
`Y(Z)` with [`Arguments`](https://golang.org/ref/spec#Arguments) `w`.
```go
var x = Y(Z)(w)
```
Fortunately, the [`go/ast` representation](https://play.golang.org/p/C0W4uMy5ek)
for both of these forms is equivalent: a nested `CallExpr`.
ℹ As above, this case is only ambiguous if `Z` is a constant or the name of a
type.
ℹ This ambiguity already appears in the existing grammar for the simpler case of
`var x = Y(w)`: whether the subexpression `Y(w)` is parsed as a `Conversion`
of `w` to `Y` or a `PrimaryExpr` applying a function `Y` to `w` already
depends upon whether `Y` names a type or a function or variable.
The ambiguities described here are low-risk: the result of misparsing is an
invalid program, not a valid program with unintended behavior.
## Rationale
This proposal provides support for parametricity and metaprogramming using a
language that is already familiar to Go programmers: a subset of Go itself.
The proposed changes to the compiler are extensive (in order to support
compile-time evaluation), but the changes to the language itself and to the
runtime are relatively minimal: extensions of existing declaration syntax to
additional scopes, a token for indicating compile-time functions, a token for
hoisting types into values, and the new built-in type `gotype`.
The programmer needs to think about which parts of the program execute at
compile-time or at run-time, but does not need to learn a whole new language (as
opposed to, say, the extensive surface area of the `reflect` or `go/ast`
packages or the entirely different language of `text/template`).
The potential applications cover a significant fraction of "metaprogramming"
use-cases that are currently well-supported only in languages much more complex
than Go, and that are not addressed by previous proposals that run closer to
conventional "generics" or "templates".
The specific language changes may be somewhat more complex than some
alternatives (particularly when compared to tools that build on top of `go
generate`), but the deployment overhead is substantially lower: instead of
preprocessing source files (and potentially iterating over the outputs many
times to reach a fix-point of code generation), the programmer need only run the
usual `go build` command.
That is: this proposal trades a bit more language complexity for a significant
reduction in tooling complexity for Go users.
With a few additional modest extensions (e.g. compile-time `init` functions),
the same mechanism can be used to make Go program initialization more efficient
and to move detection of more errors from run-time to compile-time.
The principles underlying this proposal are based on existing (if little-used)
designs from programming language research: namely, higher-order types, partial
evaluation[1][][2][], and dependent function types.
[1]: https://www.cs.cmu.edu/~fp/papers/jacm00.pdf
[2]: https://www.cs.cmu.edu/~fp/papers/sope98.pdf
## Caveats
With this style of metaprogramming, it would be difficult (perhaps infeasible)
to add deduction for `gotype` arguments in a subsequent revision of the language
in a way that maintains backward-compatibility.
## Compatibility
This proposal is intended to be compatible with Go 1.
(The author has looked for incompatibilities and not yet found any.)
## Implementation
The implementation of `gotype` is straightforward, especially if we decline to
add conversions to and from `reflect.Type`.
The detection and evaluation of compile-time expressions adds a new algorithm to
the compiler, but it should be a straightforward one (a bottom-up analysis of
expressions) and is closely related to common compiler optimizations
(e.g. constant folding).
The export data format would have to be expanded to include definitions of
compile-time functions (as I assume it does today for inlinable functions).
The bulk of the implementation work is likely to be in support of executing Go
functions at compile time: compile-time functions have similar semantics to
run-time functions, but (because they are executed at compile time and because
they can have dependent types) would need support for a more dynamically-typed
evaluation.