Alternative to PEP 532: delayed evaluation of expressions

Creating a new thread, instead of hijacking the PEP 532 discussion. From PEP 532: this PEP
Instead of new syntax that only works in this one specific case, I'd prefer a more general solution. I accept being "more general" probably seals the deal in killing any proposal! I realize the following proposal has at least been hinted at before, but I couldn't find a specific discussion about it. Since it applies to the short-circuiting issues addressed by PEP 532 and its predecessors, I thought I'd bring it up here. It could also be used to solve some of the problems addressed by the rejected PEP 463 (Exception-catching expressions). See also PEP 312 (Simple Implicit Lambda). It might also be usable for some of the use cases presented in PEP 501 (General purpose string interpolation, aka i-strings). I'd rather see the ability to have unevaluated expressions, that can later be evaluated. I'll use backticks here to mean: "parse, but do not execute the enclosed code". This produces an object that can later be evaluated with a new builtin I'll call "evaluate_now". Obviously these are strawmen, and partly chosen to be ugly and unacceptable names and symbols in the form I'll discuss here. Then you could write a function: eval_else(`foo.bar`, `some_func()`) whose value is foo.bar, unless foo.bar cannot be evaluated, in which case the value is some_func(). def eval_else(expr, fallback, exlist=(AttributeError,)): try: return evaluate_now(expr) except exlist: return evaluate_now(fallback) Exactly which exceptions you catch is up to you. Of course there's the chance that someone would pass in something for which the caught exception is too broad, and it's raised deep inside evaluating the first expression, but that's no different than catching exceptions now. Except I grant that hiding the try/except inside a called function increases the risk. Like f-strings, the expressions are entirely created at the site they're specified inside ``. So they'd have access to locals and globals, etc., at the definition site. def x(foo, i): return eval_else(`foo.bar`, `some_func(i, __name__)`) And like the expressions in f-strings, they have to be valid expressions. But unlike f-strings, they aren't evaluated right when they're encountered. The fact that they may never be evaluated is one of their features. For example the if/else expression: if_else(`y`, x is None, `x.a`) could be defined as being exactly like: y if x is None else x.a including only evaluating x.a if x is not None. def if_else(a, test, b): if test: return evaluate_now(a) return evaluate_now(b) You could do fancier things that require more than 2 expressions. Whether `` returns an AST that could later be manipulated, or it's something else that's opaque is another discussion. Let's assume it's opaque for now. You could go further and say that any argument to a function that's specially marked would get an unevaluated expression. Suppose that you can mark arguments as & to mean "takes an unevaluated expression". Then you could write: def if_else(&a, test, &b): if test: return evaluate_now(a) return evaluate_now(b) And call it as: if_else(y, x is None, x.a) But now you've made it non-obvious at the caller site exactly what's happening. There are other downsides, such as only being able to create an unevaluated expression when calling a function. Or maybe that's a good thing! In any event, having unevaluated expressions would open up more possibilities than just the short-circuit evaluation model. And it doesn't involve a new protocol. Eric.

[top posting from my phone] Chris Angelico points out the & part of the idea interacts poorly with *args and **kwargs, so I drop that idea. Re-reading PEP 312, this idea is basically identical, with different spellings. The point remains: do we want to be able to create unevaluated expressions that can be evaluated at a different point? -- Eric.

On Sun, Nov 06, 2016 at 09:31:06AM -0500, Eric V. Smith wrote:
The point remains: do we want to be able to create unevaluated expressions that can be evaluated at a different point?
I sometimes think that such unevaluated expressions (which I usually call "thunks") would be cool to have. But in more realistic moments I think that they're a solution looking for a problem to solve. If your PEP suggests a problem that they will solve, I'm interested. But note that we already have two ways of generating thunk-like objects: functions and compiled byte-code. thunk = lambda: a + b - c thunk() thunk = compile('a + b - c', '', 'single') eval(thunk) Both are very heavyweight: the overhead of function call syntax is significant, and the keyword "lambda" is a lot of typing just to delay evaluation of an expression. compile(...) is even worse. -- Steve

On 2016-11-06 21:46, Steven D'Aprano wrote:
I sometimes want these too. But note that both the solutions you propose are quite a ways from a true "unevaluated expression". The big problem with an ordinary lambda (or def) is that you cannot explicitly control where it will decide to look for its free variables. If it uses a local variable from an enclosing namespace, it will always look for it in that namespace, so you can't "patch in" a value by setting a global variable. If it uses a variable that isn't local to any enclosing namespace, it will always look for it in the global namespace, so you can't patch in a value by setting a local variable in the context where you're calling the function. You can get around this in a def by, for instance, using global to mark all variables global, and then using eval to pass in a custom global namespace. But that is a lot of boilerplate. What I want (when I want this) is a way to create a function that will allow the injection of values for *any* variables, regardless of whether the function originally thought they were local, nonlocal (i.e., local to some enclosing scope) or global. The way it is now, the status of a function's variables is inextricably linked to the syntactic context where it was defined. This is a good thing most of the time, but it's not what you want if you want to define an expression that should later be evaluated in some other context. I consider the compile-based solution a nonstarter, because it puts the code in a string. With the code in a string, you are blocked from using syntax highlighting or any other handy editor features. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Eric V. Smith wrote:
So far you've essentially got a compact notation for a lambda with no arguments. Suggestions along these lines have been made before, but didn't go anywhere.
Now *that* would be truly interesting, but it would complicate some fundamental parts of the implementation tremendously. Before calling any function, it would be necessary to introspect it to find out which parameters should be evaluated. Alternatively, every parameter would have to be passed as a lambda, with the function deciding which ones to evaluate. I fear this would be far too big a change to swallow. -- Greg

On Sun, Nov 6, 2016 at 5:06 AM, Eric V. Smith <eric@trueblade.com> wrote:
If we're considering options along these lines, then I think the local optimum is actually a "quoted-call" operator, rather than a quote operator. So something like (borrowing Rust's "!"): eval_else!(foo.bar, some_func()) being sugar for eval_else.__macrocall__(<unevaluated thunk of foo.bar>, <unevaluated thunk of some_func()>) You can trivially use this to recover a classic quote operator if you really want one: def quote!(arg): return arg but IMO this way is more ergonomic for most use cases (similar to your '&' suggestion), while retaining the call-site marking that "something magical is happening here" (which is also important, both for readability + implementation simplicity -- it lets the compiler know statically when it needs to retain the AST, solving the issue that Greg pointed out). Some other use cases: Log some complicated object, but only pay the cost of stringifying the object if debugging is enabled: log.debug!(f"Message: {message_object!r}") Generate a plot where the axes are automatically labeled "x" and "np.sin(x)" (this is one place where R's plotting APIs are more convenient than Python's): import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10) plt.plot!(x, np.sin(x)) What PonyORM does, but without the thing where currently their implementation involves decompiling bytecode...: db.select!(c for c in Customer if sum(c.orders.price) > 1000) Filtering out a subset of rows from a data frame in pandas; 'height' and 'age' refer to columns in the data frame (equivalent to data_frame[data_frame["height"] > 100 and data_frame["age"] < 5], but more ergonomic and faster (!)): data_frame.subset!(height > 100 and age < 5) (IIRC pandas has at least experimented with various weird lambda hacks for this kind of thing; not sure what the current status is.) Every six months or so I run into someone who's really excited about the idea of adding macros to python, and I suggest this approach. So far none of them have been excited enough to actually write a PEP, but if I were going to write a PEP then this is the direction that I'd take :-). -n -- Nathaniel J. Smith -- https://vorpus.org

On 7 November 2016 at 02:32, Nathaniel Smith <njs@pobox.com> wrote:
Oh great! Good to know I am not alone thinking in this direction. I have however one minor problem here: the problem is that "!" sign is almost invisible in the code, unless there is syntax highlighting which paints it in some very bright color. On the other hand I am not sure if it *must* be very visible... So to make it more distinctive in code I would propose something like: macros<>( x, y ) macros>( x, y ) macros::( x, y ) And those are already used operators, sad :( It would look so neat... But probably still possible to do? Mikhail

On Nov 6, 2016 7:32 PM, "Nathaniel Smith" <njs@pobox.com> wrote:
Would the log.debug implementation need to fetch the context to evaluate the delayed expression (say by using sys._getframe) or would that be bound? Is a frame necessary or just a (bound?) symbol table? Could a substitute be provided by on evaluation? Curious how this looks to the callee and what is possible. Also what is the meaning (if desirable) of something like: def debug!(...): pass Persistent delayed calls? Delayed default arguments? Something else? Not valid?

C Anthony Risinger wrote:
Not if it expands to log.debug(lambda: f"Message: {message_object!r}")
Also what is the meaning (if desirable) of something like:
def debug!(...): pass
Nothing like that would be needed. The implementation of debug() would just be an ordinary function receiving callable objects as parameters. -- Greg

On Sun, Nov 6, 2016 at 9:08 PM, C Anthony Risinger <anthony@xtfx.me> wrote:
There are a lot of ways one could go about it -- I'll leave the details to whoever decides to actually write the PEP :-) -- but one sufficient solution would be to just pass AST objects. Those are convenient (the compiler has just parsed the code anyway), they allow the code to be read or modified before use (in case you want to inject variables, or convert to SQL as in the PonyORM case, etc.), and if you want to evaluate the thunks then you can look up the appropriate environment using sys._getframe and friends. Maybe one can do even better, but simple ASTs are a reasonable starting point. -n -- Nathaniel J. Smith -- https://vorpus.org

On 7 November 2016 at 16:19, Nathaniel Smith <njs@pobox.com> wrote:
PEP 501 suggested taking the machinery that was added to support f-string interpolation and making it possible to separate the expression evaluation and string rendering steps: https://www.python.org/dev/peps/pep-0501/ Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote:
Xonsh does it: http://xon.sh/tutorial_macros.html At least for "function" call, and make use of Python type annotations to decide whether to expand the expression or not, or passing, string, Ast, to the defined macro. I haven't tried it in a while but there were some ideas floating around for context-manager as well, to get the block they wrap. -- M

On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote:
We abandoned the experiment because we couldn't make it work properly. There's no way to inject local variables in the appropriate scope around the lambda function: https://github.com/pandas-dev/pandas/issues/13040

Would it be helped by an explicit "free variable" marker? (I'm sure I've seen someone demo a prototype of this):
data_frame.subset($height > 100 and $age < 5)
Which essentially translates into:
data_frame.subset(lambda **a: a["height"] > 100 and a["age"] < 5)
Maybe the generated thunk can keep the AST around too in case there are better transformations available (e.g. convert into a SQL/Blaze query), but simply calling it with named arguments or a mapping (I am deliberately not requiring the eventual caller to know the exact signature) would get you the result with a mix of closed and free variables. Cheers, Steve Top-posted from my Windows Phone -----Original Message----- From: "Stephan Hoyer" <shoyer@gmail.com> Sent: 11/10/2016 18:09 To: "Nathaniel Smith" <njs@pobox.com> Cc: "Eric V. Smith" <eric@trueblade.com>; "Python-Ideas" <python-ideas@python.org> Subject: Re: [Python-ideas] Alternative to PEP 532: delayed evaluation ofexpressions On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote: Filtering out a subset of rows from a data frame in pandas; 'height' and 'age' refer to columns in the data frame (equivalent to data_frame[data_frame["height"] > 100 and data_frame["age"] < 5], but more ergonomic and faster (!)): data_frame.subset!(height > 100 and age < 5) (IIRC pandas has at least experimented with various weird lambda hacks for this kind of thing; not sure what the current status is.) We abandoned the experiment because we couldn't make it work properly. There's no way to inject local variables in the appropriate scope around the lambda function: https://github.com/pandas-dev/pandas/issues/13040

On 6 November 2016 at 23:06, Eric V. Smith <eric@trueblade.com> wrote:
Being more general isn't the goal here: just as with decorators, context managers, and coroutines, the goal is pattern extraction based on idioms that people *already use* (both in Python and in other languages) to make them shorter (and hence easier to recognise), as well as to give them names (and hence make them easier to learn, teach and find on the internet). Consider the conditional expression usage examples in the PEP: value1 = expr1.field.of.interest if expr1 is not None else None value2 = expr2["field"]["of"]["interest"] if expr2 is not None else None value3 = expr3 if expr3 is not None else expr4 if expr4 is not None else expr5 Which would become the following in the basic form of the PEP: value1 = missing(expr1) else expr1.field.of.interest value2 = missing(expr2) else expr2.["field"]["of"]["interest"] value3 = exists(expr3) else exists(expr4) else expr5 or else the following in the more comprehensive variant that also allows short-circuiting "if" expressions (and would hence mainly include "missing()" as the answer to "What does 'not exists()' return?"): value1 = expr1.field.of.interest if exists(expr1) value2 = expr2.["field"]["of"]["interest"] if exists(expr2) value3 = exists(expr3) else exists(expr4) else expr5 Now, how would those look with a lambda based solution instead? value1 = eval_if_exists(expr, lambda: expr1.field.of.interest) value2 = eval_if_exists(expr, lambda: expr2["field"]["of"]["interest"]) value3 = first_existing(expr3, lambda: expr4, lambda: expr5) This is not an idiom people typically use, and that's not solely due to them finding the "lambda:" keyword unattractive or function calls being much slower than inline expressions. We can swap in a Java inspired "()->" operator to help illustrate that (where "()" indicates an empty parameter list, and the "->" indicates the LHS is a parameter list and the RHS is a lambda expression): value1 = eval_if_exists(expr, ()->expr1.field.of.interest) value2 = eval_if_exists(expr, ()->expr2["field"]["of"]["interest"]) value3 = first_existing(expr3, ()->expr4, ()->expr5) Without even getting into the subtle differences between inline evaluation and nested scopes in Python, the fundamental challenge with this approach is that it takes something that was previously about imperative conditional control flow and turns it into a use case for callback based programming, which requires an understanding of custom first class functions at the point of *use*, which is conceptually a much more difficult concept than conditional evaluation. By contrast, while as with any other protocol you would need to understand object-oriented programming in order to define your own circuit breakers, *using* circuit breakers would only require knowledge of imperative condition control flow, as well as how the specific circuit breaker being used affects the "if" and "else" branches. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

[top posting from my phone] Chris Angelico points out the & part of the idea interacts poorly with *args and **kwargs, so I drop that idea. Re-reading PEP 312, this idea is basically identical, with different spellings. The point remains: do we want to be able to create unevaluated expressions that can be evaluated at a different point? -- Eric.

On Sun, Nov 06, 2016 at 09:31:06AM -0500, Eric V. Smith wrote:
The point remains: do we want to be able to create unevaluated expressions that can be evaluated at a different point?
I sometimes think that such unevaluated expressions (which I usually call "thunks") would be cool to have. But in more realistic moments I think that they're a solution looking for a problem to solve. If your PEP suggests a problem that they will solve, I'm interested. But note that we already have two ways of generating thunk-like objects: functions and compiled byte-code. thunk = lambda: a + b - c thunk() thunk = compile('a + b - c', '', 'single') eval(thunk) Both are very heavyweight: the overhead of function call syntax is significant, and the keyword "lambda" is a lot of typing just to delay evaluation of an expression. compile(...) is even worse. -- Steve

On 2016-11-06 21:46, Steven D'Aprano wrote:
I sometimes want these too. But note that both the solutions you propose are quite a ways from a true "unevaluated expression". The big problem with an ordinary lambda (or def) is that you cannot explicitly control where it will decide to look for its free variables. If it uses a local variable from an enclosing namespace, it will always look for it in that namespace, so you can't "patch in" a value by setting a global variable. If it uses a variable that isn't local to any enclosing namespace, it will always look for it in the global namespace, so you can't patch in a value by setting a local variable in the context where you're calling the function. You can get around this in a def by, for instance, using global to mark all variables global, and then using eval to pass in a custom global namespace. But that is a lot of boilerplate. What I want (when I want this) is a way to create a function that will allow the injection of values for *any* variables, regardless of whether the function originally thought they were local, nonlocal (i.e., local to some enclosing scope) or global. The way it is now, the status of a function's variables is inextricably linked to the syntactic context where it was defined. This is a good thing most of the time, but it's not what you want if you want to define an expression that should later be evaluated in some other context. I consider the compile-based solution a nonstarter, because it puts the code in a string. With the code in a string, you are blocked from using syntax highlighting or any other handy editor features. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Eric V. Smith wrote:
So far you've essentially got a compact notation for a lambda with no arguments. Suggestions along these lines have been made before, but didn't go anywhere.
Now *that* would be truly interesting, but it would complicate some fundamental parts of the implementation tremendously. Before calling any function, it would be necessary to introspect it to find out which parameters should be evaluated. Alternatively, every parameter would have to be passed as a lambda, with the function deciding which ones to evaluate. I fear this would be far too big a change to swallow. -- Greg

On Sun, Nov 6, 2016 at 5:06 AM, Eric V. Smith <eric@trueblade.com> wrote:
If we're considering options along these lines, then I think the local optimum is actually a "quoted-call" operator, rather than a quote operator. So something like (borrowing Rust's "!"): eval_else!(foo.bar, some_func()) being sugar for eval_else.__macrocall__(<unevaluated thunk of foo.bar>, <unevaluated thunk of some_func()>) You can trivially use this to recover a classic quote operator if you really want one: def quote!(arg): return arg but IMO this way is more ergonomic for most use cases (similar to your '&' suggestion), while retaining the call-site marking that "something magical is happening here" (which is also important, both for readability + implementation simplicity -- it lets the compiler know statically when it needs to retain the AST, solving the issue that Greg pointed out). Some other use cases: Log some complicated object, but only pay the cost of stringifying the object if debugging is enabled: log.debug!(f"Message: {message_object!r}") Generate a plot where the axes are automatically labeled "x" and "np.sin(x)" (this is one place where R's plotting APIs are more convenient than Python's): import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10) plt.plot!(x, np.sin(x)) What PonyORM does, but without the thing where currently their implementation involves decompiling bytecode...: db.select!(c for c in Customer if sum(c.orders.price) > 1000) Filtering out a subset of rows from a data frame in pandas; 'height' and 'age' refer to columns in the data frame (equivalent to data_frame[data_frame["height"] > 100 and data_frame["age"] < 5], but more ergonomic and faster (!)): data_frame.subset!(height > 100 and age < 5) (IIRC pandas has at least experimented with various weird lambda hacks for this kind of thing; not sure what the current status is.) Every six months or so I run into someone who's really excited about the idea of adding macros to python, and I suggest this approach. So far none of them have been excited enough to actually write a PEP, but if I were going to write a PEP then this is the direction that I'd take :-). -n -- Nathaniel J. Smith -- https://vorpus.org

On 7 November 2016 at 02:32, Nathaniel Smith <njs@pobox.com> wrote:
Oh great! Good to know I am not alone thinking in this direction. I have however one minor problem here: the problem is that "!" sign is almost invisible in the code, unless there is syntax highlighting which paints it in some very bright color. On the other hand I am not sure if it *must* be very visible... So to make it more distinctive in code I would propose something like: macros<>( x, y ) macros>( x, y ) macros::( x, y ) And those are already used operators, sad :( It would look so neat... But probably still possible to do? Mikhail

On Nov 6, 2016 7:32 PM, "Nathaniel Smith" <njs@pobox.com> wrote:
Would the log.debug implementation need to fetch the context to evaluate the delayed expression (say by using sys._getframe) or would that be bound? Is a frame necessary or just a (bound?) symbol table? Could a substitute be provided by on evaluation? Curious how this looks to the callee and what is possible. Also what is the meaning (if desirable) of something like: def debug!(...): pass Persistent delayed calls? Delayed default arguments? Something else? Not valid?

C Anthony Risinger wrote:
Not if it expands to log.debug(lambda: f"Message: {message_object!r}")
Also what is the meaning (if desirable) of something like:
def debug!(...): pass
Nothing like that would be needed. The implementation of debug() would just be an ordinary function receiving callable objects as parameters. -- Greg

On Sun, Nov 6, 2016 at 9:08 PM, C Anthony Risinger <anthony@xtfx.me> wrote:
There are a lot of ways one could go about it -- I'll leave the details to whoever decides to actually write the PEP :-) -- but one sufficient solution would be to just pass AST objects. Those are convenient (the compiler has just parsed the code anyway), they allow the code to be read or modified before use (in case you want to inject variables, or convert to SQL as in the PonyORM case, etc.), and if you want to evaluate the thunks then you can look up the appropriate environment using sys._getframe and friends. Maybe one can do even better, but simple ASTs are a reasonable starting point. -n -- Nathaniel J. Smith -- https://vorpus.org

On 7 November 2016 at 16:19, Nathaniel Smith <njs@pobox.com> wrote:
PEP 501 suggested taking the machinery that was added to support f-string interpolation and making it possible to separate the expression evaluation and string rendering steps: https://www.python.org/dev/peps/pep-0501/ Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote:
Xonsh does it: http://xon.sh/tutorial_macros.html At least for "function" call, and make use of Python type annotations to decide whether to expand the expression or not, or passing, string, Ast, to the defined macro. I haven't tried it in a while but there were some ideas floating around for context-manager as well, to get the block they wrap. -- M

On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote:
We abandoned the experiment because we couldn't make it work properly. There's no way to inject local variables in the appropriate scope around the lambda function: https://github.com/pandas-dev/pandas/issues/13040

Would it be helped by an explicit "free variable" marker? (I'm sure I've seen someone demo a prototype of this):
data_frame.subset($height > 100 and $age < 5)
Which essentially translates into:
data_frame.subset(lambda **a: a["height"] > 100 and a["age"] < 5)
Maybe the generated thunk can keep the AST around too in case there are better transformations available (e.g. convert into a SQL/Blaze query), but simply calling it with named arguments or a mapping (I am deliberately not requiring the eventual caller to know the exact signature) would get you the result with a mix of closed and free variables. Cheers, Steve Top-posted from my Windows Phone -----Original Message----- From: "Stephan Hoyer" <shoyer@gmail.com> Sent: 11/10/2016 18:09 To: "Nathaniel Smith" <njs@pobox.com> Cc: "Eric V. Smith" <eric@trueblade.com>; "Python-Ideas" <python-ideas@python.org> Subject: Re: [Python-ideas] Alternative to PEP 532: delayed evaluation ofexpressions On Sun, Nov 6, 2016 at 5:32 PM, Nathaniel Smith <njs@pobox.com> wrote: Filtering out a subset of rows from a data frame in pandas; 'height' and 'age' refer to columns in the data frame (equivalent to data_frame[data_frame["height"] > 100 and data_frame["age"] < 5], but more ergonomic and faster (!)): data_frame.subset!(height > 100 and age < 5) (IIRC pandas has at least experimented with various weird lambda hacks for this kind of thing; not sure what the current status is.) We abandoned the experiment because we couldn't make it work properly. There's no way to inject local variables in the appropriate scope around the lambda function: https://github.com/pandas-dev/pandas/issues/13040

On 6 November 2016 at 23:06, Eric V. Smith <eric@trueblade.com> wrote:
Being more general isn't the goal here: just as with decorators, context managers, and coroutines, the goal is pattern extraction based on idioms that people *already use* (both in Python and in other languages) to make them shorter (and hence easier to recognise), as well as to give them names (and hence make them easier to learn, teach and find on the internet). Consider the conditional expression usage examples in the PEP: value1 = expr1.field.of.interest if expr1 is not None else None value2 = expr2["field"]["of"]["interest"] if expr2 is not None else None value3 = expr3 if expr3 is not None else expr4 if expr4 is not None else expr5 Which would become the following in the basic form of the PEP: value1 = missing(expr1) else expr1.field.of.interest value2 = missing(expr2) else expr2.["field"]["of"]["interest"] value3 = exists(expr3) else exists(expr4) else expr5 or else the following in the more comprehensive variant that also allows short-circuiting "if" expressions (and would hence mainly include "missing()" as the answer to "What does 'not exists()' return?"): value1 = expr1.field.of.interest if exists(expr1) value2 = expr2.["field"]["of"]["interest"] if exists(expr2) value3 = exists(expr3) else exists(expr4) else expr5 Now, how would those look with a lambda based solution instead? value1 = eval_if_exists(expr, lambda: expr1.field.of.interest) value2 = eval_if_exists(expr, lambda: expr2["field"]["of"]["interest"]) value3 = first_existing(expr3, lambda: expr4, lambda: expr5) This is not an idiom people typically use, and that's not solely due to them finding the "lambda:" keyword unattractive or function calls being much slower than inline expressions. We can swap in a Java inspired "()->" operator to help illustrate that (where "()" indicates an empty parameter list, and the "->" indicates the LHS is a parameter list and the RHS is a lambda expression): value1 = eval_if_exists(expr, ()->expr1.field.of.interest) value2 = eval_if_exists(expr, ()->expr2["field"]["of"]["interest"]) value3 = first_existing(expr3, ()->expr4, ()->expr5) Without even getting into the subtle differences between inline evaluation and nested scopes in Python, the fundamental challenge with this approach is that it takes something that was previously about imperative conditional control flow and turns it into a use case for callback based programming, which requires an understanding of custom first class functions at the point of *use*, which is conceptually a much more difficult concept than conditional evaluation. By contrast, while as with any other protocol you would need to understand object-oriented programming in order to define your own circuit breakers, *using* circuit breakers would only require knowledge of imperative condition control flow, as well as how the specific circuit breaker being used affects the "if" and "else" branches. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
participants (11)
-
Brendan Barnwell
-
C Anthony Risinger
-
Eric V. Smith
-
Greg Ewing
-
Matthias Bussonnier
-
Mikhail V
-
Nathaniel Smith
-
Nick Coghlan
-
Stephan Hoyer
-
Steve Dower
-
Steven D'Aprano