Skip to content

Commit 1a1a7e2

Browse files
committed
docs: improve readme
1 parent b6079db commit 1a1a7e2

1 file changed

Lines changed: 35 additions & 10 deletions

File tree

README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Statically typed, purely functional effects for Python.
55
# Motivation
66
Programming with side-effects is hard: To reason about a unit in your code, like a function, you need to know what the other units in the program are doing to the program state, and understand how that affects what you're trying to achieve.
77

8-
Programming without side-effects is _less_ hard: To reason about a unit in you code, like a function, you can focus on what _that_ function is doing, since the units it interacts with don't affect the state of the program in any way.
8+
Programming without side-effects is _less_ hard: To reason about a unit in your code, like a function, you can focus on what _that_ function is doing, since the units it interacts with don't affect the state of the program in any way.
99

1010
But of course side-effects can't be avoided, since what we ultimately care about in programming are the side effects, such as printing to the console or writing to a database.
1111

@@ -15,10 +15,21 @@ As a result, "business logic" code never performs side-effects, which makes it e
1515

1616
# Quickstart
1717

18+
## Install
19+
20+
21+
```console
22+
> pip install stateless
23+
```
24+
25+
26+
## Usage
27+
28+
1829
```python
1930
from typing import Any, Never
2031

21-
from stateless import Effect, Need, need, throws, catch, run
32+
from stateless import Effect, Need, need, throws, catch, run, supply
2233

2334

2435
# stateless.Effect is just an alias for:
@@ -50,7 +61,7 @@ def print_(value: Any) -> Effect[Need[Console], Never, None]:
5061
# Effects can yield exceptions. 'stateless.throws' will catch exceptions
5162
# for you and yield them to other functions so you can handle them with
5263
# type safety. The return type of the decorated function in this
53-
# example is: ´Effect[Need[Files], OSError, str]'
64+
# example is: ´Effect[Need[Files], OSError, str]`
5465
@throws(OSError)
5566
def read_file(path: str) -> Effect[Need[Files], Never, str]:
5667
files = yield from need(Files)
@@ -91,7 +102,7 @@ from stateless import Ability
91102

92103
type Effect[A: Ability, E: Exception, R] = Generator[A | E, Any, R]
93104
```
94-
In other words, an `Effect` is a generator that can yield values of type `A` or exceptions of type `E`, can be sent anything, and returns results of type `R`. Let's break that down a bit further:
105+
In other words, an `Effect` is a generator that can yield values of type `A` or error values of type `E`, can be sent anything, and returns results of type `R`. Let's break that down a bit further:
95106

96107
- The type parameter `A` stands for _"Ability"_. This is the type of value, or types of values, that must be handled in order for the effect to produce its result.
97108

@@ -158,7 +169,7 @@ def hello_world() -> Effect[Greet, Never, None]:
158169
print(greeting)
159170
```
160171

161-
When `hello_world` returns an `Effect[Greet, Never, None]`, it means that it depends on the `Greet` ability (`A` is parameterized with `Greet`). It doesn't produce errors (`E` is parameterized with `Never`), and it doesn't return a value (`R` is parameterized with `None`).
172+
When `hello_world` has return type `Effect[Greet, Never, None]`, it means that it depends on the `Greet` ability (`A` is parameterized with `Greet`). It doesn't produce errors (`E` is parameterized with `Never`), and it doesn't return a value (`R` is parameterized with `None`).
162173

163174
To run an `Effect` that depends on abilities, you need to handle all of the abilities yielded by that effect. Abilities are handled using `stateless.Handler`, defined as:
164175

@@ -203,7 +214,7 @@ def run[R](effect: Effect[Async, Exception, R]) -> R:
203214
...
204215
```
205216

206-
In words: the effect passed to `run` must have had all of its abilities handled (except the built-in `Async` ability. Don't worry about this for now, we'll explain it later). The result of running `effect` is the result type `R`.
217+
In words: In words: the effect passed to `run` must depend **only** on the built-in `Async` ability (Don't worry about the `Async` ability for now, we'll explain it later). All other abilities must have been handled before calling `run`. The result of running `effect` is the result type `R`.
207218

208219
If we try to do:
209220
```python
@@ -355,6 +366,9 @@ This means that:
355366

356367
## Built-in Abilities
357368

369+
While you can write your own abilities and handlers, `stateless` supplies a few
370+
built-in abilities.
371+
358372
### Need
359373

360374
`Need` is an ability for type-safe dependency injection. By "type-safe" we mean:
@@ -404,21 +418,31 @@ When trying to handle `Need[Console]` with `supply(MockConsole())`, you may need
404418

405419
To assist with type inference for type checkers with local type narrowing, stateless supplies a utility function `as_type`, that tells your type checker to treat a subtype as a supertype in a certain context.
406420

421+
`as_type` has type:
422+
423+
424+
```python
425+
from typing import Type, Callable
426+
427+
428+
def as_type[T](t: Type[T]) -> Callable[[T], T]: ...
429+
```
430+
407431
Let's use `as_type` with `supply`:
408432
```python
409433
from stateless import as_type, supply
410434

411435

412436
console = as_type(Console)(MockConsole())
413-
effect = supply(console)(say_hello)('foo.txt')
437+
effect = supply(console)(say_hello)()
414438
run(effect)
415439
```
416440
Using `as_type`, our type checker has correctly inferred that the `Need[Console]` ability yielded by `say_hello` was eliminated by `supply(console)`.
417441

418442
### Async
419443
The `Async` ability is used to run code asynchronously, either with `asyncio` or `concurrent.futures`.
420444

421-
to use the result of an `asyncio` coroutine in an effect, use the `stateless.wait` function. Its defined as:
445+
To use the result of an `asyncio` coroutine in an effect, use the `stateless.wait` function. Its defined as:
422446

423447

424448
```python
@@ -564,9 +588,9 @@ def f() -> Success[str]:
564588
time = Time()
565589
effect = supply(time)(f)()
566590
result = run(effect)
567-
print(run) # outputs: ("hi!", "hi!")
591+
print(result) # outputs: ("hi!", "hi!")
568592
```
569-
Effects created through repeat depends on the `Need[stateless.Time]` because it needs to sleep between each execution of the effect.
593+
Effects created through repeat depends on the `Need[stateless.Time]` ability because it needs to sleep between each execution of the effect.
570594

571595
Schedules are a good example of a pattern used a lot in `stateless`: Classes with an `__iter__` method that returns effects.
572596

@@ -632,6 +656,7 @@ def g() -> Depend[Need[Console], tuple[str, str]]:
632656
second = yield from f()
633657
return first, second
634658

659+
635660
console = Console()
636661
effect = supply(console)(f)()
637662
result = run(effect) # outputs: 'f was called' once, even though the effect `f()` was yielded from twice

0 commit comments

Comments
 (0)