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: spec: lazy values #37739

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

proposal: spec: lazy values #37739

ianlancetaylor opened this issue Mar 7, 2020 · 77 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@ianlancetaylor
Copy link
Contributor

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 (edited original proposal to use current syntax):

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

    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):

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:

    F(promise.Promise(lazy F())

and used like this:

func F(c chan T) {
    // do stuff
    v := <-c
    // do more stuff
}
@ianlancetaylor ianlancetaylor added LanguageChange Suggested changes to the Go language v2 An incompatible library change Proposal labels Mar 7, 2020
@ianlancetaylor ianlancetaylor added this to the Proposal milestone Mar 7, 2020
@randall77
Copy link
Contributor

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

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.

@zigo101
Copy link

zigo101 commented Mar 8, 2020

related: #36497

I believe what @jimmyfrasche means is: lazy expression with lifetime bound is prone to cause careless subtle concurrency bugs. An example:

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

@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 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 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
Contributor

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
Contributor

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 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

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
Contributor

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 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

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
Contributor

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

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
Contributor

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

@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
Contributor

fzipp commented Mar 9, 2020

@mdcfrancis I see, thanks.

@mdcfrancis
Copy link

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
Contributor

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

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

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 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

@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

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 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.

@stokito
Copy link

stokito commented Jan 31, 2021

Yep, that's why I wrote may. This is not required. The change will be breaking but to avoid unexpected performance pitfalls this is something that may be useful. I'll update my comment,

@zigo101
Copy link

zigo101 commented Feb 3, 2021

Recently, I let my log functions return a bool, so that I can use bool AND to avoid a if-block:

_= log.VerboseLogging() && log.Verbose("graph depth %d", GraphDepth(g))

It is simpler, though still not perfect.

@mdcfrancis
Copy link

mdcfrancis commented Feb 3, 2021

Following the principle of least surprise and other recent languages like Rust and Julia ( and a long history prior) why not add an explicit macro syntax

log.Logger!( "graph depth %d", GraphDepth(g) )

At a glance you know this is not behaving the way other functions are in go. You can build lazy and a plethora of other language features without requiring changes to the core language. Provided the macro syntax is not such a bad thing as long as it is a hygienic macro. Finally, with package scoping of the macros, you only have to deal with the macros that you import.

@deanveloper
Copy link

Hygienic macros would be nice, although they are extremely hard to get right and often times they essentially involve having a second "macro language" which would not be desirable.

@mdcfrancis
Copy link

Hygienic macros would be nice, although they are extremely hard to get right and often times they essentially involve having a second "macro language" which would not be desirable.

It is - I like the Julia Lang approach which is that the macro is regular julia code that returns the AST of the expansion. Fo go I'd assume you would compile the 'macro' snippet and pass the thunks (or you could use an expression evaluator ) the ast is then checked for escapes etc. It would be fairly easy to prototype using go generate.

@beoran
Copy link

beoran commented Feb 3, 2021

I already proposed Hygienic macros in #32620 and I closed it, partially because I was unable to write a macro preprocessor. If there is enough support perhaps it could be reopened though, although I think generics are likely to be a higher priority.

@tv42
Copy link

tv42 commented Feb 3, 2021

@mdcfrancis go build is not allowed to execute arbitrary code.

@mdcfrancis
Copy link

@mdcfrancis go build is not allowed to execute arbitrary code.

I understand the intent.

Rust seems to have a nice solution by taking a template approach to the problem https://doc.rust-lang.org/reference/macros-by-example.html this seems pretty clean and powerful approach.

@beoran
Copy link

beoran commented Feb 5, 2021

I just checked and it turns out someone wrote a C like macro preprocessor for use with Go in Rust: https://github.com/Boyux/go_macro. Hook that up with go generate, and you're ready to rock. ;)

@extemporalgenome
Copy link
Contributor

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.

Even with #21498, there may still be some value in special casing thunks. I do agree that functions are preferable to interfaces here.

Mini counter-proposal follows...

Much like the type/expression duality of * with respect to pointers (and dereferencing), we could introduce @ as a "thunk of" sigil/operator:

log.Verbose("graph depth %d", @GraphDepth(g))

When used in an expression, @ must precede a call (much like go or defer), and @F(x) is equivalent to func() T { return F(x) }.

When used as part of a type descriptor, @T likewise means func() T.

Unwrapping a thunk does not have special syntax, since x := @F(); x() is convenient enough.

Regarding use with fmt: to avoid breaking backwards compatibility, a new format modifier (%@v) could be introduced to indicate that thunks should be unwrapped (perhaps transparently handling non-function values as though the @ modifier were not specified).

@snadrus
Copy link

snadrus commented Mar 1, 2021

A definition of a pure function for Go would supply much of what's needed for this (after Generics). Proven-Pure functions then could be deferred automatically by the linker with no code change (and cached against argument changes).

Things to consider for pure functions:

  • no writing.
  • No reading from streams either.
  • Question: What about "escaped" arguments & threading? This could require a full Rust-level evaluation regarding the in-context const-ness of an input.

@warrenstephens
Copy link

How about a wild impossible idea? (it's Friday)

What about having the ability to inject log.Whatever statements anywhere in code after compiling??? at runtime???

Don't we all want the ability to arbitrarily insert, and later remove, log or print statements anywhere in our code after the code is built and running somewhere? If I could only just see what that XYZ variable is doing... right... now. Ha ha!

Does any language in history have the ability to inject or overlay prints in running code? -- like some types of probes do with hardware?

I imagine that if this were actually attempted that an enormous number of NO-OP type instructions would have to first be placed between each compiled line of code -- where each could be toggled with calls to a log. afterwards.

What a hugely productive time-saver this would be. How much time I spend re-building code just to add and remove debug output!!!

@deanveloper
Copy link

If you use tools like delve, this is precisely what they're for! I used to find debuggers a bit daunting, but they're actually quite simple in most IDEs. Set a breakpoint and you can see whatever data you need! (This is all a bit off-topic though)

@warrenstephens
Copy link

@deanveloper Am familiar with delve already, but debuggers and IDEs cannot be used in containerized cloud environments -- only logging is available, where post-deployment logging customization would be very useful.

I am just contemplating the performance hit for typical code -- 5%? 10%? more? Most code would not care about that level of impact.

Anyhow, I think it would be a truly transformative change in the relationship between the language and its logging functions -- and could be useful for post-deployment performance profiling as well!

@ianlancetaylor
Copy link
Contributor Author

@warrenstephens It's an interesting idea but it doesn't really have anything to do with this proposal. I suggest that you discuss it elsewhere to avoid cluttering up the discussion here. Thanks.

@ghostsquad
Copy link

should lazy be sugar for something like func() (T, error) as opposed to func() T? I feel like if you can't handle an error condition, then it would make folks want to write more Must...() type of functions, which panic instead of returning an error.

Though, I suppose if you could create your own lazy closure, then you could check if the error was set to something.

func FetchCertificate() (string, error) {
    fmt.Println("fetching certificate...")
    return "fetched certificate", nil
}

func main() {
    var err error
    cert = Cond(false, lazy { v, err := FetchCertificate(); return v }, ()->nil)
    if err != nil {
	panic(err)
    }
}

Full working example, caching, short syntax, etc below. This feels totally doable today without changing syntax, however, it does require the explicit support for a Lazy. It would be very nice if it was possible to make laziness transparent (e.g. allow a func(lazy string) to take a string or lazy string).

https://go.dev/play/p/x2zjn0Xdxno


In fact, instead of marking an argument as lazy we may consider them all as lazy and function calls as pure. This is how it made in functional languages.

This breaks the Go 1 compatibility promise. It would require significant language changes because it's well-known that arguments to functions are always evaluated before the function is called (in fact, in defer statements, they are evaluated when the defer is reached, not when the function exits). Not only this, but many functions in Go are not made to be pure functions.

It seems that for backwards compatibility, functions must opt-in to allow for a lazy variant.

func DoSomething(v lazy string) {}

If the function does not opt in to the lazy semantics then the lazy function is evaluated (lazy.Eval()) prior to calling the DoSomething function, which meets the Go 1 compatibility promise. Otherwise, lazy.Eval is passed into the function instead.

@DeedleFake
Copy link

DeedleFake commented Sep 22, 2023

Now that sync.OnceValue() and friends are out, maybe it's time to take another look at this? Personally, I find the APIs for those to be kind of unwieldy when used inside of a struct, as they necessitate the use of a constructor or some kind of init() method, and in the latter case that method needs its own synchronization, usually using, ironically, a sync.Once. I experimented a little and I kind of like this design for just simple lazy values:

type Lazy[T any] struct {
  val T
  ok bool
}

func (lazy *Lazy[T]) Get(f func() T) T {
  if !lazy.ok {
    lazy.val, lazy.ok = f(), true
  }
  return lazy.val
}

By doing it this way, it leaves the initialization to the call site, rather than requiring it up front. This lets it be used with zero values much more easily:

type Example struct {
  someValue Lazy[string]
}

func (e *Example) SomeValue() string {
  return e.someValue.Get(func() string {
    // Do the calculation here.
  })
}

Because of the API, it may make more sense to call this something like Cache instead of Lazy.

An extra function could also be added for when wrapping a function is all that's necessary:

func Memoize[T any](f func() T) func() T {
  var lazy Lazy[T]
  return func() T { return lazy.Get(f) }
}

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: lazy values proposal: spec: lazy values Aug 6, 2024
@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests