Note
This does not exist, I am sketching out a design idea.
The go keyword accepts a call expression, evaluates all the terms of the expression up to the point of the call itself, then issues the call inside a newly-created goroutine.
By analogy, the call keyword accepts a call expression and generates a function literal that evalutes that expression.
Given:
func f() {}
func g(a, b int) {}
func h(s string) int { return 0 }
func do(v int) (bool, error) { return false, nil }
type thing struct{}
func (thing) act(a int, b string) {}
func (thing) perform() {}
func (thing) get() int { return 0 }| Call | Meaning |
|---|---|
call f() |
func() { f() } |
call g(5, 3) |
func() { g(5, 3) } |
call h("foo") |
func() int { return h("foo") } |
call p.perform() |
func() { p.perform() } |
call p.act(1, "x") |
func() { p.act(1, "x") } |
call p.get() |
func() int { return p.get() } |
Note that whether the resulting function returns or not depends on the result type of the call expression.
With arguments, call behaves like a function literal, call(a type1, b type2) allows currying arguments for the call expression.
| Call | Meaning |
|---|---|
call(a int) g(a, 3) |
func(a int) { g(a, 3) } |
call(b int) g(5, b) |
func(b int) { g(5, b) } |
call(s string) h(s) |
func(s string) int { return h(s) } |
call do(10) |
func() (bool, error) { return do(10) } |
call(z int) do(z) |
func(z int) (bool, error) { return do(z) } |
call(p thing) p.perform() |
func(p thing) { p.perform() } |
call(p thing) p.get() |
func(p thing) int { return p.get() } |
No mechanism is provided for currying result values; at that point you should just write the function literal out. The call notation mixes just fine with ordinary literals, e.g.,
call(z int) func() bool { b, _ := do(z); return b }()
becomes
func(z int) bool { return func() bool { b, _ := do(z); return b }() }Potentially, the compiler could be taught to do reverse beta conversion (for function literals specifically) which would make this:
func(z int) bool { b, _ := do(z); return b }Though, at that point you might as well just write out the literal directly. This does admit the possibility of helper functions, though, e.g., given:
func noError[T any](v T, _ error) T { return v }You could then productively write call(v int) noError(do(v)) to get:
func(v int) bool { return noError(do(v)) }The result of the call form is an expression, so it can appear in places where expressions go:
func do(flag bool, f func(s string) int) { /* ... */ }
do(true, call(s string) h(s))
func f() func() int {
var p thing
return call p.get()
}call(sig1...) <call-expr>
where <call-expr> has result type (sig2...), becomes
func(sig1...) (sig2...) { [return] <call-expr> }
As a special case, call <call-expr> is equivalent to call() <call-expr>.
In the case where sig2... is empty, the return is omitted. In the case where sig1... is omitted, the resulting function has an empty argument list.
Maybe a new statement-like keyword would be tricky. It avoids conflict with existing code, but I haven't explored all the implications on the existing expression syntax. As an alternative, you could imagine using function notation, like for the other built-in generics (make, new, etc.):
call(sig1 ..., <call-expr>)
so, for example:
call(z int, do(z))
This still requires extension of the grammar—this new construct accepts type signature arguments like func does, for example. But it means the resulting expression "looks like" a call expression to everything outside that nonterminal.
One interesting question is whether the compiler could reliably infer the types of named call parameters, e.g., given
func h(s string) int { return len(s) }could call(s) h(s) infer func(s string) int { return h(s) }? In this simple case maybe so, but it's less clear in more complex cases with nested calls where types may be boxed by interfaces. In theory that shouldn't prevent inference, but there are a already cases today where the compiler declines to infer a type when there is no a priori principal type.