Skip to content

Commit

Permalink
Add cycle
Browse files Browse the repository at this point in the history
Resolves   #690.
  • Loading branch information
evhub committed Dec 30, 2022
1 parent a4f286e commit f2686ce
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 24 deletions.
39 changes: 39 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3022,6 +3022,45 @@ count()$[10**100] |> print
**Python:**
_Can't be done quickly without Coconut's iterator slicing, which requires many complicated pieces. The necessary definitions in Python can be found in the Coconut header._

### `cycle`

**cycle**(_iterable_, _times_=`None`)

Coconut's `cycle` is a modified version of `itertools.cycle` with a `times` parameter that controls the number of times to cycle through _iterable_ before stopping. `cycle` also supports `in`, slicing, `len`, `reversed`, `.count()`, `.index()`, and `repr`.

##### Python Docs

**cycle**(_iterable_)

Make an iterator returning elements from the iterable and saving a copy of each. When the iterable is exhausted, return elements from the saved copy. Repeats indefinitely. Roughly equivalent to:

```coconut_python
def cycle(iterable):
# cycle('ABCD') --> A B C D A B C D A B C D ...
saved = []
for element in iterable:
yield element
saved.append(element)
while saved:
for element in saved:
yield element
```

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

##### Example

**Coconut:**
```coconut
cycle(range(2), 2) |> list |> print
```

**Python:**
```coconut_python
from itertools import cycle, islice
print(list(islice(cycle(range(2)), 4)))
```

### `makedata`

**makedata**(_data\_type_, *_args_)
Expand Down
20 changes: 19 additions & 1 deletion __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -592,13 +592,31 @@ class _count(_t.Iterable[_T]):
def __getitem__(self, index: slice) -> _t.Iterable[_T]: ...

def __hash__(self) -> int: ...
def count(self, elem: _T) -> int: ...
def count(self, elem: _T) -> int | float: ...
def index(self, elem: _T) -> int: ...
def __fmap__(self, func: _t.Callable[[_T], _Uco]) -> _count[_Uco]: ...
def __copy__(self) -> _count[_T]: ...
count = _coconut_count = _count # necessary since we define .count()


class cycle(_t.Iterable[_T]):
def __new__(self, iterable: _t.Iterable[_T], times: _t.Optional[int]=None) -> cycle[_T]: ...
def __iter__(self) -> _t.Iterator[_T]: ...
def __contains__(self, elem: _T) -> bool: ...

@_t.overload
def __getitem__(self, index: int) -> _T: ...
@_t.overload
def __getitem__(self, index: slice) -> _t.Iterable[_T]: ...

def __hash__(self) -> int: ...
def count(self, elem: _T) -> int | float: ...
def index(self, elem: _T) -> int: ...
def __fmap__(self, func: _t.Callable[[_T], _Uco]) -> _t.Iterable[_Uco]: ...
def __copy__(self) -> cycle[_T]: ...
def __len__(self) -> int: ...


class flatten(_t.Iterable[_T]):
def __new__(self, iterable: _t.Iterable[_t.Iterable[_T]]) -> flatten[_T]: ...

Expand Down
7 changes: 4 additions & 3 deletions coconut/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3065,10 +3065,11 @@ def op_match_funcdef_handle(self, original, loc, tokens):
def set_literal_handle(self, tokens):
"""Converts set literals to the right form for the target Python."""
internal_assert(len(tokens) == 1 and len(tokens[0]) == 1, "invalid set literal tokens", tokens)
if self.target_info < (2, 7):
return "_coconut.set(" + set_to_tuple(tokens[0]) + ")"
contents, = tokens
if self.target_info < (2, 7) or "testlist_star_expr" in contents:
return "_coconut.set(" + set_to_tuple(contents) + ")"
else:
return "{" + tokens[0][0] + "}"
return "{" + contents[0] + "}"

def set_letter_literal_handle(self, tokens):
"""Process set literals with set letters."""
Expand Down
73 changes: 54 additions & 19 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ class _coconut_has_iter(_coconut_base_hashable):
with self.lock:
self.iter = _coconut_reiterable(self.iter)
return self.iter
def __fmap__(self, func):
return _coconut_map(func, self)
class reiterable(_coconut_has_iter):
"""Allow an iterator to be iterated over multiple times with the same results."""
__slots__ = ()
Expand All @@ -166,8 +168,6 @@ class reiterable(_coconut_has_iter):
return (self.__class__, (self.iter,))
def __copy__(self):
return self.__class__(self.get_new_iter())
def __fmap__(self, func):
return _coconut_map(func, self)
def __getitem__(self, index):
return _coconut_iter_getitem(self.get_new_iter(), index)
def __reversed__(self):
Expand Down Expand Up @@ -432,8 +432,6 @@ class scan(_coconut_has_iter):
if not _coconut.isinstance(self.iter, _coconut.abc.Sized):
return _coconut.NotImplemented
return _coconut.len(self.iter)
def __fmap__(self, func):
return _coconut_map(func, self)
class reversed(_coconut_has_iter):
__slots__ = ()
__doc__ = getattr(_coconut.reversed, "__doc__", "<see help(py_reversed)>")
Expand Down Expand Up @@ -838,8 +836,6 @@ class multi_enumerate(_coconut_has_iter):
__slots__ = ()
def __repr__(self):
return "multi_enumerate(%s)" % (_coconut.repr(self.iter),)
def __fmap__(self, func):
return _coconut_map(func, self)
def __reduce__(self):
return (self.__class__, (self.iter,))
def __copy__(self):
Expand Down Expand Up @@ -882,19 +878,22 @@ class multi_enumerate(_coconut_has_iter):
return self.iter.size
return _coconut.NotImplemented
class count(_coconut_base_hashable):
"""count(start, step) returns an infinite iterator starting at start and increasing by step.

If step is set to 0, count will infinitely repeat its first argument.
"""
__slots__ = ("start", "step")
__doc__ = getattr(_coconut.itertools.count, "__doc__", "count(start, step) returns an infinite iterator starting at start and increasing by step.")
def __init__(self, start=0, step=1):
self.start = start
self.step = step
def __reduce__(self):
return (self.__class__, (self.start, self.step))
def __repr__(self):
return "count(%s, %s)" % (_coconut.repr(self.start), _coconut.repr(self.step))
def __iter__(self):
while True:
yield self.start
if self.step:
self.start += self.step
def __fmap__(self, func):
return _coconut_map(func, self)
def __contains__(self, elem):
if not self.step:
return elem == self.start
Expand Down Expand Up @@ -932,12 +931,51 @@ class count(_coconut_base_hashable):
if not self.step:
return self
raise _coconut.TypeError(_coconut.repr(self) + " object is not reversible")
def __repr__(self):
return "count(%s, %s)" % (_coconut.repr(self.start), _coconut.repr(self.step))
class cycle(_coconut_has_iter):
__slots__ = ("times",)
def __new__(cls, iterable, times=None):
self = _coconut_has_iter.__new__(cls, iterable)
self.times = times
return self
def __reduce__(self):
return (self.__class__, (self.start, self.step))
def __fmap__(self, func):
return _coconut_map(func, self)
return (self.__class__, (self.iter, self.times))
def __copy__(self):
return self.__class__(self.get_new_iter(), self.times)
def __repr__(self):
return "cycle(%s, %r)" % (_coconut.repr(self.iter), self.times)
def __iter__(self):
i = 0
while self.times is None or i < self.times:
for x in self.get_new_iter():
yield x
i += 1
def __contains__(self, elem):
return elem in self.iter
def __getitem__(self, index):
if not _coconut.isinstance(index, _coconut.slice):
if self.times is not None and index // _coconut.len(self.iter) >= self.times:
raise _coconut.IndexError("cycle index out of range")
return self.iter[index % _coconut.len(self.iter)]
if self.times is None:
return _coconut_map(self.__getitem__, _coconut_count()[index])
else:
return _coconut_map(self.__getitem__, _coconut_range(0, _coconut.len(self))[index])
def __len__(self):
if self.times is None:
return _coconut.NotImplemented
return _coconut.len(self.iter) * self.times
def __reversed__(self):
if self.times is None:
raise _coconut.TypeError(_coconut.repr(self) + " object is not reversible")
return self.__class__(_coconut_reversed(self.get_new_iter()), self.times)
def count(self, elem):
"""Count the number of times elem appears in the cycle."""
return self.iter.count(elem) * (float("inf") if self.times is None else self.times)
def index(self, elem):
"""Find the index of elem in the cycle."""
if elem not in self.iter:
raise _coconut.ValueError(_coconut.repr(elem) + " not in " + _coconut.repr(self))
return self.iter.index(elem)
class groupsof(_coconut_has_iter):
"""groupsof(n, iterable) splits iterable into groups of size n.

Expand Down Expand Up @@ -973,8 +1011,6 @@ class groupsof(_coconut_has_iter):
return (self.__class__, (self.group_size, self.iter))
def __copy__(self):
return self.__class__(self.group_size, self.get_new_iter())
def __fmap__(self, func):
return _coconut_map(func, self)
class recursive_iterator(_coconut_base_hashable):
"""Decorator that optimizes a recursive function that returns an iterator (e.g. a recursive generator)."""
__slots__ = ("func", "reit_store", "backup_reit_store")
Expand Down Expand Up @@ -1102,7 +1138,6 @@ _coconut_addpattern = addpattern
{def_prepattern}
class _coconut_partial(_coconut_base_hashable):
__slots__ = ("func", "_argdict", "_arglen", "_pos_kwargs", "_stargs", "keywords")
__doc__ = getattr(_coconut.functools.partial, "__doc__", "Partial application of a function.")
def __init__(self, _coconut_func, _coconut_argdict, _coconut_arglen, _coconut_pos_kwargs, *args, **kwargs):
self.func = _coconut_func
self._argdict = _coconut_argdict
Expand Down Expand Up @@ -1551,4 +1586,4 @@ def _coconut_multi_dim_arr(arrs, dim):
max_arr_dim = _coconut.max(arr_dims)
return _coconut_concatenate(arrs, max_arr_dim - dim)
_coconut_self_match_types = {self_match_types}
_coconut_Expected, _coconut_MatchError, _coconut_count, _coconut_enumerate, _coconut_flatten, _coconut_filter, _coconut_ident, _coconut_map, _coconut_multiset, _coconut_reiterable, _coconut_reversed, _coconut_starmap, _coconut_tee, _coconut_zip, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, count, enumerate, flatten, filter, ident, map, multiset, reiterable, reversed, starmap, tee, zip, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile
_coconut_Expected, _coconut_MatchError, _coconut_count, _coconut_enumerate, _coconut_flatten, _coconut_filter, _coconut_ident, _coconut_map, _coconut_multiset, _coconut_range, _coconut_reiterable, _coconut_reversed, _coconut_starmap, _coconut_tee, _coconut_zip, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, count, enumerate, flatten, filter, ident, map, multiset, range, reiterable, reversed, starmap, tee, zip, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile
1 change: 1 addition & 0 deletions coconut/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ def get_bool_env_var(env_var, default=False):
"multi_enumerate",
"cartesian_product",
"multiset",
"cycle",
"py_chr",
"py_hex",
"py_input",
Expand Down
2 changes: 1 addition & 1 deletion coconut/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
VERSION = "2.1.1"
VERSION_NAME = "The Spanish Inquisition"
# False for release, int >= 1 for develop
DEVELOP = 25
DEVELOP = 26
ALPHA = False # for pre releases rather than post releases

# -----------------------------------------------------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions coconut/tests/src/cocotest/agnostic/main.coco
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,14 @@ def main_test() -> bool:
assert not (m{1} == {1:1, 2:0})
assert s{1, 2, *(2, 3, 4), *(4, 5)} == s{1, 2, 3, 4, 5}
assert m{1, 2, *(2, 3, 4), *(4, 5)} == m{1, 2, 2, 3, 4, 4, 5}
assert {*(1, 2)} == {1, 2}
assert cycle(range(3))[:5] |> list == [0, 1, 2, 0, 1] == cycle(range(3)) |> iter |> .$[:5] |> list
assert cycle(range(2), 2)[:5] |> list == [0, 1, 0, 1] == cycle(range(2), 2) |> iter |> .$[:5] |> list
assert 2 in cycle(range(3))
assert reversed(cycle(range(2), 2)) |> list == [1, 0, 1, 0]
assert cycle((_ for _ in range(2)), 2) |> list == [0, 1, 0, 1]
assert cycle(range(3)).count(0) == float("inf")
assert cycle(range(3), 3).index(2) == 2
return True

def test_asyncio() -> bool:
Expand Down

0 comments on commit f2686ce

Please sign in to comment.