`for` loop abstraction using `$` symbol

I appreciate thoughts and feedback on the following idea.

Idea:

To abstract sequential execution of the for loop using $ (or another suitable) symbol, such as: $iterable$
This translates into sequential python statements, all exactly the same, except the iterator value updates. So while * is unpacking in the horizontal way within a statement, $ can be thought of unpacking statements themselves in a vertical way.

Definition:
Code example 1:

#Current
for name in names:
    print(name)

is equivalent in execution to

#Proposed
print($names$)

Code example 2:

#Current:
for firstname, lastname in zip(firstnames, lastnames):
    print(firstname, lastname)

is equivalent in execution to

#Proposed:
print( $firstnames$, $lastnames$ )

Code example 3: Nested loops are specified using a numeric parameter before $.

#Current:
for item in items:
    for size, color in zip(sizes, colors):
        print(item, size, color)

is equivalent in execution to

#Proposed:
print( $items$, 2$sizes$, 2$colors$ )

Whenever in doubt about the rules, the developer can imagine the (nested) for loops.

Examples and features:
Example 1:

#Current, method 1:
total = 0
for number in numbers:
    total += number 
#Current, method 2:
total = sum(numbers)
#Proposed:
total = 0
total += $numbers$

Example 2:

#Current: 
my_dict = dict(zip(keys, values))
#Proposed:
my_dict = {}
my_dict[$keys$] = $values$

Example 3:

#Current method 1:
filtered = []
for value in values:
    filtered += [] if value>5 else [value]
#Current method 2:
filtered = [value for value in values if value <= 5]
#Current method 3:
filtered = list(filter(lambda value: value<=5, values))
#Proposed method 1:
filtered = []
filtered += [] if $values$ >5 else [$values$]
#Proposed method 2:
filtered = []
if $values$ <= 5: filtered += [$values$]

Example 4: Notice how examples 3, 4 use the more advanced functional programming which can be simplified using the proposed method and basic commands. Also, notice how in map, developer is required to break the flow of thought that they want to apply a function to a list of arguments, so now they have to treat f as an argument itself to the function map. But in proposed way, developer follows the usual pattern of thought of calling a function with its arguments in parenthesis in front of it.

#Current:
g = map(f, sequence)
#Proposed:
g = []
g.append(f($sequence$))

Example 5: Note that there is no need to copy the same iterable in the proposed way

#Current:
fibo = [1,1]
for i in range(2,10):
   fibo.append( fibo[i-1] + fibo[i-2] )
#Proposed:
fibo = [1,1]
rng = range(2,10)
fibo.append( fibo[$rng$-1] + fibo[$rng$-2] )

Example 6: Current method requires developer to decompose the variables into each component to get a clear print statement for debugging. But in the proposed way, simply, the parenthesis and arguments in front of fly can be copied and modified.

#Current:
print(f”Debug variable {‘speed’} is {speed}”)
print(f”Debug variable {‘altitude’} is {altitude}”)
print(f”Debug variable {‘angle’} is {angle}”)
fly(speed, altitude, angle)
#Proposed:
print(f”Debug variable {$(‘speed’, ‘altitude’, ‘angle’)$} is {$(speed, altitude, angle)$})
fly(speed, altitude, angle)

Example 7:

#Current method 1:
for color in colors:
    for size in sizes:
        print(color, size)
#Current method 2:
from itertools import product
for color,size in product(colors, sizes):
    print(color,size)
#Proposed:
print($colors$, 2$sizes$)

Example 8: Using inline assignments, shortens the expression.

#Current:
for i in range(rows):
    for j in range(cols):
        transposed[j][i] = matrix[i][j]
#Proposed:
transposed[j := $range(cols)$][i := $range(rows)$] = matrix[i][j]

Example 9: Code repeats for the entire block (this can be obvious if the for loop structures which we are abstracting away can be imagined.)
If there is nested blocks, we put the imaginary loops before the first one with $. Examples 9 and 10 clarify this.

#Current:
for student, grade in zip(students, grades):
    if grade > 50:
        print(f”{student} passed”)
#Proposed:
if $(grades) > 50:
    print(f”{$students$} passed”)

Example 10: Clarifying example

#Current:
for student, grade, age in zip(students, grades, ages):
    if grade > 50:
        if age > 5:
            print(f”{student} passed”)
#Proposed:
if $grades$ > 50:
    if $ages$ > 5:
        print(f”{$students$} passed”)

I think it’s safe to say this won’t happen. Not every pair of lines needs special syntax to reduce them to a single line.

12 Likes

How can you tell when the loop ends without being able to dedent? My reading of this is that if I wrote:

print("$things$")
print("$things$")

then it could become two for loops with one print statement each or one for loop with both print statements.

Perl anyone? i.e. Python is not Perl.

2 Likes

Exactly, each single statement has the for loop before it, and then it ends.
If we have used $ in an if block’s condition, then, it will be the entire if that will be under the loop (as in example 9 and 10)

In your case:

print($things$)
print($things$)

is equivalent to

for thing in things:
    print(thing)
for thing in things:
    print(thing)

Well if we’re limited to one line for loop bodies then I can almost always accomplish the same thing with a comprehension loop (at least all 10 of your examples can be written that way).

Technically we can have multiple lines.
For example:

from itertools import repeat
if $repeat(True)$: #using a dummy
   line 1
   line 2
   ...

I think the idea lies in the simplicity of using the same familiar methods for functional programming, comprehension, component-wise operation vectors, rather than relying on more advanced topics or libraries like that.

1 Like

Although I don’t think the proposed syntax is a good way to address it, my takeaway is that at the core of this is a simple need for some sort of vectorization conveniences.

numpy is a great tool for that. And numpy.vectorize can be used to achieve pretty much everything you desire.

Code example 1.

vprint = numpy.vectorize(print, cache=True)
vprint(names);

Code example 2.

vprint(firstnames, lastnames);

Code example 3.

vprint([[i] for i in items], sizes, colors)

For “Examples and features” current methods to me look better than proposed ones. Less verbose and more readable.



I think in the long run it might be beneficial to introduce some conveniences for vectorization in standard library.

But for more basic things it is easy to implement needed conveniences, while anything more complex is a lot of work and maintenance. Not sure what the happy middle could be


1 Like

If you don’t mind using map for side effect, example 1 and 2 can be done with:

any(map(print, names))
any(map(print, firstnames, lastnames))

Example 3 is best kept as a nested loop as your proposal with numeric nest levels is hardly readable, and so is trying to use itertools.product and chain to make it happen.

1 Like

Sorry @humblebumblebee, I don’t see it as a simplification. I also have a strong dislike of using $ near names as that is a major Perl-ism. Perl itself is a great language in its own right, but I dread the nightmare of debugging others Perl and your suggestion triggers me.

2 Likes
list(map(print, names))

Works as a one liner. There is no benefit in adding obscure symbols to a language that prides itself on readability.

5 Likes

This is nearly as bad, and I don’t consider this an argument against adding such syntax, or a substitute for the original for loop.

If you’re keen to write programs this sort of way, you might want to look at array languages, e.g. APL or J.

The way this problem is addressed by this solution worries me. I believe it is a symptom of the (in my opinion) missteps the python language has taken in its recent updates.

Python should be simple to read and understand. The barrier for entry should be minimal, and anyone familiar with core principals of programming should be able to navigate a codebase.

While the pattern that this idea highlights is something to look at, the proposed solution is worrisome.

As a language, python should be extremely hesitant to add any more syntax bloat. The recent changes to the language involving type annotations and generics have already introduced additional complexity. As a community, we should be extremely skeptical of any suggestion which is proposing to introduce syntax for the sake of convenience. I do not believe adding a new symbol to the language to “simplify” code structures does anyone any favors.
Explicit is better than implicit.

The convenience is not worth the mental burden imposed on developers who have to read the code.

I don’t think it is very readable. This is more readable:

from typing import Callable

names = ["bob", "fred"]

def execute_for_items(func: Callable, items: list):
    res = []
    for item in items:
        res.append(func(item))
    return res

execute_for_items(print, names)

Can you explain how Annotations and generics have made code more complicated since they reduce repetition?

I don’t think it is fair to say that Annotations and generics are the same as $ syntax, which to me looks like perl or PHP and I’d rather not go there.

For example, with pydantic and annotations you can make sure fields are of a certain length quite easily:

StrRequired = Annotated[str, MinLen(1)]

class MyClass(BaseModel):
   my_field: StrRequired = Field(..., alias="myField")

I don’t see anything complicated about that?

This execute_for_items function already exists, it’s called map. The only thing it does differently is that it takes any iterable, and returns an iterable, so to see the results, you need to iterate over it (e.g., by calling list).

names = ["bob", "fred"]
list(map(print, names))
1 Like

It does, but the point is the illustration.

Not really on topic. I’m talking about things like

class Foo[X]():
    ...