Skip to content

apfel.core.iter

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 exposed in the package namespace.

Implementation

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

API#

Classes

NameDescription
Iteratorpython

Functions

NameDescription
itrtCreate an Iterator from a standard Python Iterator or Iterable.

Iteratorclass#

class Iterator(_dispatch.ABCDispatch, Generic[I])

Bases: _dispatch.ABCDispatch, Generic[I]

class Iterator[I](ABC):
__next__

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

__eq__method#

def __eq__(other: object) -> bool

__iter__method#

def __iter__() -> Iterator[I]

__next__methodabstract#

def __next__() -> I

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

accumulatemethod#

def accumulate(state: T, func: Callable[[T, I], T] | 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_bymethod#

def 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

allmethod#

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

Returns True if all elements of the iterator satisfy the predicate pred. 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

anymethod#

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

Returns True if any element of the iterator satisfies the predicate pred. 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

chainmethod#

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

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

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()

countmethod#

def 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

enumeratemethod#

def enumerate(init: int = 0) -> Iterator[tuple[int, I]] | 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.

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()

Parameters

NameTypeDefaultDescription
initint0The starting index for enumeration. Default is 0.

eqmethod#

def eq(other: Iterator[I] | VanillaIterator[I] | 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)

filtermethod#

def filter(pred: Callable[[I], bool] | Callable[[Item], bool], /) -> Iterator[I] | 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_mapmethod#

def filter_map(pred: Callable[[I], Maybe[U]] | 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()

findmethod#

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

Returns 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.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_mapmethod#

def find_map(pred: Callable[[I], Maybe[U]] | 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_mapmethod#

def flat_map(func: Callable[[I], Iterable[U]] | 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()

flattenmethod#

def flatten() -> Iterator[I]

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

foldmethod#

def fold(init: T, func: Callable[[T, I], T] | 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.

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_eachmethod#

def for_each(func: Callable[[I], Any] | Callable[[Item], Any], /)

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

hintmethod#

def hint(hint: TypeForm[Item], /) -> Iterator[Item]

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

interspersemethod#

def intersperse(separator: I | Item, /) -> Iterator[I] | 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_withmethod#

def intersperse_with(sep_fn: Callable[[], I] | Callable[[], Item], /) -> Iterator[I] | 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']

lastmethod#

def last() -> Maybe[I] | 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()

mapmethod#

def map(func: Callable[[I], U] | 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_whilemethod#

def map_while(pred: Callable[[I], Maybe[U]] | 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()

nextmethod#

def next() -> _maybe.Maybe

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()

nthmethod#

def nth(n: int, /) -> Maybe[I] | 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.

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

pipemethod#

def pipe(func: Callable[Concatenate[Iterable[I], P], T] | 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"

positionmethod#

def position(pred: Callable[[I], bool] | 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

reducemethod#

def reduce(func: Callable[[I, I], I] | Callable[[Item, Item], Item], /) -> Maybe[I] | 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

scanmethod#

def scan(state: St, func: Callable[[St, I], Maybe[B]] | 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()

skipmethod#

def skip(n: int, /) -> Iterator[I] | 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.

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

Parameters

NameTypeDescription
nintThe number of elements to skip from the start of the iterator.

Raises

ValueError
If n is negative.

skip_whilemethod#

def skip_while(pred: Callable[[I], bool] | Callable[[Item], bool], /) -> Iterator[I] | 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_bymethod#

def step_by(n: int, /) -> Iterator[I] | 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()

takemethod#

def take(n: int, /) -> Iterator[I] | 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.

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

Parameters

NameTypeDescription
nintThe number of elements to take from the start of the iterator.

Raises

ValueError
If n is negative.

take_whilemethod#

def take_while(pred: Callable[[I], bool] | Callable[[Item], bool], /) -> Iterator[I] | 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

tapmethod#

def tap(func: Callable[[I], Any] | Callable[[Item], Any], /) -> Iterator[I] | 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()

zipmethod#

def zip(*others: VanillaIterator[I] | Iterable[Any] | VanillaIterator[Item]) -> Iterator[tuple[I, U]] | Iterator[tuple[I, U1, U2]] | Iterator[tuple[I, U1, U2, U3]] | Iterator[tuple[I, ...]] | Iterator[tuple[Any, ...]] | Iterator[tuple[Item, U]] | Iterator[tuple[Item, U1, U2]] | Iterator[tuple[Item, U1, U2, U3]] | Iterator[tuple[Item, ...]]

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.

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()

itrtfunction#

def itrt(iterable: Iterator[I] | Iterable[I] | VanillaIterator[I], /) -> Iterator[I]

Create an Iterator from a standard Python Iterator or Iterable.

Parameters

NameTypeDescription
iterableIterator[I] | Iterable[I] | VanillaIterator[I]A Python standard collections.abc.Iterator or collections.abc.Iterable.

Returns

Iterator[I]
An iterator that guarantees the apfel Iterator interface.

Raises

TypeError
If the input is neither an Iterator nor an Iterable.