pipetools
pipetools copied to clipboard
foreach_i
I'd like to propose a new util: foreach_i.
Motivation
You know how JS's Array.map also passes the element index as a 2nd parameter to the function?
> ['a', 'b', 'c'].map((x, i) => `Element ${i} is ${x}`)
[ 'Element 0 is a', 'Element 1 is b', 'Element 2 is c' ]
That's exactly what I'm trying to do here. The only difference is that the index would be passed as the 1st param:
def test_foreach_i():
r = ['a', 'b', 'c'] > (pipe
| foreach_i(lambda i, x: f'Element {i} is {x}')
| list
)
assert r == [ 'Element 0 is a'
, 'Element 1 is b'
, 'Element 2 is c'
]
(Naïve) Implementation
from pipetools.utils import foreach, as_args
from typing import Callable, TypeVar
A = TypeVar('A')
B = TypeVar('B')
def foreach_i(f: Callable[[int, A], B]):
return enumerate | foreach(as_args(f))
The same could be done for foreach_do.
Hey!
there are a few issues with this I'd like to think about first.
- Giving the index as a separate argument forces you to use a
lambdaexpression. Whereas using a single argument tuple let's you use auto string formatting or destructuring:
In [2]: [1,2,3] > enumerate | foreach("Element {0} is {1}") | list
Out[2]: ['Element 0 is 1', 'Element 1 is 2', 'Element 2 is 3']
In [3]: [1,2,3] > enumerate | foreach({X[0]: X[1] * 2}) | list
Out[3]: [{0: 2}, {1: 4}, {2: 6}]
-
The ordering of index and element is not clear, because JavaScript and C# have it reversed - but in Python we should stick to what
enumeratedoes. But just looking atforeach_iit might not be clear what the ordering is. -
I'm not loving the name, but can't really think of anything obvious to call it.
All in all not sure if it's worth it adding this one, since it will only save you adding enumerate in your pipe - which in turn makes it obvious to the reader what's going on and what shape the argument is going to be in.
Hi, @0101! :wave: Thank you for your reply.
it will only save you adding
enumeratein your pipe
Not really: enumerate and as_args.
At lease in my use case.
This:
| enumerate
| foreach(as_args(func))
is equivalent to:
| foreach_i(func)
I'm not loving the name
foreach_index, maybe? 🤔
Not really: enumerate and as_args.
At lease in my use case.
Which brings us back to point 1. 🙂 It would mean adding a utility function that does not behave like the other ones.
@tfga do you maybe have some more specific examples of how you'd use it?
Not really.
Every once in a while I find myself in a situation where I want to do a "foreach with index". The particular piece of code that prompted me to write this issue was:
# (i, td) => (td, colWidth[i])
def pairWithWidth(i, td): return td, colWidth[i]
tds = tr > (pipe
| enumerate
| foreach(as_args(pairWithWidth))
| foreach(as_args(addPadding))
| ' '.join
)
With foreach_i, it would become:
# (i, td) => (td, colWidth[i])
def pairWithWidth(i, td): return td, colWidth[i]
tds = tr > (pipe
| foreach_i(pairWithWidth)
| foreach(as_args(addPadding))
| ' '.join
)
I see. Technically, you can instead do:
tds = tr > enumerate | foreach((X[1], X[0] | colWidth.__getitem__))
But it's quite unreadable.
I think what we could do is detect if a two-parameter function is being passed in and then pass two separate parameters - and otherwise pass in the tuple so things like foreach_i("Element {0} is {1}") still work as expected.