Skip to content

Commit 78ec2b0

Browse files
authored
feat: new schedule api (#18)
1 parent 828fa7c commit 78ec2b0

File tree

4 files changed

+51
-40
lines changed

4 files changed

+51
-40
lines changed

README.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -539,25 +539,25 @@ from datetime import timedelta
539539
from stateless import Depend, Ability
540540

541541

542-
class Schedule[A: Ability](Protocol):
542+
class Schedule[A: Ability]:
543543
def __iter__(self) -> Depend[A, Iterator[timedelta]]:
544544
...
545545
```
546546
The type parameter `A` is present because some schedules may require abilities to complete.
547547

548-
The `stateless.schedule` module contains a number of of helpful implemenations of `Schedule`, for example `Spaced` or `Recurs`.
548+
The `stateless.schedule` module contains a number of of helpful implementations of `Schedule`, for example `spaced` or `recurs`.
549549

550550
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:
551551

552552
```python
553553
from datetime import timedelta
554554

555555
from stateless import repeat, success, Success, supply, run
556-
from stateless.schedule import Recurs, Spaced
556+
from stateless.schedule import recurs, spaced
557557
from stateless.time import Time
558558

559559

560-
@repeat(Recurs(2, Spaced(timedelta(seconds=2))))
560+
@repeat(recurs(2, spaced(timedelta(seconds=2))))
561561
def f() -> Success[str]:
562562
return success("hi!")
563563

@@ -574,7 +574,7 @@ This is a useful pattern because such objects can be yielded from in functions r
574574

575575
```python
576576
def this_works() -> Success[timedelta]:
577-
schedule = Spaced(timedelta(seconds=2))
577+
schedule = spaced(timedelta(seconds=2))
578578
deltas = yield from schedule
579579
deltas_again = yield from schedule # safe!
580580
return deltas
@@ -589,14 +589,14 @@ when the decorated function yields no errors, or fails when the schedule is exha
589589
from datetime import timedelta
590590

591591
from stateless import retry, throw, Try, throw, success, supply, run
592-
from stateless.schedule import Recurs, Spaced
592+
from stateless.schedule import recurs, spaced
593593
from stateless.time import Time
594594

595595

596596
fail = True
597597

598598

599-
@retry(Recurs(2, Spaced(timedelta(seconds=2))))
599+
@retry(recurs(2, spaced(timedelta(seconds=2))))
600600
def f() -> Try[RuntimeError, str]:
601601
global fail
602602
if fail:
@@ -670,10 +670,7 @@ Moreover, monads famously do not compose, meaning that when writing code that ne
670670

671671
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.
672672

673-
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.
674-
675-
676-
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.
673+
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.
677674

678675
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
679676
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.

src/stateless/schedule.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,56 @@
33
import itertools
44
from dataclasses import dataclass
55
from datetime import timedelta
6-
from typing import Any, Iterator, Protocol, TypeVar
7-
from typing import NoReturn as Never
6+
from typing import Any, Callable, Generic, Iterator, TypeVar
7+
8+
from typing_extensions import Never
89

910
from stateless.ability import Ability
1011
from stateless.effect import Depend, Success, success
1112

1213
A = TypeVar("A", covariant=True, bound=Ability[Any])
1314

1415

15-
class Schedule(Protocol[A]):
16+
@dataclass(frozen=True)
17+
class Schedule(Generic[A]):
1618
"""An iterator of timedeltas depending on stateless abilities."""
1719

20+
schedule: Callable[[], Depend[A, Iterator[timedelta]]]
21+
1822
def __iter__(self) -> Depend[A, Iterator[timedelta]]:
1923
"""Iterate over the schedule."""
20-
... # pragma: no cover
24+
return self.schedule()
2125

2226

23-
@dataclass(frozen=True)
24-
class Spaced(Schedule[Never]):
25-
"""A schedule that yields a timedelta at a fixed interval forever."""
27+
def spaced(interval: timedelta) -> Schedule[Never]:
28+
"""
29+
Create a schedule that yields a fixed timedelta forever.
2630
27-
interval: timedelta
31+
Args:
32+
----
33+
interval: the fixed interval to yield.
2834
29-
def __iter__(self) -> Success[Iterator[timedelta]]:
30-
"""Iterate over the schedule."""
31-
return success(itertools.repeat(self.interval))
35+
"""
3236

37+
def schedule() -> Success[Iterator[timedelta]]:
38+
return success(itertools.repeat(interval))
3339

34-
@dataclass(frozen=True)
35-
class Recurs(Schedule[A]):
36-
"""A schedule that yields timedeltas from the schedule given as arguments fixed number of times."""
40+
return Schedule(schedule)
3741

38-
n: int
39-
schedule: Schedule[A]
4042

41-
def __iter__(self) -> Depend[A, Iterator[timedelta]]:
42-
"""Iterate over the schedule."""
43-
deltas = yield from self.schedule
44-
return itertools.islice(deltas, self.n)
43+
def recurs(n: int, schedule: Schedule[A]) -> Schedule[A]:
44+
"""
45+
Create schedule that yields timedeltas from the schedule given as arguments fixed number of times.
46+
47+
Args:
48+
----
49+
n: the number of times to yield from `schedule`.
50+
schedule: The schedule to yield from.
51+
52+
"""
53+
54+
def _() -> Depend[A, Iterator[timedelta]]:
55+
deltas = yield from schedule
56+
return itertools.islice(deltas, n)
57+
58+
return Schedule(_)

tests/test_effect.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from stateless.effect import SuccessEffect
2020
from stateless.functions import RetryError
2121
from stateless.need import need
22-
from stateless.schedule import Recurs, Spaced
22+
from stateless.schedule import recurs, spaced
2323
from stateless.time import Time
2424

2525
from tests.utils import run_with_abilities
@@ -98,15 +98,15 @@ def effect() -> Never:
9898

9999

100100
def test_repeat() -> None:
101-
@repeat(Recurs(2, Spaced(timedelta(seconds=1))))
101+
@repeat(recurs(2, spaced(timedelta(seconds=1))))
102102
def effect() -> Success[int]:
103103
return success(42)
104104

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

107107

108108
def test_repeat_on_error() -> None:
109-
@repeat(Recurs(2, Spaced(timedelta(seconds=1))))
109+
@repeat(recurs(2, spaced(timedelta(seconds=1))))
110110
def effect() -> Try[RuntimeError, Never]:
111111
return throw(RuntimeError("oops"))
112112

@@ -115,7 +115,7 @@ def effect() -> Try[RuntimeError, Never]:
115115

116116

117117
def test_retry() -> None:
118-
@repeat(Recurs(2, Spaced(timedelta(seconds=1))))
118+
@repeat(recurs(2, spaced(timedelta(seconds=1))))
119119
def effect() -> Try[RuntimeError, Never]:
120120
return throw(RuntimeError("oops"))
121121

@@ -126,7 +126,7 @@ def effect() -> Try[RuntimeError, Never]:
126126
def test_retry_on_eventual_success() -> None:
127127
counter = 0
128128

129-
@retry(Recurs(2, Spaced(timedelta(seconds=1))))
129+
@retry(recurs(2, spaced(timedelta(seconds=1))))
130130
def effect() -> Effect[Never, RuntimeError, int]:
131131
nonlocal counter
132132
if counter == 1:
@@ -138,7 +138,7 @@ def effect() -> Effect[Never, RuntimeError, int]:
138138

139139

140140
def test_retry_on_failure() -> None:
141-
@retry(Recurs(2, Spaced(timedelta(seconds=1))))
141+
@retry(recurs(2, spaced(timedelta(seconds=1))))
142142
def effect() -> Effect[Never, RuntimeError, int]:
143143
return throw(RuntimeError("oops"))
144144

tests/test_schedule.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
from typing import Iterator
44

55
from stateless import Success, run
6-
from stateless.schedule import Recurs, Spaced
6+
from stateless.schedule import recurs, spaced
77

88

99
def test_spaced() -> None:
1010
def effect() -> Success[Iterator[timedelta]]:
11-
schedule = yield from Spaced(timedelta(seconds=1))
11+
schedule = yield from spaced(timedelta(seconds=1))
1212
return itertools.islice(schedule, 3)
1313

1414
deltas = run(effect())
1515
assert list(deltas) == [timedelta(seconds=1)] * 3
1616

1717

1818
def test_recurs() -> None:
19-
schedule = Recurs(3, Spaced(timedelta(seconds=1)))
19+
schedule = recurs(3, spaced(timedelta(seconds=1)))
2020
deltas = run(iter(schedule))
2121
assert list(deltas) == [timedelta(seconds=1)] * 3

0 commit comments

Comments
 (0)