I could emulate the "where" semantics as described by Steven using the  
class statement and eval :

I guess it is useful if someone want to try refactoring a piece of "real world"  code, and see
if it really feels better - then we could try to push for the "where" syntax, which I kinda like:

(This emulation requires the final expression to be either a string to be eval'ed,
or a lambda function - but then some namespace retrieval and parameter matching
would require a bit more code on the metaclass):



class Where(type):
   def __new__(metacls, name, bases, namespace, *, expr=''):
       return eval(expr, namespace)

def sqroot(n):
   class roots(expr="[(-b + r)/ (2 * a)  for r in (+ delta **0.5, - delta ** 0.5) ]", 
metaclass=Where):
       a, b, c = n
       delta = b ** 2 - 4 * a * c

   return roots




On 21 June 2017 at 10:31, Brice PARENT <contact@brice.xyz> wrote:

I might not have understood it completely, but I think the use cases would probably better be splitted into two categories, each with a simple solution (simple in usage at least):

When we just want a tiny scope for a variable:

Syntax:

with [assignment]:
    # use of the variable

# assigned variable is now out of scope

Examples:

with b = a + 1:
    y = b + 2

# we can use y here, but not b

or

with delta = lambda a, b, c: b**2 - 4 * a * c:
    x1 = (- b - math.sqrt(delta(a, b, c))) / (2 * a)
    x2 = (- b + math.sqrt(delta(a, b, c))) / (2 * a)

# delta func is now out of scope and has been destroyed

We don't keep unnecessarily some variables, as well as we don't risk any collision with outer scopes (and we preserve readability by not declaring a function for that).

It would probably mean the assignment operator should behave differently than it does now which could have unexpected (to me) implications. It would have to support both __enter__ and __exit__ methods, but I don't know if this makes any sense. I don't know if with a + 1 as b: would make a better sense or be a side-effect or special cases hell.

When we want to simplify a comprehension:

(although it would probably help in many other situations)

Syntax:

prepare_iterable(sequence, *functions)

which creates a new iterable containing tuples like (element, return_of_function_1, return_of_function_2, ...)

Examples:

[m(spam)[eggs] for _, m in prepare_iterable(sequence, lambda obj: obj[0].field.method) if m]

or, outside of a comprehension:

sequence = [0, 1, 5]
prepare_iterable(sequence, lambda o: o * 3, lambda o: o + 1)
# -> [(0, 0, 1), (1, 3, 2), (5, 15, 6)]

The "prepare_iterable" method name might or might not be the right word to use. But English not being my mother language, I'm not the right person to discuss this...
It would be a function instead of a method shared by all iterables to be able to yield the elements instead of processing the hole set of data right from the start.
This function should probably belong to the standard library but probably not in the general namespace.

-- Brice

Le 17/06/17 à 12:27, Steven D'Aprano a écrit :
On Sat, Jun 17, 2017 at 09:03:54AM +0200, Sven R. Kunze wrote:
On 17.06.2017 02:27, Steven D'Aprano wrote:
I think this is somewhat similar to a suggestion of Nick Coghlan's. One
possible syntax as a statement might be:

y = b + 2 given:
    b = a + 1
Just to get this right:this proposal is about reversing the order of 
chaining expressions?
Partly. Did you read the PEP?

https://www.python.org/dev/peps/pep-3150/

I quote:

    The primary motivation is to enable a more declarative style of 
    programming, where the operation to be performed is presented to the 
    reader first, and the details of the necessary subcalculations are 
    presented in the following indented suite.
    [...]
    A secondary motivation is to simplify interim calculations in module 
    and class level code without polluting the resulting namespaces.

It is not *just* about reversing the order, it is also about avoiding 
polluting the current namespace (global, or class) with unnecessary 
temporary variables. This puts the emphasis on the important part of the 
expression, not the temporary/implementation variables:

    page = header + body + footer where:
        header = ...
        body = ... 
        footer = ... 

There is prior art: the "where" and "let" clauses in Haskell, as well as 
mathematics, where it is very common to defer the definition of 
temporary variables until after they are used.


Instead of:

b = a + 1
c = b + 2

we could write it in reverse order:

c = b + 2 given/for:
    b = a + 1
Right. But of course such a trivial example doesn't demonstrate any 
benefit. This might be a better example.

Imagine you have this code, where the regular expression and the custom 
sort function are used in one place only. Because they're only used 
*once*, we don't really need them to be top-level global names, but 
currently we have little choice.

regex = re.compile(r'.*?(\d*).*')

def custom_sort(string):
    mo = regex.match(string)
    ... some implementation
    return key

# Later
results = sorted(some_strings, key=custom_sort)

# Optional
del custom_sort, regex


Here we get the order of definitions backwards: the thing we actually 
care about, results = sorted(...), is defined last, and mere 
implementation details are given priority as top-level names that 
either hang around forever, or need to be explicitly deleted.

Some sort of "where" clause could allow:

results = sorted(some_strings, key=custom_sort) where:
    regex = re.compile(r'.*?(\d*).*')

    def custom_sort(string):
        mo = regex.match(string)
        ... some implementation
        return key


If this syntax was introduced, editors would soon allow you to fold the 
"where" block and hide it. The custom_sort and regex names would be 
local to the where block and the results = ... line.

Another important use-case is comprehensions, where we often have to 
repeat ourselves:

[obj[0].field.method(spam)[eggs] for obj in sequence if obj[0].field.method]

One work around:

[m(spam)[eggs] for m in [obj[0].field.method for obj in sequence] if m]

But perhaps we could do something like:

[m(spam)[eggs] for obj in sequence where m = obj[0].field.method if m]

or something similar.



If so, I don't know if it just complicates the language with a feature 
which does not save writing nor reading 
It helps to save reading, by pushing less-important implementation 
details of an expression into an inner block where it is easy to ignore 
them. Even if you don't have an editor which does code folding, it is 
easy to skip over an indented block and just read the header line, 
ignoring the implementation. We already do this with classes, functions, 
even loops:

    class K:
        ... implementation of K

    def func(arg):
        ... implementation of func

    for x in seq:
        ... implementation of loop body

    page = header + body + footer where:
        ... implementation of page


As a general rule, any two lines at the same level of indentation are 
read as being of equal importance. When we care about the implementation 
details, we "drop down" into the lower indentation block. But when 
skimming the code for a high-level overview, we skip the details of 
indented blocks and focus only on the current level:

    class K:
    def func(arg):
    for x in seq:
    page = header + body + footer where:


(That's why editors often provide code folding, to hide the details of 
an indented block. But even without that feature, we can do it in our 
own head, although not as effectively.)





_______________________________________________
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/