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.
Files changed (71) hide show
  1. oslash-3.0.0/.github/dependabot.yml +22 -0
  2. {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/ci.yml +6 -6
  3. oslash-3.0.0/.github/workflows/conventional-pr-title.yml +27 -0
  4. {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/python-publish.yml +1 -1
  5. {oslash-2.0.0 → oslash-3.0.0}/.github/workflows/release-please.yml +2 -2
  6. {oslash-2.0.0 → oslash-3.0.0}/.pre-commit-config.yaml +3 -2
  7. oslash-3.0.0/.release-please-manifest.json +3 -0
  8. {oslash-2.0.0 → oslash-3.0.0}/CHANGELOG.md +24 -0
  9. {oslash-2.0.0 → oslash-3.0.0}/CLAUDE.md +33 -34
  10. {oslash-2.0.0 → oslash-3.0.0}/PKG-INFO +60 -8
  11. {oslash-2.0.0 → oslash-3.0.0}/README.md +58 -6
  12. oslash-3.0.0/examples/hello.py +28 -0
  13. {oslash-2.0.0 → oslash-3.0.0}/notebooks/Reader.ipynb +9 -9
  14. {oslash-2.0.0 → oslash-3.0.0}/oslash/__init__.py +5 -2
  15. {oslash-2.0.0 → oslash-3.0.0}/oslash/do.py +22 -13
  16. {oslash-2.0.0 → oslash-3.0.0}/oslash/either.py +37 -13
  17. {oslash-2.0.0 → oslash-3.0.0}/oslash/identity.py +9 -11
  18. {oslash-2.0.0 → oslash-3.0.0}/oslash/ioaction.py +12 -14
  19. {oslash-2.0.0 → oslash-3.0.0}/oslash/list.py +30 -15
  20. {oslash-2.0.0 → oslash-3.0.0}/oslash/maybe.py +26 -13
  21. {oslash-2.0.0 → oslash-3.0.0}/oslash/monadic.py +1 -1
  22. {oslash-2.0.0 → oslash-3.0.0}/oslash/reader.py +15 -10
  23. {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/applicative.py +5 -9
  24. {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/functor.py +5 -4
  25. {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/monad.py +8 -9
  26. oslash-3.0.0/oslash/util/apply.py +28 -0
  27. {oslash-2.0.0 → oslash-3.0.0}/oslash/util/numerals.py +7 -3
  28. oslash-3.0.0/oslash/validation.py +100 -0
  29. {oslash-2.0.0 → oslash-3.0.0}/oslash/writer.py +56 -31
  30. {oslash-2.0.0 → oslash-3.0.0}/pyproject.toml +2 -2
  31. oslash-3.0.0/tests/__init__.py +0 -0
  32. oslash-3.0.0/tests/test_apply.py +11 -0
  33. {oslash-2.0.0 → oslash-3.0.0}/tests/test_cont.py +4 -4
  34. {oslash-2.0.0 → oslash-3.0.0}/tests/test_do.py +4 -2
  35. {oslash-2.0.0 → oslash-3.0.0}/tests/test_either.py +12 -1
  36. {oslash-2.0.0 → oslash-3.0.0}/tests/test_identity.py +9 -0
  37. {oslash-2.0.0 → oslash-3.0.0}/tests/test_list.py +1 -1
  38. {oslash-2.0.0 → oslash-3.0.0}/tests/test_observable.py +1 -1
  39. oslash-3.0.0/tests/test_read_file.py +42 -0
  40. oslash-3.0.0/tests/test_validation.py +57 -0
  41. oslash-3.0.0/tests/test_writer.py +74 -0
  42. oslash-3.0.0/tests/typing_contracts.py +170 -0
  43. {oslash-2.0.0 → oslash-3.0.0}/uv.lock +62 -49
  44. oslash-2.0.0/.release-please-manifest.json +0 -3
  45. oslash-2.0.0/examples/hello.py +0 -18
  46. oslash-2.0.0/tests/test_writer.py +0 -45
  47. {oslash-2.0.0 → oslash-3.0.0}/.gitattributes +0 -0
  48. {oslash-2.0.0 → oslash-3.0.0}/.github/release-please-config.json +0 -0
  49. {oslash-2.0.0 → oslash-3.0.0}/.gitignore +0 -0
  50. {oslash-2.0.0 → oslash-3.0.0}/LICENSE +0 -0
  51. {oslash-2.0.0 → oslash-3.0.0}/examples/hello2.py +0 -0
  52. {oslash-2.0.0 → oslash-3.0.0}/examples/hello3.py +0 -0
  53. {oslash-2.0.0 → oslash-3.0.0}/examples/issue.py +0 -0
  54. {oslash-2.0.0 → oslash-3.0.0}/notebooks/README.md +0 -0
  55. {oslash-2.0.0 → oslash-3.0.0}/oslash/cont.py +0 -0
  56. {oslash-2.0.0 → oslash-3.0.0}/oslash/observable.py +0 -0
  57. /oslash-2.0.0/tests/__init__.py → /oslash-3.0.0/oslash/py.typed +0 -0
  58. {oslash-2.0.0 → oslash-3.0.0}/oslash/state.py +0 -0
  59. {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/__init__.py +0 -0
  60. {oslash-2.0.0 → oslash-3.0.0}/oslash/typing/monoid.py +0 -0
  61. {oslash-2.0.0 → oslash-3.0.0}/oslash/util/__init__.py +0 -0
  62. {oslash-2.0.0 → oslash-3.0.0}/oslash/util/basic.py +0 -0
  63. {oslash-2.0.0 → oslash-3.0.0}/oslash/util/fn.py +0 -0
  64. {oslash-2.0.0 → oslash-3.0.0}/release-please-config.json +0 -0
  65. {oslash-2.0.0 → oslash-3.0.0}/tests/test_ioaction.py +0 -0
  66. {oslash-2.0.0 → oslash-3.0.0}/tests/test_maybe.py +0 -0
  67. {oslash-2.0.0 → oslash-3.0.0}/tests/test_monad.py +0 -0
  68. {oslash-2.0.0 → oslash-3.0.0}/tests/test_numerals.py +0 -0
  69. {oslash-2.0.0 → oslash-3.0.0}/tests/test_reader.py +0 -0
  70. {oslash-2.0.0 → oslash-3.0.0}/tests/test_state.py +0 -0
  71. {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@v4
14
- - uses: actions/setup-python@v5
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@v4
31
+ - uses: actions/checkout@v7
32
32
 
33
33
  - name: Set up Python ${{ matrix.python-version }}
34
- uses: actions/setup-python@v5
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@v4
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@v4
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
@@ -16,7 +16,7 @@ jobs:
16
16
  runs-on: ubuntu-latest
17
17
 
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v7
20
20
 
21
21
  - name: Install uv
22
22
  uses: astral-sh/setup-uv@v7
@@ -13,7 +13,7 @@ jobs:
13
13
  release-please:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: googleapis/release-please-action@v4
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@v4
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.14.5
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: v5.0.0
20
+ rev: v6.0.0
20
21
  hooks:
21
22
  - id: trailing-whitespace
22
23
  - id: end-of-file-fixer
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "3.0.0"
3
+ }
@@ -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[U](self, fn: Callable[[T], U]) -> Functor[U]:
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[U](self, something: Applicative[Callable[[T], U]]) -> Applicative[U]:
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) -> Self:
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[U](self, fn: Callable[[T], Monad[U]]) -> Monad[U]:
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) -> Self:
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[T, U](func: Callable[[T], U], functor: Functor[T]) -> Functor[U]:
298
- """Flipped version of map for partial application."""
299
- return functor.map(func)
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 to provide perfect type inference for 0-7 arguments.
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, C](
332
- f: Callable[[B], Monad[C]],
333
- g: Callable[[A], Monad[B]]
334
- ) -> Callable[[A], Monad[C]]:
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
- We minimize `Any` usage (<10 instances), but it's justified when:
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 creates ambiguity**:
399
+ 2. **Partial application across callable arities**:
398
400
 
399
401
  ```python
400
- # Partially applied function has complex intermediate type
401
- result = partial(func, arg1) # type: ignore # Partial: Complex type inference
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 limitations**:
407
+ 3. **Protocol implementation boundaries**:
405
408
 
406
409
  ```python
407
- # Protocol can't express "returns same type as receiver"
408
- def copy(self) -> Any: # Any: Protocol limitation, actual type is Self
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
- **Always add inline comment** explaining why:
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
- # Ensure all protocol methods are implemented
583
- class MyMonad[T](Monad[T]):
584
- def bind[U](self, fn: Callable[[T], Monad[U]]) -> Monad[U]:
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[U](self, fn: Callable[[T], U]) -> Functor[U]: ...
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: oslash
3
- Version: 2.0.0
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. Objects are never used
60
- for storing values or mutable data; data exists only within function closures.
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 use [Expression](https://github.com/dbrattli/Expression) instead.
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
- ## Abstract Base Classes
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. Objects are never used
11
- for storing values or mutable data; data exists only within function closures.
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 use [Expression](https://github.com/dbrattli/Expression) instead.
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
- ## Abstract Base Classes
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.4, aka Ø. You can install Ø using:\n",
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 the pipe `|` operator (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:"
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 | (lambda x: unit(x.replace(\"Hi\", \"Hello\")))\n",
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 (`*`) 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)."
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 + \"!!!\") * r\n",
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() | (lambda x: unit(f\"Hi {x}!\"))\n",
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() | (lambda name: unit(\"Hello, \" + name + \"!\"))\n",
298
+ " return ask().bind(lambda name: unit(\"Hello, \" + name + \"!\"))\n",
299
299
  "\n",
300
300
  "\n",
301
301
  "def bye():\n",
302
- " return ask() | (lambda name: unit(\"Bye, \" + name + \"!\"))\n",
302
+ " return ask().bind(lambda name: unit(\"Bye, \" + name + \"!\"))\n",
303
303
  "\n",
304
304
  "\n",
305
305
  "def convo():\n",
306
- " return hello() | (lambda c1: bye() | (lambda c2: unit(f\"{c1} {c2}\")))\n",
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__ = "2.0.0"
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
  ]