-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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
Comments
In the implementation of |
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 Would the |
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 |
I'm assuming that Yes, it should be safe to call I hadn't thought about calling Yes, for many uses we could use You are probably right that the Using |
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:
Despite (2), Rather than rely on tools, I'd like to make a suggestion:
|
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. |
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. |
Why not let "lazy E" simply be of type "func() T"?
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:
or similar. (In this specific example we can write
anyway) |
@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 |
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 If I write the expression |
@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:
A function with lazy parameters:
Usage:
The issue is still mainly the verbosity of the function literal:
With some kind of lambda expression syntax:
|
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? |
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. |
@jimmyfrasche The format verb syntax could be extended for functions, e.g.:
vs.
Or with the the "lazy" package code I posted above:
|
How about a general apply/curry syntax which allows partial application of arguments so :
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.
|
@mdcfrancis Can you show how currying solves any of the examples in the proposal? I don't see the connection here. |
@fzipp - curry/apply with no unbound args provides the same output as the proposed lazy operator but is more general in form. So |
@mdcfrancis I see, thanks. |
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
|
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. |
You can do almost all of this today in the existing type system except for where you would want generics.
|
Is there ever a need to explicitly use Idris has declarative laziness without the explicit need to call |
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. |
@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 |
Question: 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. 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
} |
yeah, i just wanted to illustrate another implementation, in case when can learn something from it. |
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, |
Recently, I let my log functions return a bool, so that I can use bool AND to avoid a if-block:
It is simpler, though still not perfect. |
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
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. |
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. |
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. |
@mdcfrancis |
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. |
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. ;) |
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 log.Verbose("graph depth %d", @GraphDepth(g)) When used in an expression, When used as part of a type descriptor, Unwrapping a thunk does not have special syntax, since Regarding use with |
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:
|
How about a wild impossible idea? (it's Friday) What about having the ability to inject 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 What a hugely productive time-saver this would be. How much time I spend re-building code just to add and remove debug output!!! |
If you use tools like |
@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! |
@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. |
should 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 https://go.dev/play/p/x2zjn0Xdxno
It seems that for backwards compatibility, functions must opt-in to allow for a
If the function does not opt in to the |
Now that 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 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) }
} |
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
where
log.Verbose
only logs a message if some command line flag is specified, andGraphDepth
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.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 ofLazy
to know whether the callF()
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 abool
, 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
withstd::launch::deferred
or Rustfutures::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 typeT
, and when it needs the value it calls the function literal. At the call site people writefunc() T { return F() }
to delay the evaluation ofF
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 methodEval()
that returns a value of typeT
. It is not comparable, except tonil
. The only supported operation, other than operations like assignment or unary&
that apply to all types, is to call theEval
method. This has some similarities to the typebut it is not the same as regards type conversion.
A value
v
of typeT
may be implicitly converted to the typelazy T
. This produces a value whoseEval()
method returnsv
. A valuev
of typeT1
may be implicitly converted to the typelazy T2
ifT1
may be implicitly converted toT2
. In this case theEval()
method returnsT2(v)
. Similarly, a valuev
of typelazy T1
may be implicitly converted to the typelazy T2
ifT1
may be implicitly converted toT2
. In this case theEval()
method returnsT2(v.Eval())
.We introduce a new kind of expression,
lazy E
. This expression does not evaluateE
. Instead, it returns a value of typelazy T
that, when theEval
method is first called, evaluatesE
and returns the value to which it evaluates. If evaluation ofE
panics, then the panic occurs when theEval
method is first called. Subsequent calls of theEval
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 theEval
method and handle the value as though it has typeT
.Some additions will be needed to the reflect package. Those are not yet specified.
The builtin functions
panic
,print
, andprintln
, will not call theEval
method of a value of typelazy T
. If a value of typelazy T
is passed topanic
, any relevantrecover
will return a value of that type.That is the entire proposal.
Examples:
This permits writing
Calls to the function will look like
The
GraphDepth
function will only be called ifverboseFlag
istrue
.Note that it is also fine to write
without having to write
lazy step
, using the implicit conversion tolazy T
.If we adopt some form of generics, this will permit writing (edited original proposal to use current syntax):
This will be called as
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):
A promise would be created like this:
and used like this:
The text was updated successfully, but these errors were encountered: