Repository: go
Issue: proposal: spec: lazy values
Labels: LanguageChange,Proposal,LanguageChangeReview

This is just a thought.  I'm interested in what people think.

Background:

Go has two short circuit binary operators, `&&` and `||`, that only evaluate their second operand under certain conditions.  There are periodic requests for additional short circuit expressions, often but not only the `?:` ternary operator found originally in C; for example: #20774, #23248, #31659, #32860, #33171, #36303, #37165.

There are also uses for short circuit operation in cases like conditional logging, in which the operands to a logging function are only evaluated if the function will actually log something.  For example, calls like

```Go
    log.Verbose("graph depth %d", GraphDepth(g))
```

where `log.Verbose` only logs a message if some command line flag is specified, and `GraphDepth` is an expensive operation.

To be clear, all short circuit operations can be expressed using `if ` statements (or occasionally `&&` or `||` operators).  But this expression is inevitably more verbose, and can on occasion overwhelm more important elements of the code.

```Go
    if log.VerboseLogging() {
        log.Verbose("graph depth %d", GraphDepth(g))
    }
```

In this proposal I consider a general mechanism for short circuiting.

Discussion:

Short circuiting means delaying the evaluation of an expression until and unless the value of that expression is needed.  If the value of the expression is never needed, then the expression is never evaluated.

(In this discussion it is important to clearly understand the distinction that Go draws between expressions (https://golang.org/ref/spec#Expressions) and statements (https://golang.org/ref/spec#Statements).  I'm not going to elaborate on that here but be aware that when I write "expression" I definitely do not mean "statement".)

In practice the only case where we are interested in delaying the evaluation of an expression is if the expression is a function call.  All expressions other than function calls complete in small bounded time and have no side effects (other than memory allocation and panicking).  While it may occasionally be nice to skip the evaluation of such an expression, it will rarely make a difference in program behavior and will rarely take a noticeable amount of time.  It's not worth changing the language to short circuit the evaluation of any expression other than a function call.

Similarly, in practice the only case where we are interested in delaying the evaluation of an expression is when passing that expression to a function.  In all other cases the expression is evaluated in the course of executing the statement or larger expression in which it appears (other than, of course, the `&&` and `||` operators).  There is no point to delaying the evaluation of expression when it is going to be evaluated very shortly in any case.  (Here I am intentionally ignoring the possibility of adding additional short circuit operators, like `?:`, to the language; the language does not have those operators today, and we could add them without affecting this proposal.)

So we are only interested in the ability to delay the evaluation of a function call that is being passed as an argument to some other function.

In order for the language to remain comprehensible to the reader, it is essential that any delay in evaluation be clearly marked at the call site.  One could in principle permit extending function declarations so that some or all arguments are evaluated lazily, but that would not be clear to the reader when calling the function.  It would mean that when reading a call like `Lazy(F())` the reader would have to be aware of the declaration of `Lazy` to know whether the call `F()` would be evaluated.  That would be a recipe for confusion.

But at the same time the fact that Go is a compiled type safe language means that the function declaration has to be aware that it will receive an expression that will be evaluated lazily.  If a function takes an `bool` argument, we can't pass in a lazily evaluated function call.  That can't be expressed as a `bool`, and there would be no way for the function to request evaluation at the appropriate time.

So what we are talking about is something akin to C++ `std::future` with `std::launch::deferred` or Rust `futures::future::lazy`.

In Go this kind of thing can be done using a function literal.  The function that wants a lazy expression takes an argument of type `func() T` for some type `T`, and when it needs the value it calls the function literal.  At the call site people write `func() T { return F() }` to delay the evaluation of `F` until the point where it is needed.

So we can already do what we want.  But it's unsatisfactory because it's verbose.  At the call site it's painful to have to write a function literal each time.  It's especially painful if some calls require lazy evaluation and some do not, as the function literal must be written out either way.  In the function that takes the lazy expression, it's annoying to have to explicitly invoke the function, especially if all you want to do is pass the value on to something like `fmt.Sprintf`.

Proposal:

We introduce a new kind of type, a lazy type, represented as `lazy T`.  This type has a single method `Eval()` that returns a value of type `T`.  It is not comparable, except to `nil`.  The only supported operation, other than operations like assignment or unary `&` that apply to all types, is to call the `Eval` method.  This has some similarities to the type

```Go
interface {
    Eval() T
}
```

but it is not the same as regards type conversion.

A value `v` of type `T` may be implicitly converted to the type `lazy T`.  This produces a value whose `Eval()` method returns `v`.  A value `v` of type `T1` may be implicitly converted to the type `lazy T2` if `T1` may be implicitly converted to `T2`.  In this case the `Eval()` method returns `T2(v)`.  Similarly, a value `v` of type `lazy T1` may be implicitly converted to the type `lazy T2` if `T1` may be implicitly converted to `T2`.  In this case the `Eval()` method returns `T2(v.Eval())`.

We introduce a new kind of expression, `lazy E`.  This expression does not evaluate `E`.  Instead, it returns a value of type `lazy T` that, when the `Eval` method is first called, evaluates `E` and returns the value to which it evaluates.  If evaluation of `E` panics, then the panic occurs when the `Eval` method is first called.  Subsequent calls of the `Eval` method return the same value, without evaluating the expression again.

For convenience, if the various fmt functions see a value of type `lazy T`, they will call the `Eval` method and handle the value as though it has type `T`.

Some additions will be needed to the reflect package.  Those are not yet specified.

The builtin functions `panic`, `print`, and `println`, will _not_ call the `Eval` method of a value of type `lazy T`.  If a value of type `lazy T` is passed to `panic`, any relevant `recover` will return a value of that type.

That is the entire proposal.

Examples:

This permits writing

```Go
package log
func Verbose(format string, a ...lazy interface{}) {
    if verboseFlag {
        log.Info(format, a...)
    }
}
```

Calls to the function will look like

```Go
    log.Verbose("graph depth %d", lazy GraphDepth(g))
```

The `GraphDepth` function will only be called if `verboseFlag` is `true`.

Note that it is also fine to write

```Go
    log.Verbose("step %d", step)
```

without having to write `lazy step`, using the implicit conversion to `lazy T`.

If we adopt some form of generics, this will permit writing (edited original proposal to use current syntax):

```Go
func Cond[T any](cond bool, v1 lazy T, v2 lazy T) T {
    if cond {
        return v1.Eval()
    }
    return v2.Eval()
}
```

This will be called as

```Go
    v := cond(useTls, lazy FetchCertificate(), nil)
```

In other words, this is a variant of the `?:` operator, albeit one that requires explicit annotation for values that should be lazily evaluated.

Note:

This idea is related to the idea of a promise as found in languages like Scheme, Java, Python, C++, etc.  A promise would look like this (edited original proposal to use current syntax):

```Go
func Promise[T any](v lazy T) chan T {
    c := make(chan T, 1)
    go func() { c <- v.Eval() }()
    return c
}
```

A promise would be created like this:

```Go
    F(promise.Promise(lazy F())
```

and used like this:

```Go
func F(c chan T) {
    // do stuff
    v := <-c
    // do more stuff
}