1

Python's list-comprehensions are incredibly useful, since they allow us to write common patterns into the simple, concise, very readable one-liners:

loop with filter loop with break
l = []
for x in xs:
if g(x):
l.append(f(x))
l = []
for x in xs:
if not g(x):
break
l.append(f(x))
[f(x) for x in xs if g(x)] [f(x) for x in takewhile(g, x)]

Consider the slightly more general patterns of allowing the filter/break to depend on all previously generated elements:

loop with history-dependent filter loop with history-dependent break
l = []
for x in xs:
if g(x, l):
l.append(f(x))
l = []
for x in xs:
if not g(x, l):
break
l.append(f(x))

Question: Is it at all possible to translate these patterns into list-comprehensions as well?

To me, it seems the answer is no / it is only doable for very special cases of g (like [1]), because in one would need to be able to reference the list as it being created.




End of the question // Some incoherent rambling:

It seems that one would need a completely new syntax along the lines of

[f(x) for x in xs if g(x, this)]

Where this is a symbolic placeholder for a new keyword that would one to reference the outer object from within a context. I have no idea if this is even in principle possible at all, but it would be kinda cool to be able to write

loop with history-dependent filter loop with history-dependent break
l = []
for x in xs:
if g(x, l):
l.append(f(x))
l = []
for x in xs:
if not g(x, l):
break
l.append(f(x))
[f(x) for x in xs if g(x, this)] [f(x) for x in xs while g(x, this)]

Or the even more general

loop with history-dependent filter and yield loop with history-dependent break and yield
l = []
for x in xs:
if g(x, l):
l.append(f(x, l))
l = []
for x in xs:
if not g(x, l):
break
l.append(f(x, l))
[f(x, this) for x in xs if g(x, this)] [f(x, this) for x in xs while g(x, this)]

Where both the next element and the exit condition of the list comprehension are allowed to depend on all previously generated elements. In set-builder notation, the two constructs would translate to

S⁽ⁿ⁺¹⁾ = { f(xₖ, S⁽ᵏ⁾) ∣ xₖ∈{x₁,…,xₙ} ∧ g(xₖ, S⁽ᵏ⁾) } 
       = S⁽ⁿ⁾ ∪ {f(xₙ, S⁽ⁿ⁾) ∣ g(xₙ, S⁽ⁿ⁾)}

and

S⁽ⁿ⁺¹⁾ = { f(xₖ, S⁽ᵏ⁾) ∣ xₖ∈{x₁,…,xₙ} ∧ ∀l≤k g(xₗ, S⁽ˡ⁾) } 
       = S⁽ⁿ⁾ ∪ {f(xₙ, S⁽ⁿ⁾) ∣ ∀k≤n g(xₖ, S⁽ᵏ⁾)}

Are there any languages out there that have a feature like this?

Hyperplane
  • 1,422
  • 1
  • 14
  • 28
  • 2
    @AlexWaygood There is no fundamental barrier to the semantics of `this` proposed in the question - the list being comprehended *does* exist while the comprehension is executed, because ultimately a list comprehension is executed by creating an empty list, running a loop which (possibly checks a condition and) computes some result that is appended the list, then returning the list. So the list does exist while the condition is checked and the result is computed, and it would not be impossible for a language to allow the condition and/or result to access a reference to it. – kaya3 Jul 26 '21 at 14:45
  • Thanks! I wasn't aware that was how comprehensions were implemented. Forgive my ignorance. I still think it's unlikely (and probably not desirable?) to ever be implemented in Python (which is a different point, I know). A little too magical for my tastes. – Alex Waygood Jul 26 '21 at 14:52
  • 3
    @kaya3 You can also try to actually [find the list](https://stackoverflow.com/a/65228120/12671057) and use it. – Kelly Bundy Aug 01 '21 at 00:22

1 Answers1

2

It's possible to translate them into list comprehensions by making the functions f and/or g stateful (typically you would want to encapsulate the state in an instance of some class). For example, here's a list comprehension which greedily builds an ascending subsequence of the original list:

class State:
    def __init__(self):
        self.x = None
    def is_ascending(self, x):
        if self.x is None or x > self.x:
            self.x = x
            return True
        else:
            return False

nums = [1, 2, 1, 3, 1, 4, 1, 5, 1, 6]
state = State()

print([x for x in nums if state.is_ascending(x)])
# [1, 2, 3, 4, 5, 6]

In the worst case, the state stores all of the elements added so far, which is redundant compared to the for loop, but nonetheless the answer is that you can write it as a list comprehension.


As an alternative, you can do something like this:

result = []
result.extend(len(result) for i in range(10) if 5 not in result)
print(result)
# [0, 1, 2, 3, 4, 5]

Note that extend has to be called with a lazily-evaluated generator expression, so that each element it generates can be added to result (and so result will have the right state before the next element gets generated). I don't think this is a good way of writing code (and it seems to depend on undocumented behaviour of extend), but it does achieve what you are trying to do.

kaya3
  • 47,440
  • 4
  • 68
  • 97