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
itrtto wrap any iterable or Python vanilla iterator, - Use
Iteratormethods 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
| Name | Description |
|---|---|
Iterator | python |
Functions
| Name | Description |
|---|---|
itrt | Create 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__() -> IReturn 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) == 1assert next(iterator) == 2assert next(iterator) == 3
try: next(iterator)except StopIteration: # Iterator is exhausted passaccumulatemethod#
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() == 1assert result.next().unwrap() == 3assert result.next().unwrap() == 6assert result.next().unwrap() == 10assert 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() == 3assert iterator.advance_by(6).unwrap_err() == 4allmethod#
def all(pred: Callable[[I], bool] | Callable[[Item], bool], /) -> boolReturns 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() == 6anymethod#
def any(pred: Callable[[I], bool] | Callable[[Item], bool], /) -> boolReturns 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() == 3chainmethod#
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() == 1assert chained_iterator.next().unwrap() == 2assert iterator1.next().is_nothing() # original iterator1 is also exhaustedassert chained_iterator.next().unwrap() == 3assert chained_iterator.next().unwrap() == 4assert iterator2.next().is_nothing() # original iterator2 is also exhaustedassert chained_iterator.next().unwrap() == 5assert chained_iterator.next().unwrap() == 6assert chained_iterator.next().is_nothing()countmethod#
def count() -> intCounts the number of elements in the iterator, until it is exhausted.
iterator = itrt([1, 2, 3, 4, 5])assert iterator.count() == 5enumeratemethod#
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
| Name | Type | Default | Description |
|---|---|---|---|
init | int | 0 | The starting index for enumeration. Default is 0. |
eqmethod#
def eq(other: Iterator[I] | VanillaIterator[I] | VanillaIterator[Item], /) -> boolChecks 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() == 2assert filtered_iterator.next().unwrap() == 4assert filtered_iterator.next().is_nothing()assert iterator.next().is_nothing() # original iterator is also exhaustedfilter_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() == 1assert filtered.next().unwrap() == 3assert 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() == 2assert iterator.find(lambda x: x <= 3).unwrap() == 3 # 1, 2 have been consumedassert iterator.find(lambda x: x > 10).is_nothing() # no such element existsassert iterator.next().is_nothing() # iterator is exhaustedfind_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() == 2flat_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() == 1assert result.next().unwrap() == 10assert result.next().unwrap() == 2assert result.next().unwrap() == 20assert result.next().unwrap() == 3assert result.next().unwrap() == 30assert 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() == 1assert flattened_iterator.next().unwrap() == 2assert flattened_iterator.next().unwrap() == 3assert flattened_iterator.next().unwrap() == 4assert flattened_iterator.next().unwrap() == 5assert flattened_iterator.next().is_nothing()assert iterator.next().is_nothing() # original iterator is also exhaustedfoldmethod#
def fold(init: T, func: Callable[[T, I], T] | Callable[[T, Item], T]) -> TFold (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 onlyfor_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 3hintmethod#
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() == 1interspersemethod#
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() == 1assert result.next().unwrap() == 0assert result.next().unwrap() == 2assert result.next().unwrap() == 0assert result.next().unwrap() == 3assert result.next().is_nothing()
assert itrt([]).intersperse(0).next().is_nothing()assert itrt([1]).intersperse(0).next().unwrap() == 1intersperse_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() == 1assert result.next().unwrap() == 0assert result.next().unwrap() == 2assert result.next().unwrap() == 0assert result.next().unwrap() == 3assert result.next().is_nothing()
n = 0def counter(): nonlocal n n += 1 return nresult = 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() == 5assert 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() == 2assert mapped_iterator.next().unwrap() == 4assert mapped_iterator.next().unwrap() == 6assert mapped_iterator.next().is_nothing()assert iterator.next().is_nothing() # original iterator is also exhaustedmap_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() == 2assert result.next().unwrap() == 4assert result.next().unwrap() == 6assert result.next().is_nothing()nextmethod#
def next() -> _maybe.MaybeReturn the next item of the iterator, wrapped in a Maybe.
iterator = itrt([1, 2, 3])assert iterator.next().unwrap() == 1assert iterator.next().unwrap() == 2assert iterator.next().unwrap() == 3assert 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 elementspipemethod#
def pipe(func: Callable[Concatenate[Iterable[I], P], T] | Callable[Concatenate[Iterable[Item], P], T], *args: P.args, **kwargs: P.kwargs) -> TPipes 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() == 1assert 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 exhaustedreducemethod#
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 reducescanmethod#
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, nothingfrom apfel.container.value import Valuefrom 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() == -1assert result.next().unwrap() == -2assert result.next().unwrap() == -6assert 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() == 3assert skipped_iterator.next().unwrap() == 4assert skipped_iterator.next().unwrap() == 5assert skipped_iterator.next().is_nothing()assert iterator.next().is_nothing() # original iterator is also exhaustedParameters
| Name | Type | Description |
|---|---|---|
n | int | The number of elements to skip from the start of the iterator. |
Raises
ValueError- If
nis 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() == 4assert skipped_iterator.next().unwrap() == 1assert skipped_iterator.next().unwrap() == 2assert 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() == 1assert stepped_iterator.next().unwrap() == 3assert stepped_iterator.next().unwrap() == 5assert stepped_iterator.next().unwrap() == 7assert 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() == 1assert taken_iterator.next().unwrap() == 2assert taken_iterator.next().unwrap() == 3assert taken_iterator.next().is_nothing()assert iterator.next().unwrap() == 4 # original iterator continues from where take stoppedParameters
| Name | Type | Description |
|---|---|---|
n | int | The number of elements to take from the start of the iterator. |
Raises
ValueError- If
nis 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() == 1assert taken_iterator.next().unwrap() == 2assert taken_iterator.next().unwrap() == 3assert taken_iterator.next().is_nothing()assert iterator.next().unwrap() == 1 # elements after the predicate failedtapmethod#
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() == 1assert result == [1]assert tapped.next().unwrap() == 2assert result == [1, 2]assert tapped.next().unwrap() == 3assert 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
| Name | Type | Description |
|---|---|---|
iterable | Iterator[I] | Iterable[I] | VanillaIterator[I] | A Python standard collections.abc.Iterator or collections.abc.Iterable. |
Returns
Iterator[I]- An iterator that guarantees the apfel
Iteratorinterface.
Raises
TypeError- If the input is neither an
Iteratornor anIterable.
