funstruct 0.1.2__tar.gz → 1.0.1__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 (106) hide show
  1. funstruct-1.0.1/.claude/settings.local.json +8 -0
  2. funstruct-1.0.1/.github/workflows/ci.yml +33 -0
  3. funstruct-1.0.1/.github/workflows/docs.yml +31 -0
  4. funstruct-1.0.1/.github/workflows/publish.yml +18 -0
  5. funstruct-1.0.1/.gitignore +136 -0
  6. funstruct-1.0.1/.pre-commit-config.yaml +30 -0
  7. funstruct-1.0.1/CONTRIBUTING.md +11 -0
  8. funstruct-1.0.1/PKG-INFO +253 -0
  9. funstruct-1.0.1/README.md +239 -0
  10. funstruct-1.0.1/benchmarks/test_bench_collections.py +151 -0
  11. funstruct-1.0.1/docs/applicative/index.md +10 -0
  12. funstruct-1.0.1/docs/applicative/validated.md +11 -0
  13. funstruct-1.0.1/docs/collections/cons.md +11 -0
  14. funstruct-1.0.1/docs/collections/frozendict.md +7 -0
  15. funstruct-1.0.1/docs/collections/index.md +10 -0
  16. funstruct-1.0.1/docs/collections/tree.md +3 -0
  17. funstruct-1.0.1/docs/functor/index.md +3 -0
  18. funstruct-1.0.1/docs/index.md +150 -0
  19. funstruct-1.0.1/docs/monad/either.md +3 -0
  20. funstruct-1.0.1/docs/monad/future.md +3 -0
  21. funstruct-1.0.1/docs/monad/index.md +15 -0
  22. funstruct-1.0.1/docs/monad/option.md +3 -0
  23. funstruct-1.0.1/docs/monad/reader.md +3 -0
  24. funstruct-1.0.1/docs/monad/result.md +3 -0
  25. funstruct-1.0.1/docs/monad/state.md +20 -0
  26. funstruct-1.0.1/docs/monad/writer.md +3 -0
  27. funstruct-1.0.1/docs/monadtransformer/either_t.md +3 -0
  28. funstruct-1.0.1/docs/monadtransformer/index.md +13 -0
  29. funstruct-1.0.1/docs/monadtransformer/option_t.md +3 -0
  30. funstruct-1.0.1/docs/monadtransformer/reader_t.md +3 -0
  31. funstruct-1.0.1/docs/monadtransformer/state_t.md +26 -0
  32. funstruct-1.0.1/docs/monadtransformer/writer_t.md +3 -0
  33. funstruct-1.0.1/docs/typeclass/applicative.md +3 -0
  34. funstruct-1.0.1/docs/typeclass/index.md +21 -0
  35. funstruct-1.0.1/docs/typeclass/monad.md +3 -0
  36. funstruct-1.0.1/docs/typeclass/monadtransformer.md +3 -0
  37. funstruct-1.0.1/docs/typeclass/monoid.md +3 -0
  38. funstruct-1.0.1/docs/typeclass/semigroup.md +3 -0
  39. funstruct-1.0.1/docs/util/tailrec.md +24 -0
  40. funstruct-1.0.1/funstruct/__init__.py +1 -0
  41. funstruct-1.0.1/funstruct/applicative/__init__.py +1 -0
  42. funstruct-1.0.1/funstruct/applicative/validated.py +169 -0
  43. funstruct-1.0.1/funstruct/collections/__init__.py +1 -0
  44. {funstruct-0.1.2/funstruct → funstruct-1.0.1/funstruct/collections}/cons.py +262 -222
  45. funstruct-1.0.1/funstruct/collections/frozendict.py +367 -0
  46. funstruct-1.0.1/funstruct/collections/tree.py +148 -0
  47. funstruct-1.0.1/funstruct/functor/__init__.py +1 -0
  48. funstruct-1.0.1/funstruct/monad/__init__.py +11 -0
  49. funstruct-1.0.1/funstruct/monad/either.py +294 -0
  50. funstruct-1.0.1/funstruct/monad/future.py +78 -0
  51. funstruct-1.0.1/funstruct/monad/option.py +270 -0
  52. funstruct-1.0.1/funstruct/monad/reader.py +105 -0
  53. funstruct-1.0.1/funstruct/monad/result.py +298 -0
  54. funstruct-1.0.1/funstruct/monad/state.py +144 -0
  55. funstruct-1.0.1/funstruct/monad/writer.py +121 -0
  56. funstruct-1.0.1/funstruct/monadtransformer/__init__.py +30 -0
  57. funstruct-1.0.1/funstruct/monadtransformer/either_t.py +185 -0
  58. funstruct-1.0.1/funstruct/monadtransformer/option_t.py +232 -0
  59. funstruct-1.0.1/funstruct/monadtransformer/reader_t.py +191 -0
  60. funstruct-1.0.1/funstruct/monadtransformer/state_t.py +202 -0
  61. funstruct-1.0.1/funstruct/monadtransformer/writer_t.py +204 -0
  62. funstruct-1.0.1/funstruct/monoid/__init__.py +21 -0
  63. funstruct-1.0.1/funstruct/py.typed +0 -0
  64. funstruct-1.0.1/funstruct/semigroup/__init__.py +19 -0
  65. funstruct-1.0.1/funstruct/typeclasses/__init__.py +17 -0
  66. funstruct-1.0.1/funstruct/typeclasses/_applicative.py +59 -0
  67. funstruct-1.0.1/funstruct/typeclasses/_functor.py +59 -0
  68. funstruct-1.0.1/funstruct/typeclasses/_monad.py +83 -0
  69. funstruct-1.0.1/funstruct/typeclasses/_monad_transformer.py +112 -0
  70. funstruct-1.0.1/funstruct/typeclasses/_monoid.py +44 -0
  71. funstruct-1.0.1/funstruct/typeclasses/_semigroup.py +36 -0
  72. funstruct-1.0.1/funstruct/util/__init__.py +1 -0
  73. funstruct-1.0.1/funstruct/util/tailrec.py +69 -0
  74. funstruct-1.0.1/justfile +61 -0
  75. funstruct-1.0.1/mise.toml +9 -0
  76. funstruct-1.0.1/mkdocs.yml +71 -0
  77. funstruct-1.0.1/noxfile.py +31 -0
  78. funstruct-1.0.1/pyproject.toml +89 -0
  79. funstruct-1.0.1/tests/__init__.py +0 -0
  80. funstruct-1.0.1/tests/laws.py +187 -0
  81. funstruct-1.0.1/tests/test_cons.py +872 -0
  82. funstruct-1.0.1/tests/test_either.py +200 -0
  83. funstruct-1.0.1/tests/test_either_t.py +245 -0
  84. funstruct-1.0.1/tests/test_frozendict.py +617 -0
  85. funstruct-1.0.1/tests/test_future.py +379 -0
  86. funstruct-1.0.1/tests/test_law_violations.py +280 -0
  87. funstruct-1.0.1/tests/test_option.py +178 -0
  88. funstruct-1.0.1/tests/test_option_t.py +244 -0
  89. funstruct-1.0.1/tests/test_reader.py +187 -0
  90. funstruct-1.0.1/tests/test_reader_t.py +293 -0
  91. funstruct-1.0.1/tests/test_result.py +62 -0
  92. funstruct-1.0.1/tests/test_state.py +203 -0
  93. funstruct-1.0.1/tests/test_state_t.py +304 -0
  94. funstruct-1.0.1/tests/test_tailrec.py +83 -0
  95. funstruct-1.0.1/tests/test_tree.py +88 -0
  96. funstruct-1.0.1/tests/test_validated.py +347 -0
  97. funstruct-1.0.1/tests/test_writer.py +199 -0
  98. funstruct-1.0.1/tests/test_writer_t.py +254 -0
  99. funstruct-1.0.1/uv.lock +1507 -0
  100. funstruct-1.0.1/uv.toml +3 -0
  101. funstruct-0.1.2/PKG-INFO +0 -28
  102. funstruct-0.1.2/README.md +0 -9
  103. funstruct-0.1.2/funstruct/frozendict.py +0 -268
  104. funstruct-0.1.2/pyproject.toml +0 -27
  105. {funstruct-0.1.2 → funstruct-1.0.1}/LICENSE +0 -0
  106. /funstruct-0.1.2/funstruct/__init__.py → /funstruct-1.0.1/docs/functor/.gitkeep +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/andrewstefanich/dev/jf/jellyfish/**)",
5
+ "Bash(make start-auth-build *)"
6
+ ]
7
+ }
8
+ }
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: astral-sh/setup-uv@v7
15
+ - run: uv sync --dev
16
+ - run: uv run ruff check funstruct/
17
+ - run: uv run ruff format --check funstruct/ tests/
18
+
19
+ typecheck:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v5
23
+ - uses: astral-sh/setup-uv@v7
24
+ - run: uv sync --dev
25
+ - run: uv run ty check funstruct/
26
+
27
+ test:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v5
31
+ - uses: astral-sh/setup-uv@v7
32
+ - run: uv sync --dev
33
+ - run: uv run pytest tests/ funstruct/ -q
@@ -0,0 +1,31 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [published]
8
+
9
+ permissions:
10
+ contents: write
11
+
12
+ jobs:
13
+ deploy:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: astral-sh/setup-uv@v7
20
+ - run: uv sync --group docs
21
+ - name: Deploy docs
22
+ run: |
23
+ git config user.name "github-actions[bot]"
24
+ git config user.email "github-actions[bot]@users.noreply.github.com"
25
+ if [ "${{ github.event_name }}" = "release" ]; then
26
+ VERSION=${{ github.event.release.tag_name }}
27
+ else
28
+ VERSION=dev
29
+ fi
30
+ uv run mike deploy "$VERSION" latest --update-aliases --push
31
+ uv run mike set-default latest --push
@@ -0,0 +1,18 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: astral-sh/setup-uv@v7
17
+ - run: uv build
18
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,136 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # personal dev configs
132
+ pyrightconfig.json
133
+ coc-settings.json
134
+ .vimspector.json
135
+ notes.txt
136
+ .benchmarks/
@@ -0,0 +1,30 @@
1
+ default_install_hook_types: [pre-commit, pre-push]
2
+
3
+ repos:
4
+ - repo: https://github.com/astral-sh/ruff-pre-commit
5
+ rev: v0.16.4
6
+ hooks:
7
+ - id: ruff-format # Run the formatter.
8
+ args:
9
+ - --config
10
+ - pyproject.toml
11
+ - id: ruff # Run the linter.
12
+ args:
13
+ - --config
14
+ - pyproject.toml
15
+ - --fix
16
+ stages: [pre-push]
17
+ - repo: https://github.com/executablebooks/mdformat
18
+ rev: 1.0.0
19
+ hooks:
20
+ - id: mdformat
21
+ additional_dependencies:
22
+ - mdformat-mkdocs
23
+ - repo: local
24
+ hooks:
25
+ - id: typecheck
26
+ name: typecheck
27
+ entry: bash -c "just check"
28
+ language: system
29
+ pass_filenames: false
30
+ stages: [pre-push]
@@ -0,0 +1,11 @@
1
+ # Contributing
2
+
3
+ This repo utilizes [just](https://github.com/casey/just), which defines a set of common
4
+ commands. Simply type `just` to see a list of available commands. These commands are for
5
+ testing, debugging, etc.
6
+
7
+ ## Formatting/Linting
8
+
9
+ This repo utilizes [ruff](https://github.com/astral-sh/ruff). Initial Setup:
10
+
11
+ - `mise trust && mise install`
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: funstruct
3
+ Version: 1.0.1
4
+ Summary: Fun & functional structures for Python
5
+ Project-URL: Homepage, https://veyga.github.io/funstruct/
6
+ Project-URL: Documentation, https://veyga.github.io/funstruct/
7
+ Project-URL: Repository, https://github.com/veyga/funstruct
8
+ Author-email: Andrew Stefanich <andrewstefanich@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cons,data structures,fp,functional,immutable
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # funstruct
16
+
17
+ A helpful collection of functional utilities.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install funstruct || uv add funstruct
23
+ ```
24
+
25
+ ## Functional Primer
26
+
27
+ ### Type Class Hierarchy
28
+
29
+ ```
30
+ Semigroup Functor
31
+ │ │
32
+ Monoid Applicative
33
+
34
+ Monad
35
+
36
+ MonadTransformer
37
+ ```
38
+
39
+ #### Diagrams
40
+
41
+ **Semigroup** — associative combine (`+` being the canonical 'combine' operation)
42
+
43
+ ```
44
+ A ─┐
45
+ ├──( + )──> A
46
+ A ─┘
47
+ ```
48
+
49
+ **Monoid** — semigroup with an identity element
50
+
51
+ ```
52
+ A ─┐
53
+ ├──( + )──> A (+ identity = A)
54
+ A ─┘
55
+ ```
56
+
57
+ **Functor** — transform the value inside a context
58
+
59
+ ```
60
+ F[A] ---( f: A -> B )---> F[B]
61
+ ```
62
+
63
+ **Applicative** — combine independent computations
64
+
65
+ ```
66
+ F[A] ─┐
67
+ ├──> F[(A, B)]
68
+ F[B] ─┘
69
+ ```
70
+
71
+ **Monad** — sequence computations that produce new contexts
72
+
73
+ ```
74
+ F[A] ---( f: A -> F[B] )---> F[B]
75
+ ```
76
+
77
+ ```python
78
+ @dataclass(frozen=True)
79
+ class Semigroup:
80
+ typ: type
81
+ combine: Callable # (A, A) -> A
82
+
83
+ @dataclass(frozen=True)
84
+ class Monoid(Semigroup):
85
+ typ: type
86
+ combine: Callable # (A, A) -> A
87
+ empty: object # identity element
88
+
89
+ class Functor(ABC):
90
+ def map(self, f) -> Functor: ...
91
+
92
+ class Applicative(Functor):
93
+ def pure(cls, value) -> Applicative: ...
94
+ def ap(self, other) -> Applicative: ...
95
+ def __add__ = ap # alias
96
+
97
+ class Monad(Applicative):
98
+ def bind(self, f) -> Monad: ...
99
+ def do(cls, gen_fn) -> Monad: ...
100
+ def __rshift__ = bind # >>
101
+
102
+ class MonadTransformer(Monad, Generic[_F, _A]):
103
+ def lift_f(cls, inner: _F) -> MonadTransformer: ...
104
+ def and_then(self, other) -> MonadTransformer: ...
105
+ ```
106
+
107
+ ```python
108
+ # Multiple semigroups for the same type:
109
+ int_add = Monoid(typ=int, combine=lambda a, b: a + b, empty=0)
110
+ int_mul = Monoid(typ=int, combine=lambda a, b: a * b, empty=1)
111
+ ```
112
+
113
+ ### ~ Scala equivalent
114
+
115
+ ```scala
116
+ trait Semigroup[A] {
117
+ def combine(x: A, y: A): A
118
+ }
119
+
120
+ trait Monoid[A] extends Semigroup[A] {
121
+ def empty: A
122
+ }
123
+
124
+ trait Functor[F[_]] {
125
+ def map[A, B](fa: F[A])(f: A => B): F[B]
126
+ }
127
+
128
+ trait Applicative[F[_]] extends Functor[F] {
129
+ def pure[A](a: A): F[A]
130
+ def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
131
+ }
132
+
133
+ trait Monad[F[_]] extends Applicative[F] {
134
+ def bind(fa: F[A])(f: A => F[B]): F[B]
135
+ }
136
+ ```
137
+
138
+ ### Implementations
139
+
140
+ | Typeclass | Implementations |
141
+ | ---------------- | -------------------------------------------- |
142
+ | Functor | Tree, frozendict, + all below |
143
+ | Applicative | Validated, + all below |
144
+ | Monad | Option, Either, State, Reader, Writer, CList |
145
+ | MonadTransformer | ReaderT, StateT, EitherT, OptionT, WriterT |
146
+
147
+ | Type | What it models |
148
+ | ------------------- | ------------------------------------------- |
149
+ | `Option[A]` | Value might not exist |
150
+ | `Either[E, A]` | Value or typed error |
151
+ | `Result[A]` (alias) | `Either[Exception, A]` + `@Try` decorator |
152
+ | `State[S, A]` | Stateful computation |
153
+ | `Reader[Ctx, A]` | Shared environment |
154
+ | `Writer[W, A]` | Accumulated output |
155
+ | `Validated[E, A]` | Error accumulation (applicative, not monad) |
156
+ | `Future[E, A]` | Lazy async + typed error |
157
+ | `CList[A]` | Persistent singly-linked list |
158
+ | `Tree[A]` | Immutable binary tree (functor only) |
159
+ | `frozendict[K, V]` | Persistent HAMT dictionary |
160
+
161
+ ### Monad Transformers
162
+
163
+ A transformer combines effects by wrapping one monad inside another.
164
+
165
+ ```
166
+ ReaderT[F, Ctx, A] = Ctx -> F[A] (environment + F's effects)
167
+ StateT[F, S, A] = S -> F[(S, A)] (state + F's effects)
168
+ EitherT[F, E, A] = F[Either[E, A]] (errors + F's effects)
169
+ OptionT[F, A] = F[Option[A]] (absence + F's effects)
170
+ WriterT[F, W, A] = F[(A, W)] (output + F's effects)
171
+ ```
172
+
173
+ **Why transformers?** Monads don't compose automatically. If you need
174
+ config + errors + logging, you'd manually unwrap 3 nested layers at
175
+ every step. Transformers flatten that into one `bind`:
176
+
177
+ ```python
178
+ # Without transformer — nested pattern matching at every step:
179
+ result = fetch_user(id) # Either[Err, Option[User]]
180
+ match result:
181
+ case Left(e):
182
+ ... # handle error
183
+ case Right(Nothing()):
184
+ ... # handle absence
185
+ case Right(Some(user)):
186
+ ... # finally, the value
187
+
188
+ # With OptionT — one flat pipeline:
189
+ pipeline = (
190
+ OptionT(fetch_user(id))
191
+ .bind(lambda user: OptionT(get_email(user)))
192
+ .map(lambda email: email.upper())
193
+ )
194
+ ```
195
+
196
+ ### Laws
197
+
198
+ Every implementation must satisfy these mathematical laws:
199
+
200
+ **Semigroup**
201
+
202
+ - Associativity: `(a + b) + c == a + (b + c)`
203
+
204
+ **Monoid**
205
+
206
+ - Left identity: `empty + a == a`
207
+ - Right identity: `a + empty == a`
208
+
209
+ **Functor**
210
+
211
+ - Identity: `fa.map(id) == fa`
212
+ - Composition: `fa.map(f).map(g) == fa.map(g ∘ f)`
213
+
214
+ **Applicative**
215
+
216
+ - Identity: `pure(id).ap(v) == v`
217
+ - Homomorphism: `pure(f).ap(pure(x)) == pure(f(x))`
218
+ - Interchange: `u.ap(pure(y)) == pure(λf. f(y)).ap(u)`
219
+ - Composition: `pure(∘).ap(u).ap(v).ap(w) == u.ap(v.ap(w))`
220
+
221
+ **Monad**
222
+
223
+ - Left identity: `pure(a).bind(f) == f(a)`
224
+ - Right identity: `m.bind(pure) == m`
225
+ - Associativity: `m.bind(f).bind(g) == m.bind(λx. f(x).bind(g))`
226
+
227
+ ## Why no IO type?
228
+
229
+ In Haskell, `IO` exists because the language is purely functional — there is
230
+ no way to perform side effects without wrapping them in the `IO` monad. The
231
+ type system enforces purity: if a function doesn't return `IO`, it cannot
232
+ touch the network, filesystem, or mutable state.
233
+
234
+ Python has no such constraint. Any function can perform side effects at any
235
+ time. An `IO` wrapper in Python would be:
236
+
237
+ 1. **Unenforceable** — nothing stops you from doing I/O outside the wrapper.
238
+ The type system can't prevent `print()` in a "pure" function.
239
+ 1. **Purely ceremonial** — it adds a wrapper you must manually construct and
240
+ unwrap, but provides no guarantee. It's a comment dressed as a type.
241
+ 1. **Redundant with async** — Python's `async/await` already separates
242
+ "description of a computation" from "execution of that computation,"
243
+ which is most of what `IO` provides in Haskell.
244
+
245
+ Instead, funstruct provides:
246
+
247
+ - **`Either[E, A]`** — for operations that might fail (the error is a value)
248
+ - **`Future[E, A]`** — for async operations that might fail (lazy, composable)
249
+ - **`@Try` / `@TryAsync`** — for wrapping exception-throwing code at boundaries
250
+
251
+ These give you the composition benefits of monadic pipelines where they
252
+ matter (error handling, async sequencing) without pretending Python is
253
+ something it isn't.