David Mertz writes:
 > On Fri, May 29, 2020 at 1:12 PM Alex Hall <alex.moj...@gmail.com> 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
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/EMUJMSSMICGXGTVH3BQO6M2W6GMH445H/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to