Skip to content

Iterator#

The apfel.core.iter module.

The abstraction for an iterator, alternative to Python's vanilla built-in iterator ABC collections.abc.Iterator. It provides a large number of methods that are commonly found in Rust Iterator and Python itertools.

To create or use this abstraction of iterator:

  • Use itrt to wrap any iterable or Python vanilla iterator,
  • Use Iterator methods directly on any Python vanilla iterator.

Iterator and itrt are are exposed in the package namespace.

Implementation#

Iterators have a large number of methods. Missing methods will be added gradually over time.

Iterator
Reference Iterator Counterpart
advance_by
all
any
array_chunks
by_ref
chain
cloned
cmp
cmp_by
collect
collect_into
copied
count
cycle
enumerate
eq
eq_by
filter
filter_map
find
find_map
flat_map
flatten
fold
for_each
fuse
ge
gt
inspect tap
intersperse
intersperse_with
is_partitioned
is_sorted
is_sorted_by
is_sorted_by_key
last
le
lt
map
map_while
map_windows
max
max_by
max_by_key
min
min_by
min_by_key
ne
next
next_chunk
nth
partial_cmp
partial_cmp_by
partition
partition_in_place
peekable
position
product pipe(math.product)
reduce
rev
rposition
scan
size_hint
skip
skip_while
step_by
sum pipe(sum)
take
take_while
try_collect
try_find
try_fold
try_for_each
try_reduce
unzip
zip
itertools

This table tracks named Iterator counterparts in itertools. Standalone itertools functions can still be used through pipe when their first argument is an iterable.

Reference itertools Counterpart
accumulate accumulate
batched
chain
chain.from_iterable flatten
compress
count
cycle
dropwhile skip_while
filterfalse filter
groupby
islice take / skip / step_by
pairwise
repeat
starmap map
takewhile take_while
tee
zip_longest
product
permutations
combinations
combinations_with_replacement

itrt(iterable) #

itrt(iterable: Iterator[I]) -> Iterator[I]
itrt(iterable: Iterable[I]) -> Iterator[I]
itrt(iterable: VanillaIterator[I]) -> Iterator[I]

Create an Iterator from a standard Python Iterator or Iterable.

Parameters:

Name Type Description Default
iterable

A Python standard collections.abc.Iterator or collections.abc.Iterable.

required

Returns:

Type Description

An iterator that guarantees the apfel Iterator interface.

Raises:

Type Description
TypeError

If the input is neither an Iterator nor an Iterable.

Iterator #

class Iterator[I](ABC):
    __next__

The interface for an iterator. See module level documentation for more information.

__next__() abstractmethod #

Return the next item of the iterator. If the iterator is exhausted, raise StopIteration.

Any implementor of collections.abc.Iterator should be directly compatible with this interface.

iterator = itrt([1, 2, 3])
assert next(iterator) == 1
assert next(iterator) == 2
assert next(iterator) == 3

try:
    next(iterator)
except StopIteration:
    # Iterator is exhausted
    pass

accumulate(state, func) #

accumulate(state: T, func: Callable[[T, I], T]) -> Iterator[T]
accumulate(state: T, func: Callable[[T, Item], T]) -> Iterator[T]

Creates a new iterator that yields the accumulated state after applying func to each element, starting with state. Unlike fold, this yields each intermediate state rather than consuming the iterator.

Compared to itertools.accumulate, this method enforces an explicit initial state and binary function; it also does not yield the initial state before consuming any element. This is also consistent with the numpy.ufunc.accumulate behavior. Also check Iterator.scan for a method that provides more generalized state control.

iterator = itrt([1, 2, 3, 4])
result = iterator.accumulate(0, lambda acc, x: acc + x)
assert result.next().unwrap() == 1
assert result.next().unwrap() == 3
assert result.next().unwrap() == 6
assert result.next().unwrap() == 10
assert result.next().is_nothing()

advance_by(n) #

advance_by(n: int) -> Result[None, int]
advance_by(n: int) -> Result[None, int]

Advance the iterator by n steps. If the iterator is exhausted before advancing n steps, return an Err containing the number of remaining steps. Otherwise, return Ok(()).

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.advance_by(2).is_ok()
assert iterator.next().unwrap() == 3
assert iterator.advance_by(6).unwrap_err() == 4

all(pred) #

all(pred: Callable[[I], bool]) -> bool
all(pred: Callable[[Item], bool]) -> bool

Returns True if all elements of the iterator satisfy the predicate f. Otherwise, returns False.

The iteration short-circuits if an element is found that does not satisfy the predicate. An empty iterator returns True.

iterator = itrt([2, 4, 6])
assert iterator.all(lambda x: x % 2 == 0)
assert iterator.next().is_nothing()

iterator = itrt([2, 3, 6])
assert not iterator.all(lambda x: x % 2 == 0)
assert iterator.next().unwrap() == 6

any(pred) #

any(pred: Callable[[I], bool]) -> bool
any(pred: Callable[[Item], bool]) -> bool

Returns True if any element of the iterator satisfies the predicate f. Otherwise, returns False.

The iteration short-circuits if an element is found that satisfies the predicate. An empty iterator returns False.

iterator = itrt([1, 3, 5])
assert not iterator.any(lambda x: x % 2 == 0)
assert iterator.next().is_nothing()

iterator = itrt([1, 2, 3])
assert iterator.any(lambda x: x % 2 == 0)
assert iterator.next().unwrap() == 3

chain(*others) #

chain(*others: Iterator[I]) -> Iterator[I]
chain(*others: VanillaIterator[Item]) -> Iterator[Item]

Creates a new iterator that yields elements from this iterator until it is exhausted, then yields elements from the other iterator.

iterator1 = itrt([1, 2])
iterator2 = itrt([3, 4])
iterator3 = itrt([5, 6])
chained_iterator = iterator1.chain(iterator2, iterator3)

assert chained_iterator.next().unwrap() == 1
assert chained_iterator.next().unwrap() == 2
assert iterator1.next().is_nothing()  # original iterator1 is also exhausted
assert chained_iterator.next().unwrap() == 3
assert chained_iterator.next().unwrap() == 4
assert iterator2.next().is_nothing()  # original iterator2 is also exhausted
assert chained_iterator.next().unwrap() == 5
assert chained_iterator.next().unwrap() == 6
assert chained_iterator.next().is_nothing()

count() #

count() -> int
count() -> int

Counts the number of elements in the iterator, until it is exhausted.

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.count() == 5

enumerate(init=0) #

enumerate(init: int = 0) -> Iterator[tuple[int, I]]
enumerate(init: int = 0) -> Iterator[tuple[int, Item]]

Creates a new iterator that yields tuples of (index, element) pairs, where index starts from 0 or the specified init value.

Parameters:

Name Type Description Default
init int

The starting index for enumeration. Default is 0.

0
iterator = itrt(['a', 'b', 'c'])
enumerated_iterator = iterator.enumerate()
assert enumerated_iterator.next().unwrap() == (0, 'a')
assert enumerated_iterator.next().unwrap() == (1, 'b')
assert enumerated_iterator.next().unwrap() == (2, 'c')
assert enumerated_iterator.next().is_nothing()

eq(other) #

eq(other: Iterator[I]) -> bool
eq(other: VanillaIterator[I]) -> bool
eq(other: VanillaIterator[Item]) -> bool

Checks if two iterators are equal by comparing their elements pairwise.

iterator1 = itrt([1, 2, 3])
iterator2 = itrt([1, 2, 3])
assert iterator1.eq(iterator2)

iterator1 = itrt([1, 2, 3])
iterator3 = itrt([1, 2, 4])
assert not iterator1.eq(iterator3)

iterator1 = itrt([1, 2, 3])
iterator4 = itrt([1, 2])
assert not iterator1.eq(iterator4)

filter(pred) #

filter(pred: Callable[[I], bool]) -> Iterator[I]
filter(pred: Callable[[Item], bool]) -> Iterator[Item]

Creates a new iterator that yields only the elements of the original iterator that satisfy the predicate pred.

iterator = itrt([1, 2, 3, 4, 5])
filtered_iterator = iterator.filter(lambda x: x % 2 == 0)
assert filtered_iterator.next().unwrap() == 2
assert filtered_iterator.next().unwrap() == 4
assert filtered_iterator.next().is_nothing()
assert iterator.next().is_nothing()  # original iterator is also exhausted

filter_map(pred) #

filter_map(pred: Callable[[I], Maybe[U]]) -> Iterator[U]
filter_map(pred: Callable[[Item], Maybe[U]]) -> Iterator[U]

Creates a new iterator that applies pred to each element and yields the unwrapped value for each result that is a Just, skipping Nothing.

from apfel.container.maybe import Maybe

def try_parse(s):
    try:
        return Maybe.make_just(int(s))
    except ValueError:
        return Maybe.make_nothing()

iterator = itrt(["1", "two", "3", "four"])
filtered = iterator.filter_map(try_parse)
assert filtered.next().unwrap() == 1
assert filtered.next().unwrap() == 3
assert filtered.next().is_nothing()

find(pred) #

find(pred: Callable[[I], bool]) -> Maybe[I]
find(pred: Callable[[Item], bool]) -> Maybe[Item]

Returns the first element in the iterator that satisfies the predicate f, wrapped in a Maybe. If no such element is found, returns Nothing.

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.find(lambda x: x % 2 == 0).unwrap() == 2
assert iterator.find(lambda x: x <= 3).unwrap() == 3  # 1, 2 have been consumed
assert iterator.find(lambda x: x > 10).is_nothing()  # no such element exists
assert iterator.next().is_nothing()  # iterator is exhausted

find_map(pred) #

find_map(pred: Callable[[I], Maybe[U]]) -> Maybe[U]
find_map(pred: Callable[[Item], Maybe[U]]) -> Maybe[U]

Applies the function pred to each element of the iterator and returns the first result that is a Just, unwrapped. If no element produces a Just, returns Nothing.

from apfel import Maybe

iterator = itrt(["lol", "NaN", "2", "5"])
def try_parse(s):
    try:
        return Maybe.make_just(int(s))
    except ValueError:
        return Maybe.make_nothing()

assert iterator.find_map(try_parse).unwrap() == 2

flat_map(func) #

flat_map(func: Callable[[I], Iterable[U]]) -> Iterator[U]
flat_map(func: Callable[[Item], Iterable[U]]) -> Iterator[U]

Creates a new iterator that applies func to each element and flattens the results. Equivalent to .map(func).flatten().

iterator = itrt([1, 2, 3])
result = iterator.flat_map(lambda x: itrt([x, x * 10]))
assert result.next().unwrap() == 1
assert result.next().unwrap() == 10
assert result.next().unwrap() == 2
assert result.next().unwrap() == 20
assert result.next().unwrap() == 3
assert result.next().unwrap() == 30
assert result.next().is_nothing()

flatten() #

flatten() -> Iterator[Item]
flatten() -> Iterator[Item]
flatten() -> Iterator[Item]

Flattens an iterable of iterators into a single iterator by yielding all elements from each inner iterator in sequence.

iterator = itrt([itrt([1, 2]), itrt([3, 4]), itrt([5])])
flattened_iterator = iterator.flatten()
assert flattened_iterator.next().unwrap() == 1
assert flattened_iterator.next().unwrap() == 2
assert flattened_iterator.next().unwrap() == 3
assert flattened_iterator.next().unwrap() == 4
assert flattened_iterator.next().unwrap() == 5
assert flattened_iterator.next().is_nothing()
assert iterator.next().is_nothing()  # original iterator is also exhausted

fold(init, func) #

fold(init: T, func: Callable[[T, I], T]) -> T
fold(init: T, func: Callable[[T, Item], T]) -> T

Fold (reduce) the elements of the iterator using the accumulation function func, starting with the initial value init.

See also reduce if the first element of the iterator should be used as the initial accumulator value.

Tip

The default implementation of this method uses functools.reduce under the hood, but allows keyword arguments for both init and func.

iterator = itrt([1, 2, 3, 4, 5])
result = iterator.fold(0, lambda acc, x: acc + x)
assert result == 15  # 0 + 1 + 2 + 3 + 4 + 5

iterator = itrt([])
result = iterator.fold(10, lambda acc, x: acc + x)
assert result == 10  # initial value only

for_each(func) #

for_each(func: Callable[[I], Any]) -> None
for_each(func: Callable[[Item], Any]) -> None

Applies the function func to each element of the iterator. Compare to map, the result of each function application is discarded, and the iterator is consumed eagerly.

iterator = itrt([1, 2, 3])
iterator.for_each(lambda x: print(x, end=" "))
# Output: 1 2 3

hint(hint) #

Hints the type of items in the iterator for better type inference. This method does not affect runtime behavior.

iterator = itrt([1, 2, 3]).hint(int)
assert iterator.next().unwrap() == 1

intersperse(separator) #

intersperse(separator: I) -> Iterator[I]
intersperse(separator: Item) -> Iterator[Item]

Creates a new iterator that places separator between adjacent elements.

iterator = itrt([1, 2, 3])
result = iterator.intersperse(0)
assert result.next().unwrap() == 1
assert result.next().unwrap() == 0
assert result.next().unwrap() == 2
assert result.next().unwrap() == 0
assert result.next().unwrap() == 3
assert result.next().is_nothing()

assert itrt([]).intersperse(0).next().is_nothing()
assert itrt([1]).intersperse(0).next().unwrap() == 1

intersperse_with(sep_fn) #

intersperse_with(sep_fn: Callable[[], I]) -> Iterator[I]
intersperse_with(sep_fn: Callable[[], Item]) -> Iterator[Item]

Creates a new iterator that places the value returned by sep_fn between adjacent elements. sep_fn is called once for each separator inserted.

iterator = itrt([1, 2, 3])
result = iterator.intersperse_with(lambda: 0)
assert result.next().unwrap() == 1
assert result.next().unwrap() == 0
assert result.next().unwrap() == 2
assert result.next().unwrap() == 0
assert result.next().unwrap() == 3
assert result.next().is_nothing()

n = 0
def counter():
    nonlocal n
    n += 1
    return n
result = itrt(['a', 'b', 'c']).intersperse_with(counter)
assert list(result) == ['a', 1, 'b', 2, 'c']

last() #

last() -> Maybe[I]
last() -> Maybe[Item]

Returns the last element of the iterator, wrapped in a Maybe. If the iterator is empty, returns Nothing.

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.last().unwrap() == 5
assert iterator.next().is_nothing()  # iterator is exhausted

iterator = itrt([])
assert iterator.last().is_nothing()

map(func) #

map(func: Callable[[I], U]) -> Iterator[U]
map(func: Callable[[Item], U]) -> Iterator[U]

Creates a new iterator that applies the function func to each element of the original iterator.

iterator = itrt([1, 2, 3])
mapped_iterator = iterator.map(lambda x: x * 2)
assert mapped_iterator.next().unwrap() == 2
assert mapped_iterator.next().unwrap() == 4
assert mapped_iterator.next().unwrap() == 6
assert mapped_iterator.next().is_nothing()
assert iterator.next().is_nothing()  # original iterator is also exhausted

map_while(pred) #

map_while(pred: Callable[[I], Maybe[U]]) -> Iterator[U]
map_while(pred: Callable[[Item], Maybe[U]]) -> Iterator[U]

Creates a new iterator that applies pred to each element and yields the unwrapped value while the result is a Just. Once pred returns Nothing, iteration stops immediately.

from apfel.container.maybe import just, nothing

def checked_double(x):
    if x < 4:
        return just(x * 2)
    return nothing()

iterator = itrt([1, 2, 3, 4, 5])
result = iterator.map_while(checked_double)
assert result.next().unwrap() == 2
assert result.next().unwrap() == 4
assert result.next().unwrap() == 6
assert result.next().is_nothing()

next() #

next() -> Maybe[I]
next() -> Maybe[Item]

Return the next item of the iterator, wrapped in a Maybe.

iterator = itrt([1, 2, 3])
assert iterator.next().unwrap() == 1
assert iterator.next().unwrap() == 2
assert iterator.next().unwrap() == 3
assert iterator.next().is_nothing()

nth(n) #

nth(n: int) -> Maybe[I]
nth(n: int) -> Maybe[Item]

Returns the n-th element of the iterator (0-indexed), wrapped in a Maybe. If the iterator has fewer than n + 1 elements, returns Nothing.

Note

This method consumes the first n + 1 elements of the iterator.

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.nth(2).unwrap() == 3  # gets index 2 (third element)
assert iterator.next().unwrap() == 4  # continues from after nth element

iterator = itrt([1, 2])
assert iterator.nth(5).is_nothing()  # not enough elements

pipe(func, *args, **kwargs) #

pipe(func: Callable[Concatenate[Iterable[I], P], T], /, *args: P.args, **kwargs: P.kwargs) -> T
pipe(func: Callable[Concatenate[Iterable[Item], P], T], /, *args: P.args, **kwargs: P.kwargs) -> T

Pipes the iterator into the function func, passing any additional positional and keyword arguments. This allows for chaining operations in a functional style.

iterator = itrt([1, 2, 3, 4, 5])
result = iterator.pipe(sum, start=10)
assert result == 25  # 10 + 1 + 2 + 3 + 4 + 5

iterator = itrt([1, 2, 3])
result = iterator.pipe(max)
assert result == 3

iterator = itrt([1, 2, 3, 4])
result = iterator.map(str).pipe("-".join)
assert result == "1-2-3-4"

position(pred) #

position(pred: Callable[[I], bool]) -> Maybe[int]
position(pred: Callable[[Item], bool]) -> Maybe[int]

Returns the index of the first element in the iterator that satisfies the predicate pred, wrapped in a Maybe. If no such element is found, returns Nothing.

iterator = itrt([1, 2, 3, 4, 5])
assert iterator.position(lambda x: x % 2 == 0).unwrap() == 1
assert iterator.next().unwrap() == 3  # 1, 2 have been consumed

iterator = itrt([1, 2, 3])
assert iterator.position(lambda x: x > 10).is_nothing()
assert iterator.next().is_nothing()  # iterator is exhausted

reduce(func) #

reduce(func: Callable[[I, I], I]) -> Maybe[I]
reduce(func: Callable[[Item, Item], Item]) -> Maybe[Item]

Reduce the elements of the iterator using the accumulation function func and returns a Maybe. The first element of the iterator is used as the initial accumulator value. If the iterator is empty, returns Nothing.

See also fold if an explicit initial value is needed.

iterator = itrt([1, 2, 3, 4, 5])
result = iterator.reduce(lambda acc, x: acc + x)
assert result.unwrap() == 15  # 1 + 2 + 3 + 4 + 5

iterator = itrt([])
result = iterator.reduce(lambda acc, x: acc + x)
assert result.is_nothing()  # no elements to reduce

scan(state, func) #

scan(state: St, func: Callable[[St, I], Maybe[B]]) -> Iterator[B]
scan(state: St, func: Callable[[St, Item], Maybe[B]]) -> Iterator[B]

Creates a new iterator that holds internal state, applying func to each element. func receives state and an element, and returns a Maybe. Yields the unwrapped value while func returns Just; stops on Nothing.

state is passed directly to func each iteration — pass a mutable container such as Value if func needs to update it across iterations.

from apfel.container.maybe import just, nothing
from apfel.container.value import Value
from apfel.core.common import imperative

state = Value(1)
iterator = itrt([1, 2, 3, 4])
result = iterator.scan(state, lambda s, x: imperative(
    s.update(lambda v: v * x),
    nothing() if s.done() > 6 else just(-s.done()),
))
assert result.next().unwrap() == -1
assert result.next().unwrap() == -2
assert result.next().unwrap() == -6
assert result.next().is_nothing()

skip(n) #

skip(n: int) -> Iterator[I]
skip(n: int) -> Iterator[Item]

Creates a new iterator that skips the first n elements of the original iterator. If the original iterator has fewer than n elements, all elements are skipped.

Parameters:

Name Type Description Default
n int

The number of elements to skip from the start of the iterator.

required

Raises:

Type Description
ValueError

If n is negative.

iterator = itrt([1, 2, 3, 4, 5])
skipped_iterator = iterator.skip(2)
assert skipped_iterator.next().unwrap() == 3
assert skipped_iterator.next().unwrap() == 4
assert skipped_iterator.next().unwrap() == 5
assert skipped_iterator.next().is_nothing()
assert iterator.next().is_nothing()  # original iterator is also exhausted

skip_while(pred) #

skip_while(pred: Callable[[I], bool]) -> Iterator[I]
skip_while(pred: Callable[[Item], bool]) -> Iterator[Item]

Creates a new iterator that skips elements while the predicate pred returns True. Once the predicate returns False, all remaining elements are yielded.

iterator = itrt([1, 2, 3, 4, 1, 2])
skipped_iterator = iterator.skip_while(lambda x: x < 4)
assert skipped_iterator.next().unwrap() == 4
assert skipped_iterator.next().unwrap() == 1
assert skipped_iterator.next().unwrap() == 2
assert skipped_iterator.next().is_nothing()

step_by(n) #

step_by(step: int) -> Iterator[I]
step_by(step: int) -> Iterator[Item]

Creates a new iterator that yields every n-th element of the original iterator. The first element (index 0) is always yielded. It does not guarantee the skipped elements are consumed before or after yielding the next element.

iterator = itrt([1, 2, 3, 4, 5, 6, 7, 8])
stepped_iterator = iterator.step_by(2)
assert stepped_iterator.next().unwrap() == 1
assert stepped_iterator.next().unwrap() == 3
assert stepped_iterator.next().unwrap() == 5
assert stepped_iterator.next().unwrap() == 7
assert stepped_iterator.next().is_nothing()

take(n) #

take(n: int) -> Iterator[I]
take(n: int) -> Iterator[Item]

Creates a new iterator that stops after the first n elements of the original iterator. If the original iterator has fewer than n elements, all elements are yielded.

Parameters:

Name Type Description Default
n int

The number of elements to take from the start of the iterator.

required

Raises:

Type Description
ValueError

If n is negative.

iterator = itrt([1, 2, 3, 4, 5])
taken_iterator = iterator.take(3)
assert taken_iterator.next().unwrap() == 1
assert taken_iterator.next().unwrap() == 2
assert taken_iterator.next().unwrap() == 3
assert taken_iterator.next().is_nothing()
assert iterator.next().unwrap() == 4  # original iterator continues from where take stopped

take_while(pred) #

take_while(pred: Callable[[I], bool]) -> Iterator[I]
take_while(pred: Callable[[Item], bool]) -> Iterator[Item]

Creates a new iterator that yields elements while the predicate pred returns True. Once the predicate returns False, iteration stops.

iterator = itrt([1, 2, 3, 4, 1, 2])
taken_iterator = iterator.take_while(lambda x: x < 4)
assert taken_iterator.next().unwrap() == 1
assert taken_iterator.next().unwrap() == 2
assert taken_iterator.next().unwrap() == 3
assert taken_iterator.next().is_nothing()
assert iterator.next().unwrap() == 1  # elements after the predicate failed

tap(func) #

tap(func: Callable[[I], Any]) -> Iterator[I]
tap(func: Callable[[Item], Any]) -> Iterator[Item]

Creates a new iterator that calls func on each element for side effects, passing the element through unchanged.

result = []
iterator = itrt([1, 2, 3])
tapped = iterator.tap(result.append)
assert tapped.next().unwrap() == 1
assert result == [1]
assert tapped.next().unwrap() == 2
assert result == [1, 2]
assert tapped.next().unwrap() == 3
assert result == [1, 2, 3]
assert tapped.next().is_nothing()

zip(*others) #

zip(other: Iterable[U]) -> Iterator[tuple[I, U]]
zip(other1: Iterable[U1], other2: Iterable[U2]) -> Iterator[tuple[I, U1, U2]]
zip(other1: Iterable[U1], other2: Iterable[U2], other3: Iterable[U3]) -> Iterator[tuple[I, U1, U2, U3]]
zip(*others: VanillaIterator[I]) -> Iterator[tuple[I, ...]]
zip(*others: Iterable[Any]) -> Iterator[tuple[Any, ...]]
zip(other: Iterable[U]) -> Iterator[tuple[Item, U]]
zip(other1: Iterable[U1], other2: Iterable[U2]) -> Iterator[tuple[Item, U1, U2]]
zip(other1: Iterable[U1], other2: Iterable[U2], other3: Iterable[U3]) -> Iterator[tuple[Item, U1, U2, U3]]
zip(*others: VanillaIterator[Item]) -> Iterator[tuple[Item, ...]]
zip(*others: Iterable[Any]) -> Iterator[tuple[Any, ...]]

Zips up this iterator with one or more other iterables into a single iterator of tuples. Iteration stops whenever an iterator or iterable is exhausted.

The iterators are guaranteed to be consumed in the order they are passed in.

Warning

The original iterators should not be pulled after being zipped together, as some of their elements may have been consumed and discarded during the zipping process.

iterator1 = itrt([1, 2, 3])
iterator2 = itrt([4, 5, 6])
zipped = iterator1.zip(iterator2)
assert zipped.next().unwrap() == (1, 4)
assert zipped.next().unwrap() == (2, 5)
assert zipped.next().unwrap() == (3, 6)
assert zipped.next().is_nothing()

iterator1 = itrt([1, 2, 3])
iterator2 = itrt(['a', 'b'])
zipped = iterator1.zip(iterator2)
assert zipped.next().unwrap() == (1, 'a')
assert zipped.next().unwrap() == (2, 'b')
assert zipped.next().is_nothing()
assert iterator1.next().is_nothing() # 3 is consumed and discarded during zipping

iterator = itrt([1, 2, 3])
zipped = iterator.zip([4, 5, 6])
assert zipped.next().unwrap() == (1, 4)
assert zipped.next().unwrap() == (2, 5)
assert zipped.next().unwrap() == (3, 6)
assert zipped.next().is_nothing()