Skip to content

Commit 76dc4e8

Browse files
committed
refactor; add map_longest, mapr_longest, zipr_longest
1 parent b562625 commit 76dc4e8

File tree

1 file changed

+97
-38
lines changed

1 file changed

+97
-38
lines changed

unpythonic/it.py

+97-38
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,70 @@
1515
http://rightfootin.blogspot.fi/2006/09/more-on-python-flatten.html
1616
"""
1717

18-
__all__ = ["scanl", "scanr", "scanl1", "scanr1",
18+
__all__ = ["make_heads",
19+
"scanl", "scanr", "scanl1", "scanr1",
1920
"foldl", "foldr", "reducel", "reducer",
20-
"flatmap", "mapr", "zipr", "uniqify", "uniq",
21+
"mapr", "zipr", "map_longest", "mapr_longest", "zipr_longest",
22+
"flatmap", "uniqify", "uniq",
2123
"take", "drop", "split_at", "unpack",
2224
"tail", "first", "second", "nth", "last",
2325
"flatten", "flatten1", "flatten_in",
2426
"iterate", "iterate1"]
2527

26-
from itertools import tee, islice
28+
from functools import partial
29+
from itertools import tee, islice, zip_longest
2730
from collections import deque
2831
from inspect import isgenerator
2932

33+
def make_heads(longest=False, fillvalue=None):
34+
"""Create a function returning the head elements from any number of iterators.
35+
36+
The return value is a function, ``heads``:
37+
38+
``heads(*iterators)`` -> ``tuple`` or ``StopIteration``.
39+
40+
When the resulting function is called on some iterators, it returns the head
41+
element from each, packed into a tuple. When elements run out, it **returns**
42+
(does **not** raise!) ``StopIteration``.
43+
44+
The parameter ``longest`` controls when we consider elements to run out:
45+
46+
- If ``longest=False``, iteration stops when the shortest input runs out.
47+
48+
- If ``longest=True``, iteration stops only when all inputs have run out.
49+
Any missing elements (after the ends of shorter inputs) are filled with
50+
the optional ``fillvalue``, which defaults to ``None``.
51+
52+
This function is a low-level utility for defining higher-order functions
53+
that may take multiple iterables, and terminate either on the shortest
54+
or on the longest input.
55+
"""
56+
if longest:
57+
def heads(iterators):
58+
hs = []
59+
nempty = 0
60+
for it in iterators:
61+
try:
62+
h = next(it)
63+
except StopIteration: # this sequence has run out
64+
h = fillvalue
65+
nempty += 1 # may legitimately contain None so must count
66+
hs.append(h)
67+
if nempty == len(iterators):
68+
return StopIteration
69+
return tuple(hs)
70+
else:
71+
def heads(iterators):
72+
hs = []
73+
for it in iterators:
74+
try:
75+
h = next(it)
76+
except StopIteration: # shortest sequence ran out
77+
return StopIteration
78+
hs.append(h)
79+
return tuple(hs)
80+
return heads
81+
3082
# require at least one iterable to make this work seamlessly with curry.
3183
def scanl(proc, init, iterable0, *iterables, longest=False, fillvalue=None):
3284
"""Scan (accumulate), optionally with multiple input iterables.
@@ -51,31 +103,8 @@ def scanl(proc, init, iterable0, *iterables, longest=False, fillvalue=None):
51103
yield proc(*elts, acc) # if this was legal syntax
52104
"""
53105
iterables = (iterable0,) + iterables
54-
if not longest: # terminate on shortest input
55-
def heads(its):
56-
hs = []
57-
for it in its:
58-
try:
59-
h = next(it)
60-
except StopIteration: # shortest sequence ran out
61-
return StopIteration
62-
hs.append(h)
63-
return tuple(hs)
64-
else: # terminate on longest input
65-
def heads(its):
66-
hs = []
67-
nempty = 0
68-
for it in its:
69-
try:
70-
h = next(it)
71-
except StopIteration: # this sequence has run out
72-
h = fillvalue
73-
nempty += 1 # may legitimately contain None so must count
74-
hs.append(h)
75-
if nempty == len(its):
76-
return StopIteration
77-
return tuple(hs)
78106
iters = tuple(iter(x) for x in iterables)
107+
heads = make_heads(longest=longest, fillvalue=fillvalue)
79108
acc = init
80109
while True:
81110
yield acc
@@ -151,6 +180,36 @@ def reducer(proc, sequence, init=None):
151180
"""
152181
return reducel(proc, reversed(sequence), init)
153182

183+
def mapr(func, *sequences):
184+
"""Like map, but walk each sequence from the right."""
185+
return map(func, *(reversed(s) for s in sequences))
186+
187+
def zipr(*sequences):
188+
"""Like zip, but walk each sequence from the right."""
189+
return zip(*(reversed(s) for s in sequences))
190+
191+
def map_longest(func, *iterables, fillvalue=None):
192+
"""Like map, but terminate on the longest input.
193+
194+
In the input to ``func``, missing elements (after end of shorter inputs)
195+
are replaced by ``fillvalue``, which defaults to ``None``.
196+
"""
197+
iters = tuple(iter(x) for x in iterables)
198+
heads = make_heads(longest=True)
199+
while True:
200+
hs = heads(iters)
201+
if hs is StopIteration:
202+
break
203+
yield func(*hs)
204+
205+
def mapr_longest(func, *sequences, fillvalue=None):
206+
"""Like map_longest, but walk each sequence from the right."""
207+
return map_longest(func, *(reversed(s) for s in sequences), fillvalue=fillvalue)
208+
209+
def zipr_longest(*sequences, fillvalue=None):
210+
"""Like itertools.zip_longest, but walk each sequence from the right."""
211+
return zip_longest(*(reversed(s) for s in sequences), fillvalue=fillvalue)
212+
154213
def flatmap(f, iterable0, *iterables):
155214
"""Map, then concatenate results.
156215
@@ -190,14 +249,6 @@ def sum_and_diff(a, b):
190249
for x in xs:
191250
yield x
192251

193-
def mapr(func, *sequences):
194-
"""Like map, but walk each sequence from the right."""
195-
return map(func, *(reversed(s) for s in sequences))
196-
197-
def zipr(*sequences):
198-
"""Like zip, but walk each sequence from the right."""
199-
return zip(*(reversed(s) for s in sequences))
200-
201252
def uniqify(iterable, key=None):
202253
"""Skip duplicates in iterable.
203254
@@ -480,7 +531,6 @@ def iterate(f, *args):
480531

481532
def test():
482533
from operator import add, mul, itemgetter
483-
from functools import partial
484534
from unpythonic.fun import curry, composer, composerc, composel, to1st, rotate, identity
485535
from unpythonic.llist import cons, nil, ll, lreverse
486536

@@ -573,6 +623,17 @@ def noneadd(a, b):
573623
return a + b
574624
assert curry(mymap_longest, noneadd, ll(1, 2, 3), ll(2, 4)) == ll(3, 6, None)
575625

626+
# Adding the missing batteries to the algebra of map and zip.
627+
# Note Python's (and Racket's) map is like Haskell's zipWith, but for n inputs.
628+
assert tuple(map(add, (1, 2), (3, 4))) == (4, 6) # builtin
629+
assert tuple(mapr(add, (1, 2), (3, 4))) == (6, 4)
630+
assert tuple(zip((1, 2, 3), (4, 5, 6), (7, 8))) == ((1, 4, 7), (2, 5, 8)) # builtin
631+
assert tuple(zipr((1, 2, 3), (4, 5, 6), (7, 8))) == ((3, 6, 8), (2, 5, 7))
632+
assert tuple(map_longest(noneadd, (1, 2, 3), (2, 4))) == (3, 6, None)
633+
assert tuple(mapr_longest(noneadd, (1, 2, 3), (2, 4))) == (7, 4, None)
634+
assert tuple(zip_longest((1, 2, 3), (2, 4))) == ((1, 2), (2, 4), (3, None)) # itertools
635+
assert tuple(zipr_longest((1, 2, 3), (2, 4))) == ((3, 4), (2, 2), (1, None))
636+
576637
reverse_one = curry(foldl, cons, nil)
577638
assert reverse_one(ll(1, 2, 3)) == ll(3, 2, 1)
578639

@@ -635,8 +696,6 @@ def sum_and_diff(a, b):
635696
assert a == tuple(range(3))
636697
assert b == ()
637698

638-
assert tuple(zipr((1, 2, 3), (4, 5, 6), (7, 8))) == ((3, 6, 8), (2, 5, 7))
639-
640699
@rotate(1)
641700
def zipper(acc, *rest): # so that we can use the *args syntax to declare this
642701
return acc + (rest,) # even though the input is (e1, ..., en, acc).

0 commit comments

Comments
 (0)