OSlash 2.0.0__tar.gz → 3.0.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.
- oslash-3.0.0/.github/dependabot.yml +22 -0
- {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/ci.yml +6 -6
- oslash-3.0.0/.github/workflows/conventional-pr-title.yml +27 -0
- {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/python-publish.yml +1 -1
- {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/release-please.yml +2 -2
- {oslash-2.0.0 → oslash-3.0.0}/.pre-commit-config.yaml +3 -2
- oslash-3.0.0/.release-please-manifest.json +3 -0
- {oslash-2.0.0 → oslash-3.0.0}/CHANGELOG.md +24 -0
- {oslash-2.0.0 → oslash-3.0.0}/CLAUDE.md +33 -34
- {oslash-2.0.0 → oslash-3.0.0}/PKG-INFO +60 -8
- {oslash-2.0.0 → oslash-3.0.0}/README.md +58 -6
- oslash-3.0.0/examples/hello.py +28 -0
- {oslash-2.0.0 → oslash-3.0.0}/notebooks/Reader.ipynb +9 -9
- {oslash-2.0.0 → oslash-3.0.0}/oslash/__init__.py +5 -2
- {oslash-2.0.0 → oslash-3.0.0}/oslash/do.py +22 -13
- {oslash-2.0.0 → oslash-3.0.0}/oslash/either.py +37 -13
- {oslash-2.0.0 → oslash-3.0.0}/oslash/identity.py +9 -11
- {oslash-2.0.0 → oslash-3.0.0}/oslash/ioaction.py +12 -14
- {oslash-2.0.0 → oslash-3.0.0}/oslash/list.py +30 -15
- {oslash-2.0.0 → oslash-3.0.0}/oslash/maybe.py +26 -13
- {oslash-2.0.0 → oslash-3.0.0}/oslash/monadic.py +1 -1
- {oslash-2.0.0 → oslash-3.0.0}/oslash/reader.py +15 -10
- {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/applicative.py +5 -9
- {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/functor.py +5 -4
- {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/monad.py +8 -9
- oslash-3.0.0/oslash/util/apply.py +28 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/util/numerals.py +7 -3
- oslash-3.0.0/oslash/validation.py +100 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/writer.py +56 -31
- {oslash-2.0.0 → oslash-3.0.0}/pyproject.toml +2 -2
- oslash-3.0.0/tests/__init__.py +0 -0
- oslash-3.0.0/tests/test_apply.py +11 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_cont.py +4 -4
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_do.py +4 -2
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_either.py +12 -1
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_identity.py +9 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_list.py +1 -1
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_observable.py +1 -1
- oslash-3.0.0/tests/test_read_file.py +42 -0
- oslash-3.0.0/tests/test_validation.py +57 -0
- oslash-3.0.0/tests/test_writer.py +74 -0
- oslash-3.0.0/tests/typing_contracts.py +170 -0
- {oslash-2.0.0 → oslash-3.0.0}/uv.lock +62 -49
- oslash-2.0.0/.release-please-manifest.json +0 -3
- oslash-2.0.0/examples/hello.py +0 -18
- oslash-2.0.0/tests/test_writer.py +0 -45
- {oslash-2.0.0 → oslash-3.0.0}/.gitattributes +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/.github/release-please-config.json +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/.gitignore +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/LICENSE +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/examples/hello2.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/examples/hello3.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/examples/issue.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/notebooks/README.md +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/cont.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/observable.py +0 -0
- /oslash-2.0.0/tests/__init__.py → /oslash-3.0.0/oslash/py.typed +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/state.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/__init__.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/monoid.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/util/__init__.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/util/basic.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/oslash/util/fn.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/release-please-config.json +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_ioaction.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_maybe.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_monad.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_numerals.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_reader.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_state.py +0 -0
- {oslash-2.0.0 → oslash-3.0.0}/tests/test_util.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "uv"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
commit-message:
|
|
8
|
+
prefix: "chore"
|
|
9
|
+
|
|
10
|
+
- package-ecosystem: "pre-commit"
|
|
11
|
+
directory: "/"
|
|
12
|
+
schedule:
|
|
13
|
+
interval: "weekly"
|
|
14
|
+
commit-message:
|
|
15
|
+
prefix: "chore"
|
|
16
|
+
|
|
17
|
+
- package-ecosystem: "github-actions"
|
|
18
|
+
directory: "/"
|
|
19
|
+
schedule:
|
|
20
|
+
interval: "weekly"
|
|
21
|
+
commit-message:
|
|
22
|
+
prefix: "chore"
|
|
@@ -10,8 +10,8 @@ jobs:
|
|
|
10
10
|
pre-commit:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
14
|
-
- uses: actions/setup-python@
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: actions/setup-python@v7
|
|
15
15
|
with:
|
|
16
16
|
python-version: '3.12'
|
|
17
17
|
- name: Install uv
|
|
@@ -28,10 +28,10 @@ jobs:
|
|
|
28
28
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
29
29
|
python-version: ['3.12', '3.13', '3.14']
|
|
30
30
|
steps:
|
|
31
|
-
- uses: actions/checkout@
|
|
31
|
+
- uses: actions/checkout@v7
|
|
32
32
|
|
|
33
33
|
- name: Set up Python ${{ matrix.python-version }}
|
|
34
|
-
uses: actions/setup-python@
|
|
34
|
+
uses: actions/setup-python@v7
|
|
35
35
|
with:
|
|
36
36
|
python-version: ${{ matrix.python-version }}
|
|
37
37
|
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
run: uv run pytest --cov=oslash --cov-report=xml --cov-report=term
|
|
46
46
|
|
|
47
47
|
- name: Upload coverage to Codecov
|
|
48
|
-
uses: codecov/codecov-action@
|
|
48
|
+
uses: codecov/codecov-action@v7
|
|
49
49
|
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
|
|
50
50
|
with:
|
|
51
51
|
file: ./coverage.xml
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
type-check:
|
|
55
55
|
runs-on: ubuntu-latest
|
|
56
56
|
steps:
|
|
57
|
-
- uses: actions/checkout@
|
|
57
|
+
- uses: actions/checkout@v7
|
|
58
58
|
|
|
59
59
|
- name: Install uv
|
|
60
60
|
uses: astral-sh/setup-uv@v7
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: conventional-pr-title
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, edited, synchronize, reopened]
|
|
6
|
+
branches: [master]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: amannn/action-semantic-pull-request@v6
|
|
13
|
+
env:
|
|
14
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
15
|
+
with:
|
|
16
|
+
types: |
|
|
17
|
+
feat
|
|
18
|
+
fix
|
|
19
|
+
chore
|
|
20
|
+
docs
|
|
21
|
+
style
|
|
22
|
+
refactor
|
|
23
|
+
perf
|
|
24
|
+
test
|
|
25
|
+
ci
|
|
26
|
+
build
|
|
27
|
+
revert
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
release-please:
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
steps:
|
|
16
|
-
- uses: googleapis/release-please-action@
|
|
16
|
+
- uses: googleapis/release-please-action@v5
|
|
17
17
|
id: release
|
|
18
18
|
with:
|
|
19
19
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
# Publish to PyPI when a release is created
|
|
22
22
|
- name: Checkout code
|
|
23
23
|
if: ${{ steps.release.outputs.release_created }}
|
|
24
|
-
uses: actions/checkout@
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
25
|
|
|
26
26
|
- name: Install uv
|
|
27
27
|
if: ${{ steps.release.outputs.release_created }}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
-
rev: v0.
|
|
3
|
+
rev: v0.17.4
|
|
4
4
|
hooks:
|
|
5
5
|
- id: ruff
|
|
6
6
|
args: [--fix]
|
|
7
7
|
- id: ruff-format
|
|
8
|
+
types_or: [python, pyi]
|
|
8
9
|
|
|
9
10
|
- repo: local
|
|
10
11
|
hooks:
|
|
@@ -16,7 +17,7 @@ repos:
|
|
|
16
17
|
pass_filenames: false
|
|
17
18
|
|
|
18
19
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
19
|
-
rev:
|
|
20
|
+
rev: v6.0.0
|
|
20
21
|
hooks:
|
|
21
22
|
- id: trailing-whitespace
|
|
22
23
|
- id: end-of-file-fixer
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.0](https://github.com/dbrattli/OSlash/compare/oslash-v2.0.0...oslash-v3.0.0) (2026-11-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* align functor and writer factory typing contracts ([#74](https://github.com/dbrattli/OSlash/issues/74))
|
|
9
|
+
* correct public typing contracts ([#67](https://github.com/dbrattli/OSlash/issues/67))
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add error-accumulating validation ([#68](https://github.com/dbrattli/OSlash/issues/68)) ([cec0c6b](https://github.com/dbrattli/OSlash/commit/cec0c6bcfa1be30dca627e2b031b12b7a53df149))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* align functor and writer factory typing contracts ([#74](https://github.com/dbrattli/OSlash/issues/74)) ([f7c7790](https://github.com/dbrattli/OSlash/commit/f7c7790d8f277106726b008c7564e73b5bcf47cd))
|
|
19
|
+
* correct public typing contracts ([#67](https://github.com/dbrattli/OSlash/issues/67)) ([bae0e4d](https://github.com/dbrattli/OSlash/commit/bae0e4da0aa6182f1451b61ed3833aa9cfe4b851))
|
|
20
|
+
* preserve file input when mapping IO actions ([#50](https://github.com/dbrattli/OSlash/issues/50)) ([18000ca](https://github.com/dbrattli/OSlash/commit/18000ca6977fe0e4d8ab3f0a339caa502480f2d4))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
* align Reader tutorial and API descriptions with current interfaces ([#51](https://github.com/dbrattli/OSlash/issues/51)) ([ce7a93c](https://github.com/dbrattli/OSlash/commit/ce7a93c3e934868ae5d56fb92a0e6031b568b5bf))
|
|
26
|
+
|
|
3
27
|
## [2.0.0](https://github.com/dbrattli/OSlash/compare/oslash-v1.0.0...oslash-v2.0.0) (2025-11-17)
|
|
4
28
|
|
|
5
29
|
|
|
@@ -51,8 +51,8 @@ Located in [oslash/typing/](oslash/typing/), these define the "laws" of function
|
|
|
51
51
|
|
|
52
52
|
```python
|
|
53
53
|
@runtime_checkable
|
|
54
|
-
class Functor[T](Protocol):
|
|
55
|
-
def map
|
|
54
|
+
class Functor[T, U, Result](Protocol):
|
|
55
|
+
def map(self, fn: Callable[[T], U], /) -> Result:
|
|
56
56
|
"""Transform values inside the container."""
|
|
57
57
|
...
|
|
58
58
|
```
|
|
@@ -65,13 +65,13 @@ class Functor[T](Protocol):
|
|
|
65
65
|
|
|
66
66
|
```python
|
|
67
67
|
@runtime_checkable
|
|
68
|
-
class Applicative[T](Protocol):
|
|
69
|
-
def apply
|
|
68
|
+
class Applicative[T, Arg, Result](Protocol):
|
|
69
|
+
def apply(self, something: Arg, /) -> Result:
|
|
70
70
|
"""Apply a wrapped function to wrapped values."""
|
|
71
71
|
...
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
|
-
def pure(cls, value: T) ->
|
|
74
|
+
def pure(cls, value: T, /) -> Applicative[T, Arg, Result]:
|
|
75
75
|
"""Wrap a pure value."""
|
|
76
76
|
...
|
|
77
77
|
```
|
|
@@ -82,13 +82,13 @@ class Applicative[T](Protocol):
|
|
|
82
82
|
|
|
83
83
|
```python
|
|
84
84
|
@runtime_checkable
|
|
85
|
-
class Monad[T](Protocol):
|
|
86
|
-
def bind
|
|
85
|
+
class Monad[T, Result](Protocol):
|
|
86
|
+
def bind(self, fn: Callable[[T], Result], /) -> Result:
|
|
87
87
|
"""Chain computations that produce wrapped values."""
|
|
88
88
|
...
|
|
89
89
|
|
|
90
90
|
@classmethod
|
|
91
|
-
def unit(cls, value: T) ->
|
|
91
|
+
def unit(cls, value: T, /) -> Monad[T, Result]:
|
|
92
92
|
"""Wrap a pure value."""
|
|
93
93
|
...
|
|
94
94
|
```
|
|
@@ -294,12 +294,12 @@ def compose[T](*funcs: Callable[[T], T]) -> Callable[[T], T]:
|
|
|
294
294
|
|
|
295
295
|
identity: Callable[[T], T] = compose()
|
|
296
296
|
|
|
297
|
-
def fmap[
|
|
298
|
-
"""
|
|
299
|
-
return
|
|
297
|
+
def fmap[A, B, C](f: Callable[[B], C], g: Callable[[A], B]) -> Callable[[A], C]:
|
|
298
|
+
"""Map a function over a function (function composition)."""
|
|
299
|
+
return compose(f, g)
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
**Note**: Uses 7 `@overload` signatures
|
|
302
|
+
**Note**: Uses 7 `@overload` signatures for 0-6 arguments.
|
|
303
303
|
|
|
304
304
|
#### [basic.py](oslash/util/basic.py) - Basic Utilities
|
|
305
305
|
|
|
@@ -328,10 +328,10 @@ succ: Callable[[ChurchNumeral[T]], ChurchNumeral[T]] = ...
|
|
|
328
328
|
### [Kleisli Composition](oslash/monadic.py)
|
|
329
329
|
|
|
330
330
|
```python
|
|
331
|
-
def compose[A, B,
|
|
332
|
-
f: Callable[[B],
|
|
333
|
-
g: Callable[[A], Monad[B]]
|
|
334
|
-
) -> Callable[[A],
|
|
331
|
+
def compose[A, B, Result](
|
|
332
|
+
f: Callable[[B], Result],
|
|
333
|
+
g: Callable[[A], Monad[B, Result]]
|
|
334
|
+
) -> Callable[[A], Result]:
|
|
335
335
|
"""Compose monadic functions."""
|
|
336
336
|
return lambda x: g(x).bind(f)
|
|
337
337
|
```
|
|
@@ -385,7 +385,9 @@ Continuation: TypeAlias = Callable[[T], R] # T, R must be defined elsewhere
|
|
|
385
385
|
|
|
386
386
|
### When `Any` is Acceptable
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
Keep `Any` at dynamic implementation boundaries and expose precise public types.
|
|
389
|
+
The `apply` overloads, for example, describe unary and two-argument callables
|
|
390
|
+
while their shared implementation accepts both shapes.
|
|
389
391
|
|
|
390
392
|
1. **Higher-kinded types aren't supported**:
|
|
391
393
|
|
|
@@ -394,21 +396,23 @@ We minimize `Any` usage (<10 instances), but it's justified when:
|
|
|
394
396
|
type ListSelector[T] = Callable[[T, List[T]], Any] # Any: Return type varies
|
|
395
397
|
```
|
|
396
398
|
|
|
397
|
-
2. **Partial application
|
|
399
|
+
2. **Partial application across callable arities**:
|
|
398
400
|
|
|
399
401
|
```python
|
|
400
|
-
#
|
|
401
|
-
|
|
402
|
+
# Public overloads return either U or Callable[[Next], U].
|
|
403
|
+
# Only the shared implementation handles Callable[..., Any].
|
|
404
|
+
result = apply_callable(func, arg1)
|
|
402
405
|
```
|
|
403
406
|
|
|
404
|
-
3. **Protocol
|
|
407
|
+
3. **Protocol implementation boundaries**:
|
|
405
408
|
|
|
406
409
|
```python
|
|
407
|
-
#
|
|
408
|
-
def
|
|
410
|
+
# Keep a type-erased implementation behind precise overloads.
|
|
411
|
+
def apply(self: Identity[Callable[..., Any]], something: Identity[Any]) -> Identity[Any]:
|
|
412
|
+
...
|
|
409
413
|
```
|
|
410
414
|
|
|
411
|
-
|
|
415
|
+
Document why `Any` is needed at its implementation boundary:
|
|
412
416
|
|
|
413
417
|
```python
|
|
414
418
|
value: Any # Any: [Brief explanation of why static types insufficient]
|
|
@@ -579,14 +583,9 @@ pythonVersion = "3.12"
|
|
|
579
583
|
3. **Protocol implementation**:
|
|
580
584
|
|
|
581
585
|
```python
|
|
582
|
-
#
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
... # Must implement
|
|
586
|
-
|
|
587
|
-
@classmethod
|
|
588
|
-
def unit(cls, value: T) -> Self:
|
|
589
|
-
... # Must implement
|
|
586
|
+
# Check the public structural contract from caller code.
|
|
587
|
+
value: Monad[int, Identity[str]] = Identity(1)
|
|
588
|
+
result: Identity[str] = value.bind(lambda x: Identity(str(x)))
|
|
590
589
|
```
|
|
591
590
|
|
|
592
591
|
### Linting and Formatting
|
|
@@ -841,8 +840,8 @@ OSlash uses **Protocol** instead of **ABC** for type definitions. Here's why:
|
|
|
841
840
|
|
|
842
841
|
```python
|
|
843
842
|
@runtime_checkable
|
|
844
|
-
class Functor[T](Protocol):
|
|
845
|
-
def map
|
|
843
|
+
class Functor[T, U, Result](Protocol):
|
|
844
|
+
def map(self, fn: Callable[[T], U], /) -> Result: ...
|
|
846
845
|
|
|
847
846
|
# Any class with a map method is a Functor - no inheritance needed
|
|
848
847
|
class MyClass[T]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: oslash
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Functional library for Functors, Applicatives, and Monads in Python 3.12+
|
|
5
5
|
Project-URL: Homepage, https://github.com/dbrattli/oslash
|
|
6
6
|
Project-URL: Repository, https://github.com/dbrattli/oslash
|
|
@@ -56,8 +56,8 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
OSlash (Ø) is a library for learning and understanding functional programming in Python 3.12+. It re-implements
|
|
58
58
|
concepts from [Learn You a Haskell for Great Good!](http://learnyouahaskell.com/) using Python with modern type annotations. OSlash unifies
|
|
59
|
-
functional and object-oriented paradigms by grouping related functions within classes.
|
|
60
|
-
|
|
59
|
+
functional and object-oriented paradigms by grouping related functions within classes. Most monads store wrapped
|
|
60
|
+
values or computations on their instances; the immutable linked list stores its elements within function closures.
|
|
61
61
|
|
|
62
62
|
## ✨ What's New in 1.0
|
|
63
63
|
|
|
@@ -71,7 +71,7 @@ for storing values or mutable data; data exists only within function closures.
|
|
|
71
71
|
**Type Safety**: OSlash is fully type-checked with Pyright in strict mode, providing excellent IDE support and catching errors at development time. It leverages Python 3.12's PEP 695 type parameter syntax for clean, ergonomic generic types.
|
|
72
72
|
|
|
73
73
|
OSlash is intended to be a tutorial. For practical functional programming in Python in production environments you
|
|
74
|
-
should
|
|
74
|
+
should consider using [Expression](https://github.com/dbrattli/Expression) instead.
|
|
75
75
|
|
|
76
76
|
## Install
|
|
77
77
|
|
|
@@ -85,7 +85,7 @@ uv add oslash
|
|
|
85
85
|
|
|
86
86
|
The project currently contains implementations for:
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## Protocols
|
|
89
89
|
|
|
90
90
|
- **[Functor](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#functors)**, for stuff that can be mapped
|
|
91
91
|
- **[Applicative](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#applicatives)**, for callable stuff
|
|
@@ -104,11 +104,63 @@ The project currently contains implementations for:
|
|
|
104
104
|
- **State**, for stateful computations of stuff
|
|
105
105
|
- **Cont**, for continuation of stuff
|
|
106
106
|
|
|
107
|
+
## Validation
|
|
108
|
+
|
|
109
|
+
`Validation` is an applicative for independent checks. `Either` stops at the first
|
|
110
|
+
`Left`, while `Validation.apply` collects errors in argument order. Convert an
|
|
111
|
+
`Either` with `Validation.from_either`; a `Left` becomes a singleton OSlash `List`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from oslash import Left, List, Validation, or_else
|
|
115
|
+
|
|
116
|
+
name = Validation.from_either(Left("name is missing"))
|
|
117
|
+
age = Validation.from_either(Left("age is invalid"))
|
|
118
|
+
result = Validation.pure(lambda name, age: (name, age)).apply(name).apply(age)
|
|
119
|
+
assert result.to_either() == Left(List(["name is missing", "age is invalid"]))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `or_else(first, fallback)` for a choice between two `Either` results. It
|
|
123
|
+
keeps the first `Right`; if that fails, it returns the fallback, including its
|
|
124
|
+
error when both fail. Both arguments are evaluated before the call.
|
|
125
|
+
|
|
126
|
+
## Typing
|
|
127
|
+
|
|
128
|
+
The concrete classes keep the wrapped result type through `map`, `bind`, and `apply`.
|
|
129
|
+
The structural protocols describe a particular operation and its result container:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from collections.abc import Callable
|
|
133
|
+
from oslash import Identity
|
|
134
|
+
from oslash.typing import Applicative, Functor, Monad
|
|
135
|
+
|
|
136
|
+
functor: Functor[int, str, Identity[str]] = Identity(1)
|
|
137
|
+
mapped: Identity[str] = functor.map(str)
|
|
138
|
+
|
|
139
|
+
monad: Monad[int, Identity[str]] = Identity(1)
|
|
140
|
+
result: Identity[str] = monad.bind(lambda value: Identity(str(value)))
|
|
141
|
+
|
|
142
|
+
fn: Callable[[int], str] = str
|
|
143
|
+
applicative: Applicative[Callable[[int], str], Identity[int], Identity[str]] = Identity(fn)
|
|
144
|
+
applied: Identity[str] = applicative.apply(Identity(1))
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`Monad` now takes the input value type and the concrete bind result type.
|
|
148
|
+
`Applicative` takes the wrapped function type, argument container type, and result container type.
|
|
149
|
+
`Functor[T, U, Result]` takes the input value type, mapped value type, and output container type.
|
|
150
|
+
This replaces the previous `Functor[T]` protocol, which only supported same-type mappings.
|
|
151
|
+
|
|
152
|
+
`Writer.map` maps the value and keeps its log. Use `Writer.map_with_log` to transform both.
|
|
153
|
+
`Writer.unit(value)` uses a string log; for another log type, pass `empty_log` or use `Writer.create`.
|
|
154
|
+
Factories expose `WriterFactory[Log]`: their `unit(value)` always uses the configured log identity
|
|
155
|
+
and does not accept `empty_log`. For example, `Writer.create("IntWriter", int).unit(1)` returns
|
|
156
|
+
`Writer[int, int]`. Annotate instances as `Writer[T, Log]`, rather than using a dynamic factory
|
|
157
|
+
name such as `StringWriter` as a type. Import `WriterFactory` from `oslash.writer` when needed.
|
|
158
|
+
`apply` has typed overloads for unary and two-argument functions; applying the first argument of a
|
|
159
|
+
two-argument function returns a wrapped one-argument function.
|
|
160
|
+
|
|
107
161
|
## Monadic functions
|
|
108
162
|
|
|
109
163
|
- **>>**, for sequencing monadic actions
|
|
110
|
-
- **lift**, for mapping a function over monadic values
|
|
111
|
-
- **join**, for removing one level of monadic structure
|
|
112
164
|
- **compose**, for composing monadic functions
|
|
113
165
|
|
|
114
166
|
## Utility functions
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
OSlash (Ø) is a library for learning and understanding functional programming in Python 3.12+. It re-implements
|
|
9
9
|
concepts from [Learn You a Haskell for Great Good!](http://learnyouahaskell.com/) using Python with modern type annotations. OSlash unifies
|
|
10
|
-
functional and object-oriented paradigms by grouping related functions within classes.
|
|
11
|
-
|
|
10
|
+
functional and object-oriented paradigms by grouping related functions within classes. Most monads store wrapped
|
|
11
|
+
values or computations on their instances; the immutable linked list stores its elements within function closures.
|
|
12
12
|
|
|
13
13
|
## ✨ What's New in 1.0
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ for storing values or mutable data; data exists only within function closures.
|
|
|
22
22
|
**Type Safety**: OSlash is fully type-checked with Pyright in strict mode, providing excellent IDE support and catching errors at development time. It leverages Python 3.12's PEP 695 type parameter syntax for clean, ergonomic generic types.
|
|
23
23
|
|
|
24
24
|
OSlash is intended to be a tutorial. For practical functional programming in Python in production environments you
|
|
25
|
-
should
|
|
25
|
+
should consider using [Expression](https://github.com/dbrattli/Expression) instead.
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ uv add oslash
|
|
|
36
36
|
|
|
37
37
|
The project currently contains implementations for:
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Protocols
|
|
40
40
|
|
|
41
41
|
- **[Functor](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#functors)**, for stuff that can be mapped
|
|
42
42
|
- **[Applicative](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#applicatives)**, for callable stuff
|
|
@@ -55,11 +55,63 @@ The project currently contains implementations for:
|
|
|
55
55
|
- **State**, for stateful computations of stuff
|
|
56
56
|
- **Cont**, for continuation of stuff
|
|
57
57
|
|
|
58
|
+
## Validation
|
|
59
|
+
|
|
60
|
+
`Validation` is an applicative for independent checks. `Either` stops at the first
|
|
61
|
+
`Left`, while `Validation.apply` collects errors in argument order. Convert an
|
|
62
|
+
`Either` with `Validation.from_either`; a `Left` becomes a singleton OSlash `List`.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from oslash import Left, List, Validation, or_else
|
|
66
|
+
|
|
67
|
+
name = Validation.from_either(Left("name is missing"))
|
|
68
|
+
age = Validation.from_either(Left("age is invalid"))
|
|
69
|
+
result = Validation.pure(lambda name, age: (name, age)).apply(name).apply(age)
|
|
70
|
+
assert result.to_either() == Left(List(["name is missing", "age is invalid"]))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `or_else(first, fallback)` for a choice between two `Either` results. It
|
|
74
|
+
keeps the first `Right`; if that fails, it returns the fallback, including its
|
|
75
|
+
error when both fail. Both arguments are evaluated before the call.
|
|
76
|
+
|
|
77
|
+
## Typing
|
|
78
|
+
|
|
79
|
+
The concrete classes keep the wrapped result type through `map`, `bind`, and `apply`.
|
|
80
|
+
The structural protocols describe a particular operation and its result container:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from collections.abc import Callable
|
|
84
|
+
from oslash import Identity
|
|
85
|
+
from oslash.typing import Applicative, Functor, Monad
|
|
86
|
+
|
|
87
|
+
functor: Functor[int, str, Identity[str]] = Identity(1)
|
|
88
|
+
mapped: Identity[str] = functor.map(str)
|
|
89
|
+
|
|
90
|
+
monad: Monad[int, Identity[str]] = Identity(1)
|
|
91
|
+
result: Identity[str] = monad.bind(lambda value: Identity(str(value)))
|
|
92
|
+
|
|
93
|
+
fn: Callable[[int], str] = str
|
|
94
|
+
applicative: Applicative[Callable[[int], str], Identity[int], Identity[str]] = Identity(fn)
|
|
95
|
+
applied: Identity[str] = applicative.apply(Identity(1))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`Monad` now takes the input value type and the concrete bind result type.
|
|
99
|
+
`Applicative` takes the wrapped function type, argument container type, and result container type.
|
|
100
|
+
`Functor[T, U, Result]` takes the input value type, mapped value type, and output container type.
|
|
101
|
+
This replaces the previous `Functor[T]` protocol, which only supported same-type mappings.
|
|
102
|
+
|
|
103
|
+
`Writer.map` maps the value and keeps its log. Use `Writer.map_with_log` to transform both.
|
|
104
|
+
`Writer.unit(value)` uses a string log; for another log type, pass `empty_log` or use `Writer.create`.
|
|
105
|
+
Factories expose `WriterFactory[Log]`: their `unit(value)` always uses the configured log identity
|
|
106
|
+
and does not accept `empty_log`. For example, `Writer.create("IntWriter", int).unit(1)` returns
|
|
107
|
+
`Writer[int, int]`. Annotate instances as `Writer[T, Log]`, rather than using a dynamic factory
|
|
108
|
+
name such as `StringWriter` as a type. Import `WriterFactory` from `oslash.writer` when needed.
|
|
109
|
+
`apply` has typed overloads for unary and two-argument functions; applying the first argument of a
|
|
110
|
+
two-argument function returns a wrapped one-argument function.
|
|
111
|
+
|
|
58
112
|
## Monadic functions
|
|
59
113
|
|
|
60
114
|
- **>>**, for sequencing monadic actions
|
|
61
|
-
- **lift**, for mapping a function over monadic values
|
|
62
|
-
- **join**, for removing one level of monadic structure
|
|
63
115
|
- **compose**, for composing monadic functions
|
|
64
116
|
|
|
65
117
|
## Utility functions
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Hello example using bind."""
|
|
2
|
+
|
|
3
|
+
from oslash import get_line, put_line
|
|
4
|
+
|
|
5
|
+
main = put_line("What is your name?") | (
|
|
6
|
+
lambda _: (
|
|
7
|
+
get_line()
|
|
8
|
+
| (
|
|
9
|
+
lambda name: (
|
|
10
|
+
put_line("What is your age?")
|
|
11
|
+
| (
|
|
12
|
+
lambda _: (
|
|
13
|
+
get_line()
|
|
14
|
+
| (
|
|
15
|
+
lambda age: (
|
|
16
|
+
put_line("Hello " + name + "!") | (lambda _: put_line("You are " + age + " years old"))
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
# print(main)
|
|
28
|
+
main()
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"\n",
|
|
11
11
|
"The Reader is just a fancy name for a wrapped function, so this monad could also be called the Function monad, or perhaps the Callable monad. Reader is all about composing wrapped functions.\n",
|
|
12
12
|
"\n",
|
|
13
|
-
"This [IPython](http://ipython.org) notebook uses the [OSlash](https://github.com/dbrattli/OSlash) library for Python 3.
|
|
13
|
+
"This [IPython](http://ipython.org) notebook uses the [OSlash](https://github.com/dbrattli/OSlash) library for Python 3.12+, aka Ø. You can install Ø using:\n",
|
|
14
14
|
"\n",
|
|
15
15
|
"```bash\n",
|
|
16
16
|
"> pip3 install oslash\n",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"source": [
|
|
116
116
|
"## Bind\n",
|
|
117
117
|
"\n",
|
|
118
|
-
"You can bind a Reader to a monadic function using
|
|
118
|
+
"You can bind a Reader to a monadic function using `.bind()` (The bind operator is called `>>=` in Haskell). A monadic function is a function that takes a value and returns a monad, and in this case it returns a new Reader monad:"
|
|
119
119
|
]
|
|
120
120
|
},
|
|
121
121
|
{
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"source": [
|
|
140
140
|
"r = Reader(lambda name: f\"Hi {name}!\")\n",
|
|
141
141
|
"\n",
|
|
142
|
-
"b = r
|
|
142
|
+
"b = r.bind(lambda x: unit(x.replace(\"Hi\", \"Hello\")))\n",
|
|
143
143
|
"b(\"Dag\")"
|
|
144
144
|
]
|
|
145
145
|
},
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
"source": [
|
|
150
150
|
"## Applicative\n",
|
|
151
151
|
"\n",
|
|
152
|
-
"Apply (
|
|
152
|
+
"Apply (`.apply()`) is a beefed up `map`. It takes a Reader that has a function in it and another Reader, and extracts that function from the first Reader and then maps it over the second one (basically composes the two functions)."
|
|
153
153
|
]
|
|
154
154
|
},
|
|
155
155
|
{
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"source": [
|
|
174
174
|
"r = Reader(lambda name: f\"Hi {name}!\")\n",
|
|
175
175
|
"\n",
|
|
176
|
-
"a = Reader.pure(lambda x: x + \"!!!\")
|
|
176
|
+
"a = Reader.pure(lambda x: x + \"!!!\").apply(r)\n",
|
|
177
177
|
"a(\"Dag\")"
|
|
178
178
|
]
|
|
179
179
|
},
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
}
|
|
229
229
|
],
|
|
230
230
|
"source": [
|
|
231
|
-
"r = ask()
|
|
231
|
+
"r = ask().bind(lambda x: unit(f\"Hi {x}!\"))\n",
|
|
232
232
|
"r(\"Dag\")"
|
|
233
233
|
]
|
|
234
234
|
},
|
|
@@ -295,15 +295,15 @@
|
|
|
295
295
|
"\n",
|
|
296
296
|
"\n",
|
|
297
297
|
"def hello():\n",
|
|
298
|
-
" return ask()
|
|
298
|
+
" return ask().bind(lambda name: unit(\"Hello, \" + name + \"!\"))\n",
|
|
299
299
|
"\n",
|
|
300
300
|
"\n",
|
|
301
301
|
"def bye():\n",
|
|
302
|
-
" return ask()
|
|
302
|
+
" return ask().bind(lambda name: unit(\"Bye, \" + name + \"!\"))\n",
|
|
303
303
|
"\n",
|
|
304
304
|
"\n",
|
|
305
305
|
"def convo():\n",
|
|
306
|
-
" return hello()
|
|
306
|
+
" return hello().bind(lambda c1: bye().bind(lambda c2: unit(f\"{c1} {c2}\")))\n",
|
|
307
307
|
"\n",
|
|
308
308
|
"\n",
|
|
309
309
|
"r = convo()\n",
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
# Monads
|
|
9
9
|
from .cont import Cont
|
|
10
10
|
from .do import do, guard, let
|
|
11
|
-
from .either import Either, Left, Right
|
|
11
|
+
from .either import Either, Left, Right, or_else
|
|
12
12
|
from .identity import Identity
|
|
13
13
|
from .ioaction import IO, Get, Put, ReadFile, Return, get_line, put_line, read_file
|
|
14
14
|
from .list import List
|
|
@@ -23,10 +23,11 @@ from .state import State
|
|
|
23
23
|
# Protocols
|
|
24
24
|
from .typing import Applicative, Functor, Monad, Monoid
|
|
25
25
|
from .util import Unit, compose, fmap, identity, indent
|
|
26
|
+
from .validation import Validation
|
|
26
27
|
from .writer import MonadWriter, StringWriter, Writer
|
|
27
28
|
|
|
28
29
|
# Version will be managed by release-please
|
|
29
|
-
__version__ = "
|
|
30
|
+
__version__ = "3.0.0"
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"IO",
|
|
@@ -54,6 +55,7 @@ __all__ = [
|
|
|
54
55
|
"State",
|
|
55
56
|
"StringWriter",
|
|
56
57
|
"Unit",
|
|
58
|
+
"Validation",
|
|
57
59
|
"Writer",
|
|
58
60
|
"compose",
|
|
59
61
|
"do",
|
|
@@ -64,6 +66,7 @@ __all__ = [
|
|
|
64
66
|
"indent",
|
|
65
67
|
"let",
|
|
66
68
|
"monadic_compose",
|
|
69
|
+
"or_else",
|
|
67
70
|
"put_line",
|
|
68
71
|
"read_file",
|
|
69
72
|
]
|