David Mertz writes:
On Fri, May 29, 2020 at 1:12 PM Alex Hall
wrote: def foo(a=17, b=42,, x=delayed randint(0,9), y=delayed randrange(1,100)):
if something: # The simple case is realizing a direct delayed val = concretize x elif something_else: # This line creates a call graph, not a computation z = ((y + 3) * x)**10 # Still call graph land w = a / z # Only now do computation (and decide randoms) val = concretize w - b
But if I understand correctly, a delayed value is concretized once, then the value is cached and remains concrete. So if we still have early binding, then x will only have one random value, unlike Stephen's lambda which generates a new value each time it's called.
I don't think that's required by the example code I wrote. The two things that are concretized, 'x' and 'w-b', are assigned to a different name. I'm taking that as meaning "walk the call graph to produce a value" rather than "transform the underlying object". So in the code, x and y would stay permanently as a special delayed object.
I think this is the correct approach, since TOOWTDI for collapsing the waveform early is "x = concretize x". TOOWDTI for preserving the call graph for a caching DeferredType doesn't exist yet. Also, Alex's interpretation makes "concretize" a name-binding operation, but I see no need for that. In another post you mention Vaex. I wonder if it's really that hard to design a language that's lazy until you need a concrete object, and it automatically gives you one. That's the way Haskell works, for example. (Making that work with existing Python semantics without explicit syntax is probably another story, but I wonder if it might be possible to do it with a minimum of explicit concretizing.)
This is a lot like a lambda, I recognize.
Except that lambda works "top-down", and it's not immediately obvious to that intermediate computations need do any looking because they could work "bottom-up". That is, DeferredType could define a full complement of dunders, all of which construct call graphs and return a corresponding DeferredType instance. This would give you full control over which subexpressions were evaluated eagerly, and which deferred. I don't think you need explicit syntax for this. The question would be do you want that level of control, or do you more often just want to defer whole expressions? (That latter would be the case if you had a expression that contained several expensive subexpressions, none of which would benefit from being cached while others are recomputed more frequently.) If the latter, I think you need syntax BTW, going back to the question of mutable defaults, it occurs to me that there is an "obvious" idiom for self-documenting sentinels for defaults that are deferred because you want a new instance each time the function is called: use the constructors! Here are some empty mutables: def foo(x=list): if x == list: x = x() def bar(x=dict): if x == dict: x = x() And here's a time-varying immutable: import datetime def baz(x=datetime.datetime.now): if x == datetime.datetime.now: x = x() I guess this fails more or less amusingly if the constructor is redefined. Of course any callable object could be the sentinel. It doesn't need to be a type or a factory function for this device to work. However, I don't see a use case for that generality. Steve