diff --git a/README.md b/README.md index cb99b9a..709dcc8 100644 --- a/README.md +++ b/README.md @@ -539,13 +539,13 @@ from datetime import timedelta from stateless import Depend, Ability -class Schedule[A: Ability](Protocol): +class Schedule[A: Ability]: def __iter__(self) -> Depend[A, Iterator[timedelta]]: ... ``` The type parameter `A` is present because some schedules may require abilities to complete. -The `stateless.schedule` module contains a number of of helpful implemenations of `Schedule`, for example `Spaced` or `Recurs`. +The `stateless.schedule` module contains a number of of helpful implementations of `Schedule`, for example `spaced` or `recurs`. Schedules can be used with the `repeat` decorator, which takes schedule as its first argument and repeats the decorated function returning an effect until the schedule is exhausted or an error occurs: @@ -553,11 +553,11 @@ Schedules can be used with the `repeat` decorator, which takes schedule as its f from datetime import timedelta from stateless import repeat, success, Success, supply, run -from stateless.schedule import Recurs, Spaced +from stateless.schedule import recurs, spaced from stateless.time import Time -@repeat(Recurs(2, Spaced(timedelta(seconds=2)))) +@repeat(recurs(2, spaced(timedelta(seconds=2)))) def f() -> Success[str]: return success("hi!") @@ -574,7 +574,7 @@ This is a useful pattern because such objects can be yielded from in functions r ```python def this_works() -> Success[timedelta]: - schedule = Spaced(timedelta(seconds=2)) + schedule = spaced(timedelta(seconds=2)) deltas = yield from schedule deltas_again = yield from schedule # safe! return deltas @@ -589,14 +589,14 @@ when the decorated function yields no errors, or fails when the schedule is exha from datetime import timedelta from stateless import retry, throw, Try, throw, success, supply, run -from stateless.schedule import Recurs, Spaced +from stateless.schedule import recurs, spaced from stateless.time import Time fail = True -@retry(Recurs(2, Spaced(timedelta(seconds=2)))) +@retry(recurs(2, spaced(timedelta(seconds=2)))) def f() -> Try[RuntimeError, str]: global fail if fail: @@ -670,10 +670,7 @@ Moreover, monads famously do not compose, meaning that when writing code that ne Additionally, in languages with dynamic binding such as Python, calling functions is relatively expensive, which means that using callbacks as the principal method for resuming computation comes with a fair amount of performance overhead. -Finally, interpreting monads is often a recursive procedure, meaning that it's necessary to worry about stack safety in languages without tail call optimisation such as Python. This is usually solved using [trampolines](https://en.wikipedia.org/wiki/Trampoline_(computing)) which further adds to the performance overhead. - - -Because of all these practical challenges of programming with monads, people have been looking for alternatives. Algebraic effects is one the things suggested that address many of the challenges of monadic effect systems. +Because of all these practical challenges of programming with monads, people have been looking for alternatives. Algebraic effects is one suggested solution that address many of the challenges of monadic effect systems. In algebraic effect systems, such as `stateless`, the programmer still supplies the effect system with a description of the side-effect to be carried out, but instead of supplying a callback function to resume the computation with, the result of handling the effect is returned to the point in program execution that the effect description was produced. The main drawback of this approach is that it requires special language features to do this. In Python however, such a language feature _does_ exist: Generators and coroutines. diff --git a/src/stateless/schedule.py b/src/stateless/schedule.py index c5a9458..dd4389f 100644 --- a/src/stateless/schedule.py +++ b/src/stateless/schedule.py @@ -3,8 +3,9 @@ import itertools from dataclasses import dataclass from datetime import timedelta -from typing import Any, Iterator, Protocol, TypeVar -from typing import NoReturn as Never +from typing import Any, Callable, Generic, Iterator, TypeVar + +from typing_extensions import Never from stateless.ability import Ability from stateless.effect import Depend, Success, success @@ -12,33 +13,46 @@ A = TypeVar("A", covariant=True, bound=Ability[Any]) -class Schedule(Protocol[A]): +@dataclass(frozen=True) +class Schedule(Generic[A]): """An iterator of timedeltas depending on stateless abilities.""" + schedule: Callable[[], Depend[A, Iterator[timedelta]]] + def __iter__(self) -> Depend[A, Iterator[timedelta]]: """Iterate over the schedule.""" - ... # pragma: no cover + return self.schedule() -@dataclass(frozen=True) -class Spaced(Schedule[Never]): - """A schedule that yields a timedelta at a fixed interval forever.""" +def spaced(interval: timedelta) -> Schedule[Never]: + """ + Create a schedule that yields a fixed timedelta forever. - interval: timedelta + Args: + ---- + interval: the fixed interval to yield. - def __iter__(self) -> Success[Iterator[timedelta]]: - """Iterate over the schedule.""" - return success(itertools.repeat(self.interval)) + """ + def schedule() -> Success[Iterator[timedelta]]: + return success(itertools.repeat(interval)) -@dataclass(frozen=True) -class Recurs(Schedule[A]): - """A schedule that yields timedeltas from the schedule given as arguments fixed number of times.""" + return Schedule(schedule) - n: int - schedule: Schedule[A] - def __iter__(self) -> Depend[A, Iterator[timedelta]]: - """Iterate over the schedule.""" - deltas = yield from self.schedule - return itertools.islice(deltas, self.n) +def recurs(n: int, schedule: Schedule[A]) -> Schedule[A]: + """ + Create schedule that yields timedeltas from the schedule given as arguments fixed number of times. + + Args: + ---- + n: the number of times to yield from `schedule`. + schedule: The schedule to yield from. + + """ + + def _() -> Depend[A, Iterator[timedelta]]: + deltas = yield from schedule + return itertools.islice(deltas, n) + + return Schedule(_) diff --git a/tests/test_effect.py b/tests/test_effect.py index 695ed2c..7f7ca90 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -19,7 +19,7 @@ from stateless.effect import SuccessEffect from stateless.functions import RetryError from stateless.need import need -from stateless.schedule import Recurs, Spaced +from stateless.schedule import recurs, spaced from stateless.time import Time from tests.utils import run_with_abilities @@ -98,7 +98,7 @@ def effect() -> Never: def test_repeat() -> None: - @repeat(Recurs(2, Spaced(timedelta(seconds=1)))) + @repeat(recurs(2, spaced(timedelta(seconds=1)))) def effect() -> Success[int]: return success(42) @@ -106,7 +106,7 @@ def effect() -> Success[int]: def test_repeat_on_error() -> None: - @repeat(Recurs(2, Spaced(timedelta(seconds=1)))) + @repeat(recurs(2, spaced(timedelta(seconds=1)))) def effect() -> Try[RuntimeError, Never]: return throw(RuntimeError("oops")) @@ -115,7 +115,7 @@ def effect() -> Try[RuntimeError, Never]: def test_retry() -> None: - @repeat(Recurs(2, Spaced(timedelta(seconds=1)))) + @repeat(recurs(2, spaced(timedelta(seconds=1)))) def effect() -> Try[RuntimeError, Never]: return throw(RuntimeError("oops")) @@ -126,7 +126,7 @@ def effect() -> Try[RuntimeError, Never]: def test_retry_on_eventual_success() -> None: counter = 0 - @retry(Recurs(2, Spaced(timedelta(seconds=1)))) + @retry(recurs(2, spaced(timedelta(seconds=1)))) def effect() -> Effect[Never, RuntimeError, int]: nonlocal counter if counter == 1: @@ -138,7 +138,7 @@ def effect() -> Effect[Never, RuntimeError, int]: def test_retry_on_failure() -> None: - @retry(Recurs(2, Spaced(timedelta(seconds=1)))) + @retry(recurs(2, spaced(timedelta(seconds=1)))) def effect() -> Effect[Never, RuntimeError, int]: return throw(RuntimeError("oops")) diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 7a861b7..2234f98 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -3,12 +3,12 @@ from typing import Iterator from stateless import Success, run -from stateless.schedule import Recurs, Spaced +from stateless.schedule import recurs, spaced def test_spaced() -> None: def effect() -> Success[Iterator[timedelta]]: - schedule = yield from Spaced(timedelta(seconds=1)) + schedule = yield from spaced(timedelta(seconds=1)) return itertools.islice(schedule, 3) deltas = run(effect()) @@ -16,6 +16,6 @@ def effect() -> Success[Iterator[timedelta]]: def test_recurs() -> None: - schedule = Recurs(3, Spaced(timedelta(seconds=1))) + schedule = recurs(3, spaced(timedelta(seconds=1))) deltas = run(iter(schedule)) assert list(deltas) == [timedelta(seconds=1)] * 3