Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,25 +539,25 @@ 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:

```python
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!")

Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 34 additions & 20 deletions src/stateless/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,56 @@
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

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(_)
12 changes: 6 additions & 6 deletions tests/test_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,15 +98,15 @@ 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)

assert run_with_abilities(effect(), supply(MockTime())) == (42, 42)


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

Expand All @@ -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"))

Expand All @@ -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:
Expand All @@ -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"))

Expand Down
6 changes: 3 additions & 3 deletions tests/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
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())
assert list(deltas) == [timedelta(seconds=1)] * 3


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
Loading