Having said that,

What about a mid-ground approach?

Simply have a `deferred` type, which only gets evaluated if it is explicitly 
asked for. It builds a graph in the same way as dask, but it does not act as a 
proxy, but rather evaluates if explicitly asked.

From technical perspective, it would be the same as dask - non-invasive so 
would have minimal to none impact on existing code. One difference would be 
that the object would not manage operations, but rather leave it as expression 
at lower level of python.

From user’s perspective it would be similar to lambdas or dask, except with 
convenient syntax and seamless integration to existing code.

E.g.
a = `1`
print(type(a))          # <Deferred>
print(type(a.graph()))  # <Graph> 
print(a.eval())     # 1
a
print(type(a))          # <Deferred>
print(!a)               # 1
print(type(a))          # <Deferred>
b = !a
print(type(b))          # int
print(!1)               # 1
c = `a + 1`
d = `c + 2`
e = `c + d`             # simply builds graph
print(eval(e))          # 7 (this is only used if one is sure that it is 
<Deferred>)
print(!e)               # 7 (this untangles if <Deferred> or just pass through 
for anything else)

So no magic really, pretty straight forward.

Benefits for dask-like applications is to have a lower level & more performant 
deferred object with API to customise evaluation. For such applications 
evaluation would be done via `eval(obj)` or `obj.eval()` as it strictly has to 
be <Deferred> object and should fail if it’s not.

While convenient builtins would be useful for expression building, Infix 
operators and similar applications. In this case it does act very similarly as 
lambda, but solves a few of lambda’s inconveniences:
1) if using lambda/callables for this purpose, then can not use them as values. 
Same argument as for async/await vs using yield. Simply another layer of depth 
is needed to remove ambiguity.
2) Syntax is too cumbersome - something properly concise IMO should be 
available for it to be convenient.
3) More performant operations than e.g. `isinstance(value, Callable)`.

Function without arguments could be converted to <Deferred> via decorator or 
multiline expression could be defined via

```
a = 1
b = a + 1
b
```
———————————————————
Expressions would naturally be evaluated at the scope of their definition. 
Nothing new.

———————————————————
Regarding Break & Continue:

Can still be achieved:

from statements import Break


def ifelse(cond, if_true, if_false):
    if cond:
        return !if_true
    else:
        return !if_false

a = 0
while True:
    # This would require to be mindful about conditional values
    a += ifelse(a < 5, `expr()`, False) or Break(return_value=0)
    # or for it to be “perfect", PEP505 is needed
    a += ifelse(a < 5, `expr()`, None) ?? Break(return_value=0)
However, it could be done differently as well. If `Break()` takes effect in the 
scope where it is evaluated. Then:

from statements import Break


def add_or_break(cond, if_true, break_return):
    if cond:
        return if_true
    else:
        return `Break(return_value=break_return)`

a = 0
while True:
    a += !(add_or_break(a < 5, `expr()`, break_return=0))
———————————————————

So these would potentially provide a fairly flexible toolkit for expression 
building and seemingly covers all the cases that I was looking at including 
reasonable route to achieve https://peps.python.org/pep-0463/ 
<https://peps.python.org/pep-0463/>.

And it might be a feasible approach for deferred evaluation. Just not sure if 
this covers the needs that it is aiming to fulfil.


DG
> On 21 Jul 2023, at 10:31, Dom Grigonis <dom.grigo...@gmail.com> wrote:
> 
> This is very much implementation dependent. And I do not have a big opinion 
> here as I certainly haven’t spent enough time to have one. I am just 
> considering functionality from user’s perspective for the time being.
> 
>> Let's tackle just this one part for a moment. What does "ensure_eval"
>> do? Evaluate a proxy object but not evaluate anything else?
> 
> In case of the library I used, it would look something like:
> 
> def ensure_eval(x):
>     if isisnstance(x, Proxy) and x.unevaulated:
>         x.evaluate()
>     return x
> 
>> This should always succeed, right? Well, what if x is itself a Proxy
>> object? How does it know not to reevaluate it?
> 
> Not necessarily. I mean, yes, but only if deferred evaluation is done by 
> actually replacing value in the namespace dictionary. In case of this 
> example, where this specific package is used, it is not so and is not 
> intended to be so.
> 
> When operation is performed, the value is evaluated and returned, but it 
> remains proxy object. So if lazy is nested, the whole nesting is evaluated on 
> first operation which needs its value.
> 
> I see this one as “decision to be made” as opposed to “reason why it’s not 
> working”.
> 
> I am not sure about the “right” approach here. Actually replacing deferred 
> object with a value does sound invasive, although given there was a way to do 
> it elegantly and without loss in performance, it seems to be more robust and 
> eliminates maintenance of proxy object.
> 
> So the big question is, which of the 2 approaches to take:
> 1) Implementing a robust proxy object.
>     Less depth more breadth - ensuring proxy works with all objects / coming 
> up with protocols, which have to be implemented for non-standard types to be 
> compatible.
>     In this case both of assertions will and should fail, although `assert y 
> is y_` will always pass.
> 
> Example from library:
> obj = Proxy(lambda: 1)
> obj2 = Proxy(lambda: obj)
> obj is obj2         # False
> print(obj == obj2)  # True
> 
> 2) Replacing deferred evaluation with it’s value.
>     in this case, a fair bit of low level decisions such as the one you 
> indicated would have to be made, a lot of deep strings to pull
>     In this case, it very much depends how “is”, “type” and other `things` 
> behave with deferred objects. So if `x` is a proxy, and ‘is’ treats proxy 
> object and not it’s value (which is reasonable), then assertions will and 
> should fail. However, unraveling the whole stack on first evaluation does 
> simplify things a bit.Then ` y_ = ensure_eval(y)` in your example is a 
> redundant line. In practice, if deferred evaluation is used, one would need 
> to: `assert  ensure_eval(x) is ensure_eval(y)`. Also, looking at deferred 
> eval RST doc, the suggested mechanics are that expression is evaluated if 
> “later” is not re-called. So in theory, it might be reasonable to have `x = 
> later expr() <equiv> x = later later later expr()`. Instead of evaluating the 
> whole stack, just not allowing it to grow in the first place. Then builtin 
> `ensure_eval` is then straight forward single operation at C level, which 
> would mostly be used on low level checks, such as asserts in your examples. 
> It could even have its syntax instead of builtin function, given there are 
> only 2 entry points (definition of deferred & ensuring it’s evaluation). E.g.
> def expr():
>     print('Evaluating')
>     return 1
> 
> a = `expr()`
> b = `a`             # carry over lazy, but it's a new object at C level
> assert a is b       # False
> assert !a is !b     # True
> assert a is b       # True
> 
> 
> 
>> On 21 Jul 2023, at 08:46, Chris Angelico <ros...@gmail.com 
>> <mailto:ros...@gmail.com>> wrote:
>> 
>> On Fri, 21 Jul 2023 at 11:08, Dom Grigonis <dom.grigo...@gmail.com 
>> <mailto:dom.grigo...@gmail.com>> wrote:
>>> Also, can't find a way to ONLY force evaluation without any additional 
>>> operations in this specific library. A simple callable which evaluates if 
>>> unevaluated & returns would do. Then:
>>> 
>>> def IF(condition, when_true, when_false):
>>>    if condition:
>>>        return ensure_eval(when_true)
>>>    else:
>>>        return ensure_eval(when_false)
>>> 
>>> Controls evaluation if deferred objects are provided, but can also be used 
>>> with `normal` values.
>>> 
>> 
>> Let's tackle just this one part for a moment. What does "ensure_eval"
>> do? Evaluate a proxy object but not evaluate anything else? That seems
>> simple, but might very well be straight-up wrong. Consider:
>> 
>> def test_proxy(x):
>>    x_ = Proxy(x)
>>    y = ensure_eval(x_)
>>    y_ = ensure_eval(y)
>>    assert x is y
>>    assert x is y_
>> 
>> This should always succeed, right? Well, what if x is itself a Proxy
>> object? How does it know not to reevaluate it?
>> 
>> ChrisA
>> _______________________________________________
>> Python-ideas mailing list -- python-ideas@python.org 
>> <mailto:python-ideas@python.org>
>> To unsubscribe send an email to python-ideas-le...@python.org 
>> <mailto:python-ideas-le...@python.org>
>> https://mail.python.org/mailman3/lists/python-ideas.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/GTCRUPQOFP63VA2T2N7BVBO6JKR3D7I6/
>> Code of Conduct: http://python.org/psf/codeofconduct/
> 

_______________________________________________
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/63J3WY63IQ6SC37LAQDLDSFXFJTIVOFG/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to