Skip to content

Instantly share code, notes, and snippets.

@creachadair
Last active December 8, 2024 18:09
Show Gist options
  • Save creachadair/19b8dfa6ac40ffdc017d89d387e720a7 to your computer and use it in GitHub Desktop.
Save creachadair/19b8dfa6ac40ffdc017d89d387e720a7 to your computer and use it in GitHub Desktop.
A Go syntax for compact function literals

A Go Syntax for Compact Function Literals

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.

Examples

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") }

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 }(z)

becomes

func(z int) bool { b, _ := do(z); return b }

Though, at that point you might as well just write out the literal directly (which is why call doesn't support it). 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(z)) to get:

func(v int) bool { return noError(do(v)) }

The result 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()
}

Translation

call(sig1...) <call-expr>

where <call-expr> has result type (sig2...), becomes

func(sig1...) (sig2...) { [return] <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.

Grammar

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.

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