Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: lazy values #37739

Open
ianlancetaylor opened this issue Mar 7, 2020 · 58 comments
Open

proposal: Go 2: lazy values #37739

ianlancetaylor opened this issue Mar 7, 2020 · 58 comments

Comments

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Mar 7, 2020

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

    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.

    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

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

package log
func Verbose(format string, a ...lazy interface{}) {
    if verboseFlag {
        log.Info(format, a...)
    }
}

Calls to the function will look like

    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

    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 (using the syntax of the current design draft):

func Cond(type T)(cond bool, v1 lazy T, v2 lazy T) T {
    if cond {
        return v1.Eval()
    }
    return v2.Eval()
}

This will be called as

    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. Using the current generics design draft, a promise would look like this:

func Promise(v lazy T) chan T {
    c := make(chan T, 1)
    go func() { c <- v.Eval() }()
    return c
}

A promise would be created like this:

    F(promise.Promise(lazy F())

and used like this:

func F(c chan T) {
    // do stuff
    v := <-c
    // do more stuff
}
@randall77
Copy link
Contributor

@randall77 randall77 commented Mar 8, 2020

In the implementation of Verbose, don't you need to call Eval on the elements of a before passing them to log.Info? Or are you assuming log.Info will do that? (Does log.Info bottom out at the fmt package at some point?)

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 8, 2020

Is it safe to Eval from multiple threads simultaneously?

If Eval panics on the first call do future calls panic with the same value?

Since the result of t := lazy v is essentially a function, instead of an Eval method could evaluation be written t()?

Would the Verbose example provided need the lazy declaration? The thunks have to be evaluated somewhere, presumably within fmt, but until then the lazy T would fit in an interface same as any other value.

@go101
Copy link

@go101 go101 commented Mar 8, 2020

related: #36497

I believe what @jimmyfrasche means is like:

func foo(z bool) {
	c := make(chan struct{})
	a, b = 1, 2
	
	go func() {
		<-c
		a, b = 8, 9
	}()
	
	f := func(b bool, x lazy int) {
		close(c)
		if b {
			println(x.Eval())
		}
	}
	
	f(z, lazy a + b)
}

It looks it should be the duty of programmers to avoid data races.

BTW, is it good to reuse the defer keyword as lazy?

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 8, 2020

@randall77

I'm assuming that log.Info bottoms out in the fmt package, yes. That is true of the log package in the standard library (although there it is log.Print rather than log.Info).

@jimmyfrasche

Yes, it should be safe to call Eval concurrently from multiple goroutines.

I hadn't thought about calling Eval a second time if the first time panics; I'm not sure what the right approach is. It could panic again, or it could return the zero value.

Yes, for many uses we could use t() rather than t.Eval(). The main advantage of t.Eval() is that the fmt package can apply it automatically. But I suppose that if lazy types show up in reflection, then the fmt package can handle them automatically anyhow. So maybe t() is better. It does have the advantage of not adding a method to the language spec.

You are probably right that the Verbose example would work without using lazy in the function declaration.

@go101

Using defer rather than introducing a new keyword is a good idea. Thanks.

@alanfo
Copy link

@alanfo alanfo commented Mar 8, 2020

There are a number of areas where 'lazy' techniques are beneficial and anything which makes such techniques easier for the Go programmer to use (as this proposal would) is therefore welcome.

As far as passing lazy arguments at the call site is concerned, you make two practical points both of which I agree with:

  1. We're only normally interested in deferred evaluation of function calls as other expressions are usually cheap and quick to evaluate anyway.

  2. Readability is improved enormously if there's some visual indication that such a call will be evaluated lazily.

Despite (2), lazy could be omitted because of the implicit conversion from T to lazy T and one could envisage 'lazy' programmers (pun intended) doing this, even from function calls, though tools could try to enforce best practice in this area.

Rather than rely on tools, I'd like to make a suggestion:

  1. The use of lazy would be obligatory where expressions consisting of or involving function calls were passed as arguments to functions with lazy parameter types. Only the function calls themselves would need to be decorated with lazy, not the rest of the expression (if any). So, for example, you'd pass 1 + lazy f() rather than lazy (1 + f()). This would also seem more natural if defer were used in place of lazy to avoid creating a new keyword.

  2. In all other cases, lazy would not be allowed at all at the call site which I think would reflect what people would prefer to do in practice anyway.

@beoran
Copy link

@beoran beoran commented Mar 8, 2020

Once we have generics, I think it would probably be easy to have a generic Lazy function that does lazy evaluation. So, while this seems interesting, I would rather have generics first and then see if we can solve the problem with them instead.

@fzipp
Copy link

@fzipp fzipp commented Mar 8, 2020

Shouldn't eval be a built-in function rather than a method? Go's built-in types don't have methods so far. It's "len(slice)", not "slice.Len()", so I'd expect "eval(v)".

@beoran Generics don't simplify lazy evaluation. You still have to write a function literal somewhere.

@fzipp
Copy link

@fzipp fzipp commented Mar 8, 2020

Why not let "lazy E" simply be of type "func() T"?

func Cond(type T)(cond bool, v1 func() T, v2 func() T) T {
    if cond {
        return v1()
    }
    return v2()
}

v := cond(useTls, lazy FetchCertificate(), nil)

Less characters on the use-site (+2 for for each "()" in the parameter list and -5 for each ".Eval" in the body), and it does not require a new type. "lazy E" would just be a shorthand for "func() T { return E }".

And then why not introduce short lambda syntax:

v := cond(useTls, {FetchCertificate()}, nil)
v := cond(useTls, func FetchCertificate(), nil)

or similar. (In this specific example we can write

v := cond(useTls, FetchCertificate, nil)

anyway)

@urandom
Copy link

@urandom urandom commented Mar 8, 2020

@fzipp, while I would personally welcome a shorthand syntax for function literals, and thing it would greatly improve the language itself, it itself will not help create easy-to-use lazy values.

A lazy value would, once invoked, compute the actual value once, store it somewhere in memory, and then provide it. And subsequent invocations will only result in fetching that same computed value.

Yours is but a function call. There is nothing in your syntax that would tell me or the compiler that whatever the function produces will be returned on subsequent calls without computing it again.

@ianlancetaylor, your Promise snippet doesn't really illustrate a promise that well. Once someone reads the promise channel, that promise can no longer be used, which isn't exaclty how promises work. Rather, Go would have to support something like go func() { close(c, v.Eval()) }() so that any reader of that channel will get the value.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 8, 2020

I hadn't thought about calling Eval a second time if the first time panics; I'm not sure what the right approach is. It could panic again, or it could return the zero value.

I would argue that one is interested in the result of evaluating the function which is either the return or the panic. I imagine it could be hard to debug if you sent a lazy value to a dozen places and it panic'd in one of them but not the others, especially if concurrency is involved. It would also mean that you'd get the panic if you eval'd in a debugger even if it had been eval'd before and the panic had been recovered and discarded making it hard to see what's gone wrong.

Also, it should be possible to convert a defer T to a func() T for interoperability with older code. This was possible with a method since you could do f((defer g(x, y)).Eval). If this were another implicit conversion you could use f(defer g(x, y)) even when f expects a func() T. I'm not a fan of implicit conversions in general but all the ones here seem appropriate since it's all sugar.

If I write the expression defer f(g()) presumably g is evaluated immediately and the thunk is f(value returned by g) so if you want both to be deferred, you need to write defer func() T { return f(g()) }(). If a func() T could also be implicitly converted to a defer T you could just write func() T { return f(g()) } but then you lose the guarantee that the same result is always returned and that f(g()) is evaluated at most once and is safe to evaluate in parallel, so that seems like a bad idea.

@fzipp
Copy link

@fzipp fzipp commented Mar 8, 2020

@urandom Right, Ian proposed cached lazy values. The question is if the caching is really needed, certainly not by the given examples. With generics (as @beoran suggested) a cached lazy value implementation could look like this:

package lazy

func Of(type T)(f func() T) Val(T) {
	return &val{f: f}
}

type Val(type T) interface {
	Eval() T
}

type val(type T) struct {
	f      func() T
	value  T
}

func (v *val(T)) Eval() T {
	if v.f != nil {
		v.value = v.f()
		v.f = nil
	}
	return v.value
}

A function with lazy parameters:

func Cond(type T)(cond bool, v1, v2 lazy.Val(T)) T {
	if cond {
		return v1.Eval()
	}
	return v2.Eval()
}

Usage:

v := Cond(useTls, lazy.Of(func() *Certificate { return FetchCertificate(x) }), nil)

The issue is still mainly the verbosity of the function literal:

But it's unsatisfactory because it's verbose. At the call site it's painful to have to write a function literal each time.

With some kind of lambda expression syntax:

v := Cond(useTls, lazy.Of({FetchCertificate(x)}), nil)
v := Cond(useTls, lazy.Of(func FetchCertificate(x)), nil)
@beoran
Copy link

@beoran beoran commented Mar 8, 2020

Thanks for better stating what I meant. It looks like with generics, this issue is reduced to the fact that there is no shorthand lambda syntax in Go. In the past requests for such syntax have been denied, but perhaps it is time to reconsider?

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 8, 2020

lambda syntax wouldn't help with the main example that requires fmt to know that some functions it sees should be evaluated but others should not be.

@fzipp
Copy link

@fzipp fzipp commented Mar 8, 2020

@jimmyfrasche The format verb syntax could be extended for functions, e.g.:

log.Verbose("graph depth %{d}", {GraphDepth(g)}) // Lazy

vs.

log.Verbose("graph depth %d", GraphDepth(g)) // Non-lazy

Or with the the "lazy" package code I posted above:

package lazy

// ...

type Val(type T) interface {
	Eval() T
	fmt.Stringer
}

// ...

func (v *val(T)) String() string {
	return fmt.Sprint(v.Eval())
}

// ...
	log.Verbose("graph depth %s", lazy.Of({GraphDepth(g)})
@mdcfrancis
Copy link

@mdcfrancis mdcfrancis commented Mar 9, 2020

How about a general apply/curry syntax which allows partial application of arguments so :

func f( a int, b int ) int  {
    return a + b 
}
g := apply f( 1 )
// type of g func( b T2 ) int 
r := g( 2 ) // 3 
// and 
k := apply g( 3 ) 
s := k() // 4 

On the fmt handling - any reason not to have lazy / apply implement an interface on the function in the same way that HanderFunc is implemented? For type safety this probably either needs code gen or generics. That way fmt could do the right thing.

Final thought - given that taking a pointer or a function is not allowed, we could steal the & to be the apply operator, this starts to look a little like magic but doesn't mean introducing a new keyword.

g := &f(1) 
g(2) // 3 
@fzipp
Copy link

@fzipp fzipp commented Mar 9, 2020

@mdcfrancis Can you show how currying solves any of the examples in the proposal? I don't see the connection here.

@mdcfrancis
Copy link

@mdcfrancis mdcfrancis commented Mar 9, 2020

@fzipp - curry/apply with no unbound args provides the same output as the proposed lazy operator but is more general in form. So lazy f( x ) is identical to apply f(x) for a single argument function.

@fzipp
Copy link

@fzipp fzipp commented Mar 9, 2020

@mdcfrancis I see, thanks.

@mdcfrancis
Copy link

@mdcfrancis mdcfrancis commented Mar 9, 2020

The type based hint for fmt would look something like the below where curry/lazy returns a type alias - note you'd need code gen to support (or generics) but fmt could ask isApplied( func() T ) / isLazy( func() T ) for vars of type func() T.

package main

import "fmt"
type Applied interface {
	Applied()
}

func f() string {
	return "hello"
}

type AppliedFunc func() string
func ( f AppliedFunc )Applied() {}

func isApplied( a interface{} ) bool {
	_, ok := a.( Applied )
	return ok
}

func main() {
	a := AppliedFunc( f )
	fmt.Print( isApplied( a ) )
	fmt.Print( a() )
}
@fzipp
Copy link

@fzipp fzipp commented Mar 10, 2020

How about a general apply/curry syntax which allows partial application of arguments

Although this generalisation is nice from a functional programming perspective, I'm not sure how useful partial application (with some unbound args) is in practice. An easy to grasp operator name like "lazy" that communicates the purpose could be worth more than generality.

@mdcfrancis
Copy link

@mdcfrancis mdcfrancis commented Mar 10, 2020

The type based hint for fmt would look something like the below where curry/lazy returns a type alias - note you'd need code gen to support (or generics) but fmt could ask isApplied( func() T ) / isLazy( func() T ) for vars of type func() T.

You can do almost all of this today in the existing type system except for where you would want generics.

package main
import "fmt"
type Applied interface {
	Applied()
}
func f(arg string) string {
	return arg
}
func Apply( f func() string ) AppliedFunc {
	return AppliedFunc( f )
}
func Apply1( f func( string ) string, arg string ) AppliedFunc {
	return AppliedFunc( func() string { return f( arg )})
}

type AppliedFunc func() string
func ( f AppliedFunc )Applied() {}

func cond( expr bool, f AppliedFunc, g AppliedFunc ) string {
	if expr {
		if f == nil {
			return ""
		}
		return f()
	}
	if g == nil {
		return ""
	}
	return g()
}

func main() {
	s := func() string { return "hello" }

	fmt.Print( cond( true, s, nil ) )
	fmt.Print( cond( false, s, nil ) )

	g := Apply1( f, "world")

	fmt.Print( cond( true, g, nil ) )
	fmt.Print( cond( false, g, nil ) )
}

@millergarym
Copy link

@millergarym millergarym commented Mar 11, 2020

Is there ever a need to explicitly use lazy when calling a function?
Should t := lazy v and f(z, lazy a + b) be allowed at all?

Idris has declarative laziness without the explicit need to call Delay (aka lazy on calling) or Force (aka Eval). The rules on panic, print & printf might complicate things, but is this type checker enforced laziness a desirable goal?
http://docs.idris-lang.org/en/latest/tutorial/typesfuns.html#laziness

@urandom
Copy link

@urandom urandom commented Mar 11, 2020

In kotlin, a value must be marked as lazy to be lazily initialized:

https://kotlinlang.org/docs/reference/delegated-properties.html

however, further arguments and usages are not marked in any way. the compiler knows that this is an object wrapped in a Lazy class and will expand to a method call of it when used.

to me, that seems like a decent solution. functions like printf/cond don't really need to know that some of its arguments are lazy.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 13, 2020

@urandom I agree that that feature is desirable, I just have no idea how to implement it. Kotlin can do it because every value is an object. That is not true in Go. In Go a func(bool) takes a value of type bool. I don't see any way to pass a lazy value to a function that expects a bool.

@millergarym
Copy link

@millergarym millergarym commented Mar 13, 2020

Question:
what would the following do?

func f(x lazy interface{]) {}

func main() {
  for {
      f( lazy <- time.After(time.Second))
     ...
     break
   }
}

I was having some issues thinking through this so created an example.
Is this the intent?

https://play.golang.org/p/gerLIiL_Dc0

package main

/*
Playing with laziness.

Currently
time go run main.go
1
1
3
real    0m3.219s
user    0m0.342s
sys     0m0.087s

With laziness
time go run main.go
1
1
1
real    0m0.429s
user    0m0.342s
sys     0m0.087s

*/

import (
	"fmt"
	"time"
)

var doDebug = true

func debug(x /*lazy*/ interface{}) {
	if doDebug {
		fmt.Printf("%v\n", x)
	}
}

func main() {
	debug(1)
	debug( /*lazy*/ expensiveMutating())
	doDebug = false
	debug(expensiveMutating())
	doDebug = true
	debug(expensiveMutating())
}

var counter int

func expensiveMutating() int {
	counter++
	<-time.After(time.Second)
	return counter
}
@urandom
Copy link

@urandom urandom commented Mar 13, 2020

@urandom I agree that that feature is desirable, I just have no idea how to implement it. Kotlin can do it because every value is an object. That is not true in Go. In Go a func(bool) takes a value of type bool. I don't see any way to pass a lazy value to a function that expects a bool.

yeah, i just wanted to illustrate another implementation, in case when can learn something from it.
i guess decorating the func argument with a lazy would pretty much box the passed value, but then perhaps we might not need to call an Eval method on it.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 17, 2020

There is no implicit laziness.

	debug( lazy expensiveMutating())
	debug(expensiveMutating()) // <-- is this call lazy?

No, that call is not lazy.

For this code:

package main

var doDebug = true

func debug(x interface{}) {
	if doDebug {
		fmt.Printf("%v\n", x)
	}
}

func main() {
	debug(1)                        // prints 1
	debug(lazy expensiveMutating()) // counter = 1, sleeps, prints 1
	doDebug = false
	debug(expensiveMutating())      // counter = 2,sleeps, doesn't print anything
	doDebug = true
	debug(expensiveMutating())      // counter = 3, sleeps, prints 3
}

var counter int

func expensiveMutating() int {
	counter++
	<-time.After(time.Second)
	return counter
}

So I think I did misread the code earlier, and it does print 1 1 3.

if fmt.Printf args was defined as lazy and using fmt.Printf("no print verbs in format string", lazy expensiveMutating()), [sic bad format string] would the function get Eval'ed?

Well, it depends on the exact implementation of fmt.Printf, but the current implementation does evaluate extra arguments, so, yes, it would be eval'ed.

does this make sense f( lazy <- time.After(time.Second))?

I don't know why you would do that, but, sure. When the value is evaluated, it will sleep for a second and return the time when the sleep wakes up.

I think the expressions with a function would do the same.

@matttproud
Copy link
Contributor

@matttproud matttproud commented Mar 24, 2020

I would be particularly interested in seeing how a proposal addresses how lazy is supposed to interact with functions that call runtime.Callers and similar. I realize this is a minor detail, but sometimes enterprise software includes the trappings of such calls for tracing, instrumentation, logging, etc., which often rely on metadata provided by the program counters.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 24, 2020

A lazy expression that calls runtime.Callers would show the call stack at the point where the expression was first evaluated. It would not show the call stack where the lazy expression was created. I don't see how it could work any other way.

@ydnar
Copy link

@ydnar ydnar commented Mar 25, 2020

@ianlancetaylor why not a unary ? operator that makes any expression lazy?

log.Printf(“error: %v", ExpensiveFunc()?)
@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

I think that would be unnecessarily cryptic and easy to miss. Also, it's nice that we use the syntax for both the expression and the function parameter, but writing func F(a? int) would be even more cryptic.

@ydnar
Copy link

@ydnar ydnar commented Mar 25, 2020

I think that would be unnecessarily cryptic and easy to miss. Also, it's nice that we use the syntax for both the expression and the function parameter, but writing func F(a? int) would be even more cryptic.

Perhaps func F(a int?) (effectively a lazy int)?

What if the caller didn’t need to express the ? at all, similar to how Go’s method syntax doesn’t vary for pointer receivers like C—it’s always .?

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

I don't see how func F(a int?) is much better. It's still hard to understand and easy to miss.

It seems to me that making ? the default would be very inefficient in the normal case. It would mean always passing a function rather than a simple value.

What is wrong with lazy?

@ydnar
Copy link

@ydnar ydnar commented Mar 25, 2020

You’re right. lazy is better.

@nightlyone
Copy link
Contributor

@nightlyone nightlyone commented Mar 25, 2020

If the compiler can prove that lazy and immediate evaluation have no different observable side effect, is it allowed to inline and immediately evaluate it?

@natefinch
Copy link
Contributor

@natefinch natefinch commented Mar 25, 2020

I don't like that we'd be adding a keyword and expanding the language just to avoid passing a function literal around.... and even then its main use case is to avoid verbosity and extra work when logging.

That just seems like an incredibly minor benefit for the cost of a language addition. In my book, it doesn't pass the test for a worthwhile change.

@qinabu
Copy link

@qinabu qinabu commented Mar 25, 2020

How about

log.VerboseFn("graph depth %s", func() string {return GraphDepth(g)})
@networkimprov
Copy link

@networkimprov networkimprov commented Mar 25, 2020

@natefinch, the func boilerplate erodes readability

doEither(which, func()int { return cheap }, func()int { return expensive() })
doEither(which, lazy cheap, lazy expensive())

But then again, so does error handling boilerplate :-)

@mdcfrancis
Copy link

@mdcfrancis mdcfrancis commented Mar 25, 2020

Any reason not to have a short form function syntax ?

do(true, ()->cheap, ()->expensive())

If you want more than a single expression in your function you have to write return. Being able to write something like the following

The following would be equivalent

f := func(x string)string->{ return fmt.sprintf( “hello ”, x ) }

f := (x string)->{ return fmt.sprintf( “hello ”, x ) }

f := (x string)->fmt.sprintf( “hello ”, x )

@natefinch
Copy link
Contributor

@natefinch natefinch commented Mar 25, 2020

I don't think we need cheap vs. expensive. lazy is expensive vs. nothing.

One of my maxims about requests for language additions is that so many of them are adding complexity just to avoid an if statement. This is one of them.

if logging.IsVerbose {
    log.Verbose(expensiveThing())
}

Is that a little verbose? Yes. But most of the time you don't even need to do that. You only need the extra if statement when you're in a hot path AND you're doing something really expensive. Otherwise, you can just log.Verbose(val, val2, val3) which is like 95% of logging. The actual cycles to pass some stuff by value and then not use it is tiny compared to that database lookup you're gonna do at the end of this call.

And when you do need that, it's incredibly boring and simple code and no one will ever misunderstand what it's doing.

I just don't see a giant benefit to this language addition. It's going to get abused and make our code more complicated for very little practical benefit.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

@nightlyone

If the compiler can prove that lazy and immediate evaluation have no different observable side effect, is it allowed to inline and immediately evaluate it?

Sure.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

Note that passing an explicit function, whether with the existing syntax or a newer one, would not permit the special handling in which the fmt functions evaluate a lazy value. The fmt functions could not change their behavior when seeing a function literal, as that would break existing code. But of course that part of the proposal could be dropped.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

A simpler syntax for function literals is discussed in #21498.

@andreimatei
Copy link
Contributor

@andreimatei andreimatei commented Mar 25, 2020

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.

I'd like to challenge this. I apologize if you'll consider this unrelated, but I'm here to argue that it's actually the allocations that are often the bigger problem (not the allocations caused by the expression evaluation itself, but related ones).
Logging seems to be the big driver of this proposal. CockroachDB has fairly advanced logging practices and libraries. Our major problem with logging is that a log.Infof(args ...interface{}) function causes all its arguments to escape to the heap. I believe this is a well understood problem: log.Infof() bottoms out at a fmt.Sprintf() call, which calls String() through the Stringer interface. Since that String() method could in theory hold on to the receiver (although in practice it never does), the receiver escapes to the heap. #8618
We jump through great hoops to avoid such allocations. Sometimes we do things like the following to at least only cause allocations when verbose logging is needed:

if ExpensiveLoggingEnabled(ctx) {
  cpy := request.Clone()
  log.Infof("my request: %s", cpy)
}

Other times, we call String() ourselves to force early stringifying in order to have the string escape to the heap, not the original variable.

log.Infof("my request: %s", myFatObject.String())

This goes directly against the purpose of the current proposal, and yet we find ourselves forced to do it.
If you want to improve logging support in Go, please keep the escape analysis angle in mind.

@beoran
Copy link

@beoran beoran commented Mar 25, 2020

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 25, 2020

@andreimatei Thanks for the examples. Honestly, logging is not really the driving issue here. It's just a convenient example. This proposal clearly does not help with the escaping to the heap that you mention.

The driving example here is trying to generalize the C trinary ?: operator in a way that will work for Go.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Mar 26, 2020

@natefinch

I don't like that we'd be adding a keyword and expanding the language just to avoid passing a function literal around

It's more than that because if you call a function literal twice, the function will be called twice, but with the lazy proposal, calling Eval twice will only evaluate the expression once. That's a significant and important difference.

Using function literals wouldn't allow the convenient assignment compatibility rules that this allows. For example, in this proposal a value of type lazy []string would be assignable to any value with underlying type []string. With function literals, you'd have to make a wrapper function to do that.

In general, I like the thrust of this proposal. It does need to be fleshed out some more though. For example, if lazy T is one of the fundamental types, then it would need to be supported by the reflect package.

I second the reservations about the use of the Eval method name - this would be the first time that the base language has used a name like this - but I can't think of a nice operator for it, and there are other advantages to using a method rather than an operator (see reply to @jimmyfrasche below).

One thought: I think you might want to restrict the use of lazy in some cases. For example, would this be disallowed? If not, what would it do?

func f() (r lazy interface{}) {
	defer func() {
		r = lazy recover()
	}()
	panic("hello")
}

func main() {
	r := f()
	fmt.Println(r)
	fmt.Println(r)
}

@jimmyfrasche

it should be possible to convert a defer T to a func() T for interoperability with older code

I presume you mean lazy T there. AIUI that would be easy: just use a method expression.

func old(f func() string) {
	fmt.Println(f())
}

func main() {
	v := lazy "hello"
	old(v.Eval)
}
@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Mar 26, 2020

The lazy recover() example is nice. I think you're right that we would have to forbid that. I think that's OK; we already forbid other built-in functions in various contexts (e.g., https://golang.org/ref/spec#Expression_statements)

@ydnar
Copy link

@ydnar ydnar commented Mar 26, 2020

If lazy T is shorthand for func() T, then what about calling it directly instead of an Eval method?

func log(v lazy string) {
	fmt.Println(v())
}

func fetch() string {
	// Expensive
	return "hello"
}

func main() {
	v := lazy fetch()
	log(v)
}

Second, could a lazy value be evaluated implicitly when assigned to a non-lazy var? e.g.

func f(v lazy string) {
	var w string
	w = v // Implicitly calls v()
}

Third, could a function that matches func() T be passed through unchanged as an acceptable lazy value? e.g these two would be equivalent:

f1 := fetch
f2 := lazy fetch()
@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 26, 2020

@rogpeppe if a var v lazy T is forced with v() instead of v.Eval() then there is no method to use in a method expression and hence the question of convertibility to a func() T arises.

@natefinch
Copy link
Contributor

@natefinch natefinch commented Mar 26, 2020

@rogpeppe caching a copy of a response is not that hard, either.

type lazyString struct {
   expensive func() string
   exists bool
   val string
}
func (l *lazyString) Eval() string{
   if !l.exists {
       l.val = l.expensive()
       l.exists = true
   }
   return l.val
}

Add a mutex if you need it to be threadsafe.

Yes, that's a lot of code, but... it's also a fairly complex concept, and I still maintain that this is not needed very often. I've written a lot of Go, and haven't ever needed this.

@jimmyfrasche I think you're responding to @ydnar

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Mar 26, 2020

@natefinch sorry, I meant to respond to @rogpeppe and not paying enough attention.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Mar 31, 2020

Yes, that's a lot of code, but... it's also a fairly complex concept

That could also be said of channels.

I've written a lot of Go, and haven't ever needed this.

I've done so quite a lot - every time I've used sync.Once to initialize an associated value.

@seebs
Copy link
Contributor

@seebs seebs commented Apr 21, 2020

This is really interesting to me. I have been overall very happy to be free of the C preprocessor, but I have some macros in C code that look like:

        if (pseudo_util_evlog_flags & (x)) { pseudo_evlog_internal(__VA_ARGS__); } \
} while (0)

which provide this behavior, and you simply can't do this in Go, and it's occasionally a source of pain when there's logging which is (1) expensive to compute the values for, (2) only sometimes useful. (You can compile-time it with tiny mid-stack-inlinable functions which have an if constExpr in them and are defined in files controlled by build tags, but that's not as nice.)

I don't consider the "this is a lot of work to avoid a single if" argument persuasive. Consider the next example in the same header:

#ifndef NDEBUG
#define pseudo_debug(x, ...) do { \
        if ((x) & PDBGF_VERBOSE) { \
                if ((pseudo_util_debug_flags & PDBGF_VERBOSE) && (pseudo_util_debug_flags & ((x) & ~PDBGF_VERBOSE))) { pseudo_diag(__VA_ARGS__); } \
        } else { \ 
                if ((pseudo_util_debug_flags & (x)) || ((x) == 0)) { pseudo_diag(__VA_ARGS__); } \
        } \
} while (0)  
[...]

All of this complexity could certainly be hidden inside a small helper function, which would be inlinable, but that would mean that the expensive computation has to happen every time. If you actually have to put this at every call site, you're starting to see a pretty significant cost.

And what happens if the condition you wanted to use changes?

Being able to put the condition inside the function, and lazy-eval the arguments, is really useful, and while this specific example is logging, I have definitely had other cases where being able to defer the actual computation would have been desireable.

I do like the suggestion of calling it defer rather than lazy to save valuable keywords, and because this is conceptually a very similar thing -- putting a hunk of code execution somewhere that it'll be done later. On the other hand, the function argument should be deferred then, or it feels grammatically wrong.

I do sort of like the idiom of just making lazy T behave like func () T. If you ignore the caching, you could then turn the proposal into:

When used as an expression rather than a statement, defer expr is equivalent to

func ()T { return expr }

where T is the type of expr.

I keep wanting to suggest an auto-eval, but the more I think about it, the more i think that feels like a bad fit for Go's usual idiom of making expensive things at all visible. Making the call explicit makes is easier to see that an actual operation is happening.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented May 5, 2020

As @mdcfrancis mentions above in #37739 (comment), if we had a simpler function literal syntax, as discussed in #21498, then we could write

func Cond(type T)(cond bool, v1 func() T, v2 func() T) T {
    if cond {
        return v1()
    }
    return v2()
}

and

    v := cond(useTls, FetchCertificate, ()->nil)

For the logging case, we could write

    log.Verbose("graph depth %d", fmt.Lazy(()->GraphDepth(g)))

where fmt.Lazy is a type that the formatting functions recognize and call when needed.

So upon reflection I'm not sure this idea adds much that isn't already in #21498.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.