wird 1.0.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wird-1.2.0/PKG-INFO +342 -0
- wird-1.2.0/README.md +331 -0
- {wird-1.0.0 → wird-1.2.0}/pyproject.toml +9 -2
- wird-1.2.0/tests/test_maybe.py +245 -0
- wird-1.2.0/tests/test_result.py +331 -0
- wird-1.2.0/uv.lock +336 -0
- wird-1.2.0/wird/__init__.py +26 -0
- {wird-1.0.0 → wird-1.2.0}/wird/_future.py +22 -2
- wird-1.2.0/wird/_maybe.py +915 -0
- wird-1.2.0/wird/_result.py +1347 -0
- {wird-1.0.0 → wird-1.2.0}/wird/_value.py +21 -2
- wird-1.2.0/wird/future_maybe.py +303 -0
- wird-1.2.0/wird/future_result.py +460 -0
- wird-1.2.0/wird/maybe.py +301 -0
- wird-1.2.0/wird/result.py +452 -0
- wird-1.0.0/PKG-INFO +0 -12
- wird-1.0.0/README.md +0 -1
- wird-1.0.0/uv.lock +0 -323
- wird-1.0.0/wird/__init__.py +0 -7
- {wird-1.0.0 → wird-1.2.0}/.github/workflows/check.yml +0 -0
- {wird-1.0.0 → wird-1.2.0}/.github/workflows/publish.yml +0 -0
- {wird-1.0.0 → wird-1.2.0}/.gitignore +0 -0
- {wird-1.0.0 → wird-1.2.0}/.python-version +0 -0
- {wird-1.0.0 → wird-1.2.0}/Makefile +0 -0
- {wird-1.0.0 → wird-1.2.0}/tests/__init__.py +0 -0
- {wird-1.0.0 → wird-1.2.0}/tests/test_future.py +0 -0
- {wird-1.0.0 → wird-1.2.0}/tests/test_value.py +0 -0
- {wird-1.0.0 → wird-1.2.0}/wird/py.typed +0 -0
wird-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wird
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Basic monads for implementing pipe-styled processing
|
|
5
|
+
Project-URL: Repository, https://github.com/katunilya/wird
|
|
6
|
+
Project-URL: Issues, https://github.com/katunilya/wird/issues
|
|
7
|
+
Author-email: Ilya Katun <katun.ilya@gmail.com>
|
|
8
|
+
Maintainer-email: Ilya Katun <katun.ilya@gmail.com>
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# wird
|
|
13
|
+
|
|
14
|
+
`wird` is a library that provides basic monads in python. Core idea is to provide
|
|
15
|
+
mechanics for writing purely python pipeline-styled code.
|
|
16
|
+
|
|
17
|
+
> **Why wird?** Wird is a misspelling of Anglo-Saxon / Old North word "wyrd". It means
|
|
18
|
+
> fate, but not totally predefined, more like a consequence of previous deeds.
|
|
19
|
+
|
|
20
|
+
## Pipelines
|
|
21
|
+
|
|
22
|
+
Before getting into `wird` API it's worth explaining concept of pipeline-styled code.
|
|
23
|
+
Mainly our code is imperative - we describe what we do to achieve some result in steps,
|
|
24
|
+
one by one. It's not worth to reject imperative code in favor of declarative one (where
|
|
25
|
+
we describe the result instead of steps for getting it), as most languages are generally
|
|
26
|
+
imperative and it's more convenient to provide better ways to write it.
|
|
27
|
+
|
|
28
|
+
Different languages provide pipelines in different forms. For example in C# or Java it
|
|
29
|
+
is provided with so called Fluent API (sometimes method chaining). Example:
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
int[] numbers = [ 5, 10, 8, 3, 6, 12 ];
|
|
33
|
+
|
|
34
|
+
IEnumerable<int> evenNumbersSorted = numbers
|
|
35
|
+
.Where(num => num % 2 == 0)
|
|
36
|
+
.OrderBy(num => num);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
There we write some class that allows us to chain method execution in order to perform
|
|
40
|
+
some action. This is quite nice approach, however it's not really extensible and does
|
|
41
|
+
not suit to most of the business cases where we want to separate bits of logic into
|
|
42
|
+
different entities.
|
|
43
|
+
|
|
44
|
+
Mostly this kind of syntax is used for builder pattern:
|
|
45
|
+
|
|
46
|
+
```csharp
|
|
47
|
+
var host = new WebHostBuilder()
|
|
48
|
+
.UseKestrel()
|
|
49
|
+
.UseContentRoot(Directory.GetCurrentDirectory())
|
|
50
|
+
.UseStartup<Startup>()
|
|
51
|
+
.Build();
|
|
52
|
+
|
|
53
|
+
host.Run();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In functional languages you can find so called "pipe operator" - `|>`. Let's take a look
|
|
57
|
+
at simple case - we want to put to square some number, that convert that to string and
|
|
58
|
+
reverse it. In F# you might write that like:
|
|
59
|
+
|
|
60
|
+
```fsharp
|
|
61
|
+
let result = rev (toStr (square 512))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Problem of this piece of code is that despite or algorithm is simple and direct, when we
|
|
65
|
+
write code it steps are written in reverse order and we need to "unwrap" function calls.
|
|
66
|
+
|
|
67
|
+
With pipe operator same code becomes much more elegant:
|
|
68
|
+
|
|
69
|
+
```fsharp
|
|
70
|
+
let result = 512
|
|
71
|
+
|> square
|
|
72
|
+
|> toStr
|
|
73
|
+
|> rev
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
All actions are written one-by-one in the same order as they executed. This is much more
|
|
77
|
+
readable code.
|
|
78
|
+
|
|
79
|
+
Basically `wird` is written to provide this mechanic to python language in some
|
|
80
|
+
opinionated form inspired by Rust language.
|
|
81
|
+
|
|
82
|
+
## Monads
|
|
83
|
+
|
|
84
|
+
### `Value`
|
|
85
|
+
|
|
86
|
+
Container for sync value that provides pipe-styled execution of arbitrary functions.
|
|
87
|
+
Let's look at the example:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import operator
|
|
91
|
+
|
|
92
|
+
from wird import Value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
res = (
|
|
96
|
+
Value(3)
|
|
97
|
+
.map(operator.add, 1) # 3 + 1 -> 4
|
|
98
|
+
.map(operator.mul, 3) # 4 * 3 -> 12
|
|
99
|
+
.map(operator.truediv, 2) # 12 / 2 -> 6
|
|
100
|
+
.inspect(print) # print 6.0 & pass next
|
|
101
|
+
.unwrap(as_type=int) # extract 6.0 from container
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`Value` is a simple wrapper around passed value with special methods (`map` /
|
|
106
|
+
`map_async` / `inspect` / `inspect_async`) that bind passed function to container value
|
|
107
|
+
(read as invoke / apply). Thus it is basically is a simplest monad.
|
|
108
|
+
|
|
109
|
+
`Value` provides the following interface:
|
|
110
|
+
|
|
111
|
+
- `Value.unwrap` - method for extracting internally stored value with optional type
|
|
112
|
+
casting (only for type checker, not actual casting happens)
|
|
113
|
+
- `Value.map` - binding method for sync functions
|
|
114
|
+
- `Value.map_async` - binding method for async functions
|
|
115
|
+
- `Value.inspect` - binding method for sync side-effect functions
|
|
116
|
+
- `Value.inspect_async` - binding method for async side-effect functions
|
|
117
|
+
|
|
118
|
+
Main different between `map` and `inspect` is that `map` wraps the result of the
|
|
119
|
+
executed function into `Value` container and `inspect` just invokes function passing
|
|
120
|
+
stored value next. If stored value is mutable, `inspect` can be used to modify it via
|
|
121
|
+
side effect.
|
|
122
|
+
|
|
123
|
+
### `Future`
|
|
124
|
+
|
|
125
|
+
Container for async values. It is similar to `Value` and provides nearly the same
|
|
126
|
+
interface. When we invoke any of async methods in `Value` we actually return `Future`
|
|
127
|
+
container, as now stored value is computed asynchronously and requires `await`.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import operator
|
|
131
|
+
|
|
132
|
+
from wird import Value
|
|
133
|
+
|
|
134
|
+
async def mul_async(x: int, y: int) -> int:
|
|
135
|
+
return x * y
|
|
136
|
+
|
|
137
|
+
async def truediv_async(x: int, y: int) -> float:
|
|
138
|
+
return x / y
|
|
139
|
+
|
|
140
|
+
async def main():
|
|
141
|
+
res = await (
|
|
142
|
+
Value(3)
|
|
143
|
+
.map(operator.add, 1) # 3 + 1 -> 4 (Value)
|
|
144
|
+
.map_async(mul_async, 3) # 4 * 3 -> 12 (Future)
|
|
145
|
+
.map_async(truediv_async, 2) # 12 / 2 -> 6.0 (Future)
|
|
146
|
+
.inspect(print) # print 6.0 & pass next (Future)
|
|
147
|
+
.unwrap() # extract awaitable 6.0 from container
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
import asyncio
|
|
152
|
+
asyncio.run(main())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`Future` provides the following interface:
|
|
156
|
+
|
|
157
|
+
- `Future.unwrap` - extract internally stored awaitable value
|
|
158
|
+
- `Future.map` - binding method for sync functions
|
|
159
|
+
- `Future.map_async` - binding method for async functions
|
|
160
|
+
- `Future.inspect` - binding method for sync side-effect functions
|
|
161
|
+
- `Future.inspect_async` - binding method for async side-effect functions
|
|
162
|
+
- `Future.from_` - static method for creating awaitable object from sync value
|
|
163
|
+
|
|
164
|
+
Also `Future` is awaitable by itself, so one can just await `Future` itself instead of
|
|
165
|
+
calling `Future.unwrap`, but to stay uniform it is recommended to use `Future.unwrap`.
|
|
166
|
+
|
|
167
|
+
### `Maybe`
|
|
168
|
+
|
|
169
|
+
Despite `Value` and `Future`, `Maybe` is not a single container, but rather a pair of
|
|
170
|
+
containers - `Some` and `Empty`. Each resembles additional property of data - its
|
|
171
|
+
presence.
|
|
172
|
+
|
|
173
|
+
`Some` indicates that data is present allowing it to be processed. `Empty` on the other
|
|
174
|
+
hand marks that there is not data and we can't perform execution ignoring that.
|
|
175
|
+
Basically it hides explicit `is None` checks, taking it as internal rule of function
|
|
176
|
+
mapping.
|
|
177
|
+
|
|
178
|
+
Possible relevant case of usage is database patch / update operations, when we
|
|
179
|
+
intentionally want to provide some abstract interface that allows optional column
|
|
180
|
+
update. For example we store in SQL database following data structure:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from dataclasses import dataclass
|
|
184
|
+
from datetime import date
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class Customer:
|
|
189
|
+
uid: int
|
|
190
|
+
first_name: str
|
|
191
|
+
second_name: str
|
|
192
|
+
birthdate: date | None = None
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
We provide HTTP route to update this entity in DB. If we've provided a field in request
|
|
196
|
+
body, then this field must be updated. Commonly one will make each field in DTO (except
|
|
197
|
+
for ID) optional with default `None` value, but what to do with `birthdate`? When
|
|
198
|
+
parsing we will propagate default `None` so we do not know if this `None` was passed
|
|
199
|
+
explicitly or we've implicitly set it via DTO default.
|
|
200
|
+
|
|
201
|
+
`Maybe` allows to explicitly separate this cases, allowing us to have `None` as present value:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from dataclasses import dataclass
|
|
205
|
+
from datetime import date
|
|
206
|
+
|
|
207
|
+
from wird import Empty, Maybe
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass
|
|
211
|
+
class CustomerUpdate:
|
|
212
|
+
uid: int
|
|
213
|
+
first_name: Maybe[str] = Empty()
|
|
214
|
+
second_name: Maybe[str] = Empty()
|
|
215
|
+
birthdate: Maybe[date | None] = Empty()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Thus when `birthdate` is `Empty` we know that we do not have to update this column at
|
|
219
|
+
all, and when it is `Some` we can safely set `None` as desired value.
|
|
220
|
+
|
|
221
|
+
`Maybe` provides the following interface:
|
|
222
|
+
|
|
223
|
+
- `Maybe.unwrap` - extract internally stored value on `Some`, raise `EmptyUnwrapError`
|
|
224
|
+
on `Empty`
|
|
225
|
+
- `Maybe.unwrap_or` - extract internally stored value on `Some` or return passed
|
|
226
|
+
replacement value on `Empty`
|
|
227
|
+
- `Maybe.unwrap_or_none` - extract internally stored value on `Some` or return `None`
|
|
228
|
+
on `Empty`
|
|
229
|
+
- `Maybe.unwrap_or_else` - extract internally stored value on `Some` or return result of
|
|
230
|
+
execution of factory function for replacement value on `Empty`
|
|
231
|
+
- `Maybe.unwrap_or_else_async` - same as `Maybe.unwrap_or_else`, but for async factory
|
|
232
|
+
function
|
|
233
|
+
- `Maybe.map` - binding method for sync functions, applies only on `Some`
|
|
234
|
+
- `Maybe.map_async` - same as `Maybe.map`, but for async functions
|
|
235
|
+
- `Maybe.inspect` - binding method for sync side-effect functions, applies only on
|
|
236
|
+
`Some`
|
|
237
|
+
- `Maybe.inspect_async` - same as `Maybe.inspect`, but for async functions
|
|
238
|
+
- `Maybe.and_` - logical AND for 2 `Maybe` values, replaces self `Maybe` with passed
|
|
239
|
+
`Maybe` if first one is `Some`
|
|
240
|
+
- `Maybe.and_then` - same as `Maybe.map`, but for sync functions that return `Maybe`
|
|
241
|
+
- `Maybe.and_then_async` - same as `Maybe.and_then`, but for async functions
|
|
242
|
+
- `Maybe.or_` - logical OR for 2 `Maybe` values, replaces self `Maybe` with passed
|
|
243
|
+
`Maybe` if first one is `Empty`
|
|
244
|
+
- `Maybe.or_else` - replaces `Empty` with `Maybe` result of passed sync function
|
|
245
|
+
- `Maybe.or_else_async` - same as `Maybe.or_else`, but for async functions
|
|
246
|
+
- `Maybe.is_some` - `True` on `Some` container
|
|
247
|
+
- `Maybe.is_some_and` - `True` on `Some` container and passed predicate being `True`
|
|
248
|
+
- `Maybe.is_some_and_async` - same as `Maybe.is_some_and`, but for async predicates
|
|
249
|
+
- `Maybe.is_empty` - `True` on `Empty` container
|
|
250
|
+
- `Maybe.is_empty_or` - `True` on `Empty` container or passed predicate being `True`
|
|
251
|
+
- `Maybe.is_empty_or_async` - same as `Maybe.is_empty_or`, but for async predicates
|
|
252
|
+
- `Maybe.filter` - if predicate is `False` replaces `Maybe` with `Empty`
|
|
253
|
+
- `Maybe.filter_async` - same as `Maybe.filter`, but for async predicates
|
|
254
|
+
|
|
255
|
+
In order to provide seamless experience, instead of making developer to work with
|
|
256
|
+
`Future[Maybe[T]]` we provide `FutureMaybe` container that provides exactly the same
|
|
257
|
+
interface as sync `Maybe`. Worth noting that `FutureMaybe` is awaitable, like `Future`,
|
|
258
|
+
and returns internally stored `Maybe` instance.
|
|
259
|
+
|
|
260
|
+
Also in some cases one might need point-free versions of `Maybe` interface methods, so
|
|
261
|
+
one can access them via `maybe` module. For `FutureMaybe` point-free functions one can
|
|
262
|
+
use `future_maybe` module.
|
|
263
|
+
|
|
264
|
+
### `Result`
|
|
265
|
+
|
|
266
|
+
Exception handling is one of the most important tasks in development. We often face
|
|
267
|
+
cases when invocation of some logic can lead to Exception raise. In python default
|
|
268
|
+
handling mechanism is `try` - `except` - `finally` block, which is actually just another
|
|
269
|
+
for of `if` statement.
|
|
270
|
+
|
|
271
|
+
Worst thing about this approach is that commonly in python the only way to know that
|
|
272
|
+
function can raise an exception is documentation (which is not always written or written
|
|
273
|
+
good). There is no explicit mechanism to tell LSP / type checker / linter, that this
|
|
274
|
+
specific function needs exception handling.
|
|
275
|
+
|
|
276
|
+
`Result` monad provides another approach, which is common for Rust and Go developers -
|
|
277
|
+
let's return exceptions instead of raising them. Thus we can explicitly tell that soma
|
|
278
|
+
action can fail and requires edge-case handling.
|
|
279
|
+
|
|
280
|
+
Like `Maybe`, `Result` is just a protocol and has 2 implementations:
|
|
281
|
+
|
|
282
|
+
- `Ok` - container indicating that calculation succeeded
|
|
283
|
+
- `Err` - container indicating that calculation failed
|
|
284
|
+
|
|
285
|
+
Simplest case of using `Result` is division:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from wird import Result, Ok, Err
|
|
289
|
+
|
|
290
|
+
def try_div(a: int, b: int) -> Result[float, ZeroDivisionError]:
|
|
291
|
+
if b == 0:
|
|
292
|
+
return Err(ZeroDivisionError())
|
|
293
|
+
|
|
294
|
+
return Ok(a / b)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
There we explicitly tell that division operation can lead to failure and even pinpoint
|
|
298
|
+
specific type of error.
|
|
299
|
+
|
|
300
|
+
`Result` provides the following interface:
|
|
301
|
+
|
|
302
|
+
- `Result.unwrap` - extract internally stored value of `Ok` or raise `ErrUnwrapError`
|
|
303
|
+
- `Result.unwrap_or` - extract internally stored value of `Ok` or return other
|
|
304
|
+
- `Result.unwrap_or_else` - extract internally stored value of `Ok` or return closure
|
|
305
|
+
result
|
|
306
|
+
- `Result.unwrap_or_else_async` - same as `Result.unwrap_or_else`, but for async
|
|
307
|
+
closures
|
|
308
|
+
- `Result.unwrap_err` - same as `Result.unwrap`, but for `Err`
|
|
309
|
+
- `Result.unwrap_err_or` - same as `Result.unwrap_err_or`, but for `Err`
|
|
310
|
+
- `Result.unwrap_err_or_else` - same as `Result.unwrap_err_or_else`, but for `Err`
|
|
311
|
+
- `Result.unwrap_err_or_else_async` - same as `Result.unwrap_err_or_else_async`, but for
|
|
312
|
+
`Err`
|
|
313
|
+
- `Result.map` - binding method for `Ok`
|
|
314
|
+
- `Result.map_async` - same as `Result.map`, but for async functions
|
|
315
|
+
- `Result.inspect` - binding side-effect method for `Ok`
|
|
316
|
+
- `Result.inspect_async`- same as `Result.inspect_async`, but for async functions
|
|
317
|
+
- `Result.map_err` - same as `Result.map`, but for `Err`
|
|
318
|
+
- `Result.map_err_async` - same as `Result.map_async`, but for `Err`
|
|
319
|
+
- `Result.inspect_err` - same as `Result.inspect`, but for `Err`
|
|
320
|
+
- `Result.inspect_err_async` - same as `Result.inspect_async`, but for `Err`
|
|
321
|
+
- `Result.and_` - logical AND, replaces current `Result` with passed on `Ok`
|
|
322
|
+
- `Result.and_then` - same as `Result.map`, but for functions returning `Result`
|
|
323
|
+
- `Result.and_then_async` - same as `Result.and_then`, but for async functions
|
|
324
|
+
- `Result.or_` - logical OR, replaces current `Result` with passed on `Err`
|
|
325
|
+
- `Result.or_else` - same as `Result.map_err`, but for functions returning `Result`
|
|
326
|
+
- `Result.or_else_async` - same as `Result.or_else`, but for async functions
|
|
327
|
+
- `Result.is_ok` - `True` on `Ok`
|
|
328
|
+
- `Result.is_ok_and` - `True` on `Ok` and predicate `True`
|
|
329
|
+
- `Result.is_ok_and_async` - same as `Result.is_ok_and`, but for async predicate
|
|
330
|
+
- `Result.is_ok_or` - `True` on `Ok` or `Err` predicate `True`
|
|
331
|
+
- `Result.is_ok_or_async` - same as `Result.is_ok_or`, but for async predicate
|
|
332
|
+
- `Result.is_err` - `True` on `Err`
|
|
333
|
+
- `Result.is_err_and` - `True` on `Err` and predicate `True`
|
|
334
|
+
- `Result.is_err_and_async` - same as `Result.is_err_and`, but for async predicate
|
|
335
|
+
- `Result.is_err_or` - `True` on `Err` or `Ok` predicate `True
|
|
336
|
+
- `Result.is_err_or_async` - same as `result.is_err_or`, but for async predicate
|
|
337
|
+
|
|
338
|
+
In the same manner as with `Maybe` we wird provides:
|
|
339
|
+
|
|
340
|
+
- `FutureResult` as seamless adapter for `Future[Result]`
|
|
341
|
+
- point-free `Result` API in `wird.result` module
|
|
342
|
+
- point-free `FutureResult` API in `wird.future_result` module
|
wird-1.2.0/README.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# wird
|
|
2
|
+
|
|
3
|
+
`wird` is a library that provides basic monads in python. Core idea is to provide
|
|
4
|
+
mechanics for writing purely python pipeline-styled code.
|
|
5
|
+
|
|
6
|
+
> **Why wird?** Wird is a misspelling of Anglo-Saxon / Old North word "wyrd". It means
|
|
7
|
+
> fate, but not totally predefined, more like a consequence of previous deeds.
|
|
8
|
+
|
|
9
|
+
## Pipelines
|
|
10
|
+
|
|
11
|
+
Before getting into `wird` API it's worth explaining concept of pipeline-styled code.
|
|
12
|
+
Mainly our code is imperative - we describe what we do to achieve some result in steps,
|
|
13
|
+
one by one. It's not worth to reject imperative code in favor of declarative one (where
|
|
14
|
+
we describe the result instead of steps for getting it), as most languages are generally
|
|
15
|
+
imperative and it's more convenient to provide better ways to write it.
|
|
16
|
+
|
|
17
|
+
Different languages provide pipelines in different forms. For example in C# or Java it
|
|
18
|
+
is provided with so called Fluent API (sometimes method chaining). Example:
|
|
19
|
+
|
|
20
|
+
```csharp
|
|
21
|
+
int[] numbers = [ 5, 10, 8, 3, 6, 12 ];
|
|
22
|
+
|
|
23
|
+
IEnumerable<int> evenNumbersSorted = numbers
|
|
24
|
+
.Where(num => num % 2 == 0)
|
|
25
|
+
.OrderBy(num => num);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
There we write some class that allows us to chain method execution in order to perform
|
|
29
|
+
some action. This is quite nice approach, however it's not really extensible and does
|
|
30
|
+
not suit to most of the business cases where we want to separate bits of logic into
|
|
31
|
+
different entities.
|
|
32
|
+
|
|
33
|
+
Mostly this kind of syntax is used for builder pattern:
|
|
34
|
+
|
|
35
|
+
```csharp
|
|
36
|
+
var host = new WebHostBuilder()
|
|
37
|
+
.UseKestrel()
|
|
38
|
+
.UseContentRoot(Directory.GetCurrentDirectory())
|
|
39
|
+
.UseStartup<Startup>()
|
|
40
|
+
.Build();
|
|
41
|
+
|
|
42
|
+
host.Run();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In functional languages you can find so called "pipe operator" - `|>`. Let's take a look
|
|
46
|
+
at simple case - we want to put to square some number, that convert that to string and
|
|
47
|
+
reverse it. In F# you might write that like:
|
|
48
|
+
|
|
49
|
+
```fsharp
|
|
50
|
+
let result = rev (toStr (square 512))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Problem of this piece of code is that despite or algorithm is simple and direct, when we
|
|
54
|
+
write code it steps are written in reverse order and we need to "unwrap" function calls.
|
|
55
|
+
|
|
56
|
+
With pipe operator same code becomes much more elegant:
|
|
57
|
+
|
|
58
|
+
```fsharp
|
|
59
|
+
let result = 512
|
|
60
|
+
|> square
|
|
61
|
+
|> toStr
|
|
62
|
+
|> rev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All actions are written one-by-one in the same order as they executed. This is much more
|
|
66
|
+
readable code.
|
|
67
|
+
|
|
68
|
+
Basically `wird` is written to provide this mechanic to python language in some
|
|
69
|
+
opinionated form inspired by Rust language.
|
|
70
|
+
|
|
71
|
+
## Monads
|
|
72
|
+
|
|
73
|
+
### `Value`
|
|
74
|
+
|
|
75
|
+
Container for sync value that provides pipe-styled execution of arbitrary functions.
|
|
76
|
+
Let's look at the example:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import operator
|
|
80
|
+
|
|
81
|
+
from wird import Value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
res = (
|
|
85
|
+
Value(3)
|
|
86
|
+
.map(operator.add, 1) # 3 + 1 -> 4
|
|
87
|
+
.map(operator.mul, 3) # 4 * 3 -> 12
|
|
88
|
+
.map(operator.truediv, 2) # 12 / 2 -> 6
|
|
89
|
+
.inspect(print) # print 6.0 & pass next
|
|
90
|
+
.unwrap(as_type=int) # extract 6.0 from container
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`Value` is a simple wrapper around passed value with special methods (`map` /
|
|
95
|
+
`map_async` / `inspect` / `inspect_async`) that bind passed function to container value
|
|
96
|
+
(read as invoke / apply). Thus it is basically is a simplest monad.
|
|
97
|
+
|
|
98
|
+
`Value` provides the following interface:
|
|
99
|
+
|
|
100
|
+
- `Value.unwrap` - method for extracting internally stored value with optional type
|
|
101
|
+
casting (only for type checker, not actual casting happens)
|
|
102
|
+
- `Value.map` - binding method for sync functions
|
|
103
|
+
- `Value.map_async` - binding method for async functions
|
|
104
|
+
- `Value.inspect` - binding method for sync side-effect functions
|
|
105
|
+
- `Value.inspect_async` - binding method for async side-effect functions
|
|
106
|
+
|
|
107
|
+
Main different between `map` and `inspect` is that `map` wraps the result of the
|
|
108
|
+
executed function into `Value` container and `inspect` just invokes function passing
|
|
109
|
+
stored value next. If stored value is mutable, `inspect` can be used to modify it via
|
|
110
|
+
side effect.
|
|
111
|
+
|
|
112
|
+
### `Future`
|
|
113
|
+
|
|
114
|
+
Container for async values. It is similar to `Value` and provides nearly the same
|
|
115
|
+
interface. When we invoke any of async methods in `Value` we actually return `Future`
|
|
116
|
+
container, as now stored value is computed asynchronously and requires `await`.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import operator
|
|
120
|
+
|
|
121
|
+
from wird import Value
|
|
122
|
+
|
|
123
|
+
async def mul_async(x: int, y: int) -> int:
|
|
124
|
+
return x * y
|
|
125
|
+
|
|
126
|
+
async def truediv_async(x: int, y: int) -> float:
|
|
127
|
+
return x / y
|
|
128
|
+
|
|
129
|
+
async def main():
|
|
130
|
+
res = await (
|
|
131
|
+
Value(3)
|
|
132
|
+
.map(operator.add, 1) # 3 + 1 -> 4 (Value)
|
|
133
|
+
.map_async(mul_async, 3) # 4 * 3 -> 12 (Future)
|
|
134
|
+
.map_async(truediv_async, 2) # 12 / 2 -> 6.0 (Future)
|
|
135
|
+
.inspect(print) # print 6.0 & pass next (Future)
|
|
136
|
+
.unwrap() # extract awaitable 6.0 from container
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
import asyncio
|
|
141
|
+
asyncio.run(main())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`Future` provides the following interface:
|
|
145
|
+
|
|
146
|
+
- `Future.unwrap` - extract internally stored awaitable value
|
|
147
|
+
- `Future.map` - binding method for sync functions
|
|
148
|
+
- `Future.map_async` - binding method for async functions
|
|
149
|
+
- `Future.inspect` - binding method for sync side-effect functions
|
|
150
|
+
- `Future.inspect_async` - binding method for async side-effect functions
|
|
151
|
+
- `Future.from_` - static method for creating awaitable object from sync value
|
|
152
|
+
|
|
153
|
+
Also `Future` is awaitable by itself, so one can just await `Future` itself instead of
|
|
154
|
+
calling `Future.unwrap`, but to stay uniform it is recommended to use `Future.unwrap`.
|
|
155
|
+
|
|
156
|
+
### `Maybe`
|
|
157
|
+
|
|
158
|
+
Despite `Value` and `Future`, `Maybe` is not a single container, but rather a pair of
|
|
159
|
+
containers - `Some` and `Empty`. Each resembles additional property of data - its
|
|
160
|
+
presence.
|
|
161
|
+
|
|
162
|
+
`Some` indicates that data is present allowing it to be processed. `Empty` on the other
|
|
163
|
+
hand marks that there is not data and we can't perform execution ignoring that.
|
|
164
|
+
Basically it hides explicit `is None` checks, taking it as internal rule of function
|
|
165
|
+
mapping.
|
|
166
|
+
|
|
167
|
+
Possible relevant case of usage is database patch / update operations, when we
|
|
168
|
+
intentionally want to provide some abstract interface that allows optional column
|
|
169
|
+
update. For example we store in SQL database following data structure:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from dataclasses import dataclass
|
|
173
|
+
from datetime import date
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class Customer:
|
|
178
|
+
uid: int
|
|
179
|
+
first_name: str
|
|
180
|
+
second_name: str
|
|
181
|
+
birthdate: date | None = None
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
We provide HTTP route to update this entity in DB. If we've provided a field in request
|
|
185
|
+
body, then this field must be updated. Commonly one will make each field in DTO (except
|
|
186
|
+
for ID) optional with default `None` value, but what to do with `birthdate`? When
|
|
187
|
+
parsing we will propagate default `None` so we do not know if this `None` was passed
|
|
188
|
+
explicitly or we've implicitly set it via DTO default.
|
|
189
|
+
|
|
190
|
+
`Maybe` allows to explicitly separate this cases, allowing us to have `None` as present value:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from dataclasses import dataclass
|
|
194
|
+
from datetime import date
|
|
195
|
+
|
|
196
|
+
from wird import Empty, Maybe
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class CustomerUpdate:
|
|
201
|
+
uid: int
|
|
202
|
+
first_name: Maybe[str] = Empty()
|
|
203
|
+
second_name: Maybe[str] = Empty()
|
|
204
|
+
birthdate: Maybe[date | None] = Empty()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Thus when `birthdate` is `Empty` we know that we do not have to update this column at
|
|
208
|
+
all, and when it is `Some` we can safely set `None` as desired value.
|
|
209
|
+
|
|
210
|
+
`Maybe` provides the following interface:
|
|
211
|
+
|
|
212
|
+
- `Maybe.unwrap` - extract internally stored value on `Some`, raise `EmptyUnwrapError`
|
|
213
|
+
on `Empty`
|
|
214
|
+
- `Maybe.unwrap_or` - extract internally stored value on `Some` or return passed
|
|
215
|
+
replacement value on `Empty`
|
|
216
|
+
- `Maybe.unwrap_or_none` - extract internally stored value on `Some` or return `None`
|
|
217
|
+
on `Empty`
|
|
218
|
+
- `Maybe.unwrap_or_else` - extract internally stored value on `Some` or return result of
|
|
219
|
+
execution of factory function for replacement value on `Empty`
|
|
220
|
+
- `Maybe.unwrap_or_else_async` - same as `Maybe.unwrap_or_else`, but for async factory
|
|
221
|
+
function
|
|
222
|
+
- `Maybe.map` - binding method for sync functions, applies only on `Some`
|
|
223
|
+
- `Maybe.map_async` - same as `Maybe.map`, but for async functions
|
|
224
|
+
- `Maybe.inspect` - binding method for sync side-effect functions, applies only on
|
|
225
|
+
`Some`
|
|
226
|
+
- `Maybe.inspect_async` - same as `Maybe.inspect`, but for async functions
|
|
227
|
+
- `Maybe.and_` - logical AND for 2 `Maybe` values, replaces self `Maybe` with passed
|
|
228
|
+
`Maybe` if first one is `Some`
|
|
229
|
+
- `Maybe.and_then` - same as `Maybe.map`, but for sync functions that return `Maybe`
|
|
230
|
+
- `Maybe.and_then_async` - same as `Maybe.and_then`, but for async functions
|
|
231
|
+
- `Maybe.or_` - logical OR for 2 `Maybe` values, replaces self `Maybe` with passed
|
|
232
|
+
`Maybe` if first one is `Empty`
|
|
233
|
+
- `Maybe.or_else` - replaces `Empty` with `Maybe` result of passed sync function
|
|
234
|
+
- `Maybe.or_else_async` - same as `Maybe.or_else`, but for async functions
|
|
235
|
+
- `Maybe.is_some` - `True` on `Some` container
|
|
236
|
+
- `Maybe.is_some_and` - `True` on `Some` container and passed predicate being `True`
|
|
237
|
+
- `Maybe.is_some_and_async` - same as `Maybe.is_some_and`, but for async predicates
|
|
238
|
+
- `Maybe.is_empty` - `True` on `Empty` container
|
|
239
|
+
- `Maybe.is_empty_or` - `True` on `Empty` container or passed predicate being `True`
|
|
240
|
+
- `Maybe.is_empty_or_async` - same as `Maybe.is_empty_or`, but for async predicates
|
|
241
|
+
- `Maybe.filter` - if predicate is `False` replaces `Maybe` with `Empty`
|
|
242
|
+
- `Maybe.filter_async` - same as `Maybe.filter`, but for async predicates
|
|
243
|
+
|
|
244
|
+
In order to provide seamless experience, instead of making developer to work with
|
|
245
|
+
`Future[Maybe[T]]` we provide `FutureMaybe` container that provides exactly the same
|
|
246
|
+
interface as sync `Maybe`. Worth noting that `FutureMaybe` is awaitable, like `Future`,
|
|
247
|
+
and returns internally stored `Maybe` instance.
|
|
248
|
+
|
|
249
|
+
Also in some cases one might need point-free versions of `Maybe` interface methods, so
|
|
250
|
+
one can access them via `maybe` module. For `FutureMaybe` point-free functions one can
|
|
251
|
+
use `future_maybe` module.
|
|
252
|
+
|
|
253
|
+
### `Result`
|
|
254
|
+
|
|
255
|
+
Exception handling is one of the most important tasks in development. We often face
|
|
256
|
+
cases when invocation of some logic can lead to Exception raise. In python default
|
|
257
|
+
handling mechanism is `try` - `except` - `finally` block, which is actually just another
|
|
258
|
+
for of `if` statement.
|
|
259
|
+
|
|
260
|
+
Worst thing about this approach is that commonly in python the only way to know that
|
|
261
|
+
function can raise an exception is documentation (which is not always written or written
|
|
262
|
+
good). There is no explicit mechanism to tell LSP / type checker / linter, that this
|
|
263
|
+
specific function needs exception handling.
|
|
264
|
+
|
|
265
|
+
`Result` monad provides another approach, which is common for Rust and Go developers -
|
|
266
|
+
let's return exceptions instead of raising them. Thus we can explicitly tell that soma
|
|
267
|
+
action can fail and requires edge-case handling.
|
|
268
|
+
|
|
269
|
+
Like `Maybe`, `Result` is just a protocol and has 2 implementations:
|
|
270
|
+
|
|
271
|
+
- `Ok` - container indicating that calculation succeeded
|
|
272
|
+
- `Err` - container indicating that calculation failed
|
|
273
|
+
|
|
274
|
+
Simplest case of using `Result` is division:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from wird import Result, Ok, Err
|
|
278
|
+
|
|
279
|
+
def try_div(a: int, b: int) -> Result[float, ZeroDivisionError]:
|
|
280
|
+
if b == 0:
|
|
281
|
+
return Err(ZeroDivisionError())
|
|
282
|
+
|
|
283
|
+
return Ok(a / b)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
There we explicitly tell that division operation can lead to failure and even pinpoint
|
|
287
|
+
specific type of error.
|
|
288
|
+
|
|
289
|
+
`Result` provides the following interface:
|
|
290
|
+
|
|
291
|
+
- `Result.unwrap` - extract internally stored value of `Ok` or raise `ErrUnwrapError`
|
|
292
|
+
- `Result.unwrap_or` - extract internally stored value of `Ok` or return other
|
|
293
|
+
- `Result.unwrap_or_else` - extract internally stored value of `Ok` or return closure
|
|
294
|
+
result
|
|
295
|
+
- `Result.unwrap_or_else_async` - same as `Result.unwrap_or_else`, but for async
|
|
296
|
+
closures
|
|
297
|
+
- `Result.unwrap_err` - same as `Result.unwrap`, but for `Err`
|
|
298
|
+
- `Result.unwrap_err_or` - same as `Result.unwrap_err_or`, but for `Err`
|
|
299
|
+
- `Result.unwrap_err_or_else` - same as `Result.unwrap_err_or_else`, but for `Err`
|
|
300
|
+
- `Result.unwrap_err_or_else_async` - same as `Result.unwrap_err_or_else_async`, but for
|
|
301
|
+
`Err`
|
|
302
|
+
- `Result.map` - binding method for `Ok`
|
|
303
|
+
- `Result.map_async` - same as `Result.map`, but for async functions
|
|
304
|
+
- `Result.inspect` - binding side-effect method for `Ok`
|
|
305
|
+
- `Result.inspect_async`- same as `Result.inspect_async`, but for async functions
|
|
306
|
+
- `Result.map_err` - same as `Result.map`, but for `Err`
|
|
307
|
+
- `Result.map_err_async` - same as `Result.map_async`, but for `Err`
|
|
308
|
+
- `Result.inspect_err` - same as `Result.inspect`, but for `Err`
|
|
309
|
+
- `Result.inspect_err_async` - same as `Result.inspect_async`, but for `Err`
|
|
310
|
+
- `Result.and_` - logical AND, replaces current `Result` with passed on `Ok`
|
|
311
|
+
- `Result.and_then` - same as `Result.map`, but for functions returning `Result`
|
|
312
|
+
- `Result.and_then_async` - same as `Result.and_then`, but for async functions
|
|
313
|
+
- `Result.or_` - logical OR, replaces current `Result` with passed on `Err`
|
|
314
|
+
- `Result.or_else` - same as `Result.map_err`, but for functions returning `Result`
|
|
315
|
+
- `Result.or_else_async` - same as `Result.or_else`, but for async functions
|
|
316
|
+
- `Result.is_ok` - `True` on `Ok`
|
|
317
|
+
- `Result.is_ok_and` - `True` on `Ok` and predicate `True`
|
|
318
|
+
- `Result.is_ok_and_async` - same as `Result.is_ok_and`, but for async predicate
|
|
319
|
+
- `Result.is_ok_or` - `True` on `Ok` or `Err` predicate `True`
|
|
320
|
+
- `Result.is_ok_or_async` - same as `Result.is_ok_or`, but for async predicate
|
|
321
|
+
- `Result.is_err` - `True` on `Err`
|
|
322
|
+
- `Result.is_err_and` - `True` on `Err` and predicate `True`
|
|
323
|
+
- `Result.is_err_and_async` - same as `Result.is_err_and`, but for async predicate
|
|
324
|
+
- `Result.is_err_or` - `True` on `Err` or `Ok` predicate `True
|
|
325
|
+
- `Result.is_err_or_async` - same as `result.is_err_or`, but for async predicate
|
|
326
|
+
|
|
327
|
+
In the same manner as with `Maybe` we wird provides:
|
|
328
|
+
|
|
329
|
+
- `FutureResult` as seamless adapter for `Future[Result]`
|
|
330
|
+
- point-free `Result` API in `wird.result` module
|
|
331
|
+
- point-free `FutureResult` API in `wird.future_result` module
|