funstruct 0.1.2__tar.gz → 1.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 (104) hide show
  1. funstruct-1.0.0/.claude/settings.local.json +8 -0
  2. funstruct-1.0.0/.github/workflows/ci.yml +33 -0
  3. funstruct-1.0.0/.github/workflows/publish.yml +18 -0
  4. funstruct-1.0.0/.gitignore +136 -0
  5. funstruct-1.0.0/.pre-commit-config.yaml +30 -0
  6. funstruct-1.0.0/CONTRIBUTING.md +11 -0
  7. funstruct-1.0.0/PKG-INFO +251 -0
  8. funstruct-1.0.0/README.md +238 -0
  9. funstruct-1.0.0/benchmarks/test_bench_collections.py +151 -0
  10. funstruct-1.0.0/docs/applicative/index.md +10 -0
  11. funstruct-1.0.0/docs/applicative/validated.md +11 -0
  12. funstruct-1.0.0/docs/collections/cons.md +11 -0
  13. funstruct-1.0.0/docs/collections/frozendict.md +7 -0
  14. funstruct-1.0.0/docs/collections/index.md +10 -0
  15. funstruct-1.0.0/docs/collections/tree.md +3 -0
  16. funstruct-1.0.0/docs/functor/index.md +3 -0
  17. funstruct-1.0.0/docs/index.md +150 -0
  18. funstruct-1.0.0/docs/monad/either.md +3 -0
  19. funstruct-1.0.0/docs/monad/future.md +3 -0
  20. funstruct-1.0.0/docs/monad/index.md +15 -0
  21. funstruct-1.0.0/docs/monad/option.md +3 -0
  22. funstruct-1.0.0/docs/monad/reader.md +3 -0
  23. funstruct-1.0.0/docs/monad/result.md +3 -0
  24. funstruct-1.0.0/docs/monad/state.md +20 -0
  25. funstruct-1.0.0/docs/monad/writer.md +3 -0
  26. funstruct-1.0.0/docs/monadtransformer/either_t.md +3 -0
  27. funstruct-1.0.0/docs/monadtransformer/index.md +13 -0
  28. funstruct-1.0.0/docs/monadtransformer/option_t.md +3 -0
  29. funstruct-1.0.0/docs/monadtransformer/reader_t.md +3 -0
  30. funstruct-1.0.0/docs/monadtransformer/state_t.md +26 -0
  31. funstruct-1.0.0/docs/monadtransformer/writer_t.md +3 -0
  32. funstruct-1.0.0/docs/typeclass/applicative.md +3 -0
  33. funstruct-1.0.0/docs/typeclass/index.md +21 -0
  34. funstruct-1.0.0/docs/typeclass/monad.md +3 -0
  35. funstruct-1.0.0/docs/typeclass/monadtransformer.md +3 -0
  36. funstruct-1.0.0/docs/typeclass/monoid.md +3 -0
  37. funstruct-1.0.0/docs/typeclass/semigroup.md +3 -0
  38. funstruct-1.0.0/docs/util/tailrec.md +24 -0
  39. funstruct-1.0.0/funstruct/__init__.py +1 -0
  40. funstruct-1.0.0/funstruct/applicative/__init__.py +1 -0
  41. funstruct-1.0.0/funstruct/applicative/validated.py +169 -0
  42. funstruct-1.0.0/funstruct/collections/__init__.py +1 -0
  43. {funstruct-0.1.2/funstruct → funstruct-1.0.0/funstruct/collections}/cons.py +262 -222
  44. funstruct-1.0.0/funstruct/collections/frozendict.py +367 -0
  45. funstruct-1.0.0/funstruct/collections/tree.py +148 -0
  46. funstruct-1.0.0/funstruct/functor/__init__.py +1 -0
  47. funstruct-1.0.0/funstruct/monad/__init__.py +11 -0
  48. funstruct-1.0.0/funstruct/monad/either.py +294 -0
  49. funstruct-1.0.0/funstruct/monad/future.py +78 -0
  50. funstruct-1.0.0/funstruct/monad/option.py +270 -0
  51. funstruct-1.0.0/funstruct/monad/reader.py +105 -0
  52. funstruct-1.0.0/funstruct/monad/result.py +298 -0
  53. funstruct-1.0.0/funstruct/monad/state.py +144 -0
  54. funstruct-1.0.0/funstruct/monad/writer.py +121 -0
  55. funstruct-1.0.0/funstruct/monadtransformer/__init__.py +30 -0
  56. funstruct-1.0.0/funstruct/monadtransformer/either_t.py +185 -0
  57. funstruct-1.0.0/funstruct/monadtransformer/option_t.py +232 -0
  58. funstruct-1.0.0/funstruct/monadtransformer/reader_t.py +191 -0
  59. funstruct-1.0.0/funstruct/monadtransformer/state_t.py +202 -0
  60. funstruct-1.0.0/funstruct/monadtransformer/writer_t.py +204 -0
  61. funstruct-1.0.0/funstruct/monoid/__init__.py +21 -0
  62. funstruct-1.0.0/funstruct/py.typed +0 -0
  63. funstruct-1.0.0/funstruct/semigroup/__init__.py +19 -0
  64. funstruct-1.0.0/funstruct/typeclasses/__init__.py +17 -0
  65. funstruct-1.0.0/funstruct/typeclasses/_applicative.py +59 -0
  66. funstruct-1.0.0/funstruct/typeclasses/_functor.py +59 -0
  67. funstruct-1.0.0/funstruct/typeclasses/_monad.py +83 -0
  68. funstruct-1.0.0/funstruct/typeclasses/_monad_transformer.py +112 -0
  69. funstruct-1.0.0/funstruct/typeclasses/_monoid.py +44 -0
  70. funstruct-1.0.0/funstruct/typeclasses/_semigroup.py +36 -0
  71. funstruct-1.0.0/funstruct/util/__init__.py +1 -0
  72. funstruct-1.0.0/funstruct/util/tailrec.py +69 -0
  73. funstruct-1.0.0/justfile +61 -0
  74. funstruct-1.0.0/mise.toml +9 -0
  75. funstruct-1.0.0/mkdocs.yml +67 -0
  76. funstruct-1.0.0/noxfile.py +31 -0
  77. funstruct-1.0.0/pyproject.toml +88 -0
  78. funstruct-1.0.0/tests/__init__.py +0 -0
  79. funstruct-1.0.0/tests/laws.py +187 -0
  80. funstruct-1.0.0/tests/test_cons.py +872 -0
  81. funstruct-1.0.0/tests/test_either.py +200 -0
  82. funstruct-1.0.0/tests/test_either_t.py +245 -0
  83. funstruct-1.0.0/tests/test_frozendict.py +617 -0
  84. funstruct-1.0.0/tests/test_future.py +379 -0
  85. funstruct-1.0.0/tests/test_law_violations.py +280 -0
  86. funstruct-1.0.0/tests/test_option.py +178 -0
  87. funstruct-1.0.0/tests/test_option_t.py +244 -0
  88. funstruct-1.0.0/tests/test_reader.py +187 -0
  89. funstruct-1.0.0/tests/test_reader_t.py +293 -0
  90. funstruct-1.0.0/tests/test_result.py +62 -0
  91. funstruct-1.0.0/tests/test_state.py +203 -0
  92. funstruct-1.0.0/tests/test_state_t.py +304 -0
  93. funstruct-1.0.0/tests/test_tailrec.py +83 -0
  94. funstruct-1.0.0/tests/test_tree.py +88 -0
  95. funstruct-1.0.0/tests/test_validated.py +347 -0
  96. funstruct-1.0.0/tests/test_writer.py +199 -0
  97. funstruct-1.0.0/tests/test_writer_t.py +254 -0
  98. funstruct-1.0.0/uv.lock +1507 -0
  99. funstruct-0.1.2/PKG-INFO +0 -28
  100. funstruct-0.1.2/README.md +0 -9
  101. funstruct-0.1.2/funstruct/frozendict.py +0 -268
  102. funstruct-0.1.2/pyproject.toml +0 -27
  103. {funstruct-0.1.2 → funstruct-1.0.0}/LICENSE +0 -0
  104. /funstruct-0.1.2/funstruct/__init__.py → /funstruct-1.0.0/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,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,251 @@
1
+ Metadata-Version: 2.5
2
+ Name: funstruct
3
+ Version: 1.0.0
4
+ Summary: Fun & functional structures for Python
5
+ Project-URL: Homepage, https://github.com/veyga/funstruct
6
+ Project-URL: Repository, https://github.com/veyga/funstruct
7
+ Author-email: Andrew Stefanich <andrewstefanich@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cons,data structures,fp,functional,immutable
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # funstruct
15
+
16
+ A helpful collection of functional utilities.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install funstruct || uv add funstruct
22
+ ```
23
+
24
+ ## Functional Primer
25
+
26
+ ### Type Class Hierarchy
27
+
28
+ ```
29
+ Semigroup Functor
30
+ │ │
31
+ Monoid Applicative
32
+
33
+ Monad
34
+
35
+ MonadTransformer
36
+ ```
37
+
38
+ #### Diagrams
39
+
40
+ **Semigroup** — associative combine (`+` being the canonical 'combine' operation)
41
+
42
+ ```
43
+ A ─┐
44
+ ├──( + )──> A
45
+ A ─┘
46
+ ```
47
+
48
+ **Monoid** — semigroup with an identity element
49
+
50
+ ```
51
+ A ─┐
52
+ ├──( + )──> A (+ identity = A)
53
+ A ─┘
54
+ ```
55
+
56
+ **Functor** — transform the value inside a context
57
+
58
+ ```
59
+ F[A] ---( f: A -> B )---> F[B]
60
+ ```
61
+
62
+ **Applicative** — combine independent computations
63
+
64
+ ```
65
+ F[A] ─┐
66
+ ├──> F[(A, B)]
67
+ F[B] ─┘
68
+ ```
69
+
70
+ **Monad** — sequence computations that produce new contexts
71
+
72
+ ```
73
+ F[A] ---( f: A -> F[B] )---> F[B]
74
+ ```
75
+
76
+ ```python
77
+ @dataclass(frozen=True)
78
+ class Semigroup:
79
+ typ: type
80
+ combine: Callable # (A, A) -> A
81
+
82
+ @dataclass(frozen=True)
83
+ class Monoid(Semigroup):
84
+ typ: type
85
+ combine: Callable # (A, A) -> A
86
+ empty: object # identity element
87
+
88
+ class Functor(ABC):
89
+ def map(self, f) -> Functor: ...
90
+
91
+ class Applicative(Functor):
92
+ def pure(cls, value) -> Applicative: ...
93
+ def ap(self, other) -> Applicative: ...
94
+ def __add__ = ap # alias
95
+
96
+ class Monad(Applicative):
97
+ def bind(self, f) -> Monad: ...
98
+ def do(cls, gen_fn) -> Monad: ...
99
+ def __rshift__ = bind # >>
100
+
101
+ class MonadTransformer(Monad, Generic[_F, _A]):
102
+ def and_then(self, other) -> MonadTransformer: ...
103
+ ```
104
+
105
+ ```python
106
+ # Multiple semigroups for the same type:
107
+ int_add = Monoid(typ=int, combine=lambda a, b: a + b, empty=0)
108
+ int_mul = Monoid(typ=int, combine=lambda a, b: a * b, empty=1)
109
+ ```
110
+
111
+ ### ~ Scala equivalent
112
+
113
+ ```scala
114
+ trait Semigroup[A] {
115
+ def combine(x: A, y: A): A
116
+ }
117
+
118
+ trait Monoid[A] extends Semigroup[A] {
119
+ def empty: A
120
+ }
121
+
122
+ trait Functor[F[_]] {
123
+ def map[A, B](fa: F[A])(f: A => B): F[B]
124
+ }
125
+
126
+ trait Applicative[F[_]] extends Functor[F] {
127
+ def pure[A](a: A): F[A]
128
+ def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
129
+ }
130
+
131
+ trait Monad[F[_]] extends Applicative[F] {
132
+ def bind(fa: F[A])(f: A => F[B]): F[B]
133
+ }
134
+ ```
135
+
136
+ ### Implementations
137
+
138
+ | Typeclass | Implementations |
139
+ | ---------------- | -------------------------------------------- |
140
+ | Functor | Tree, frozendict, + all below |
141
+ | Applicative | Validated, + all below |
142
+ | Monad | Option, Either, State, Reader, Writer, CList |
143
+ | MonadTransformer | ReaderT, StateT, EitherT, OptionT, WriterT |
144
+
145
+ | Type | What it models |
146
+ | ------------------- | ------------------------------------------- |
147
+ | `Option[A]` | Value might not exist |
148
+ | `Either[E, A]` | Value or typed error |
149
+ | `Result[A]` (alias) | `Either[Exception, A]` + `@Try` decorator |
150
+ | `State[S, A]` | Stateful computation |
151
+ | `Reader[Ctx, A]` | Shared environment |
152
+ | `Writer[W, A]` | Accumulated output |
153
+ | `Validated[E, A]` | Error accumulation (applicative, not monad) |
154
+ | `Future[E, A]` | Lazy async + typed error |
155
+ | `CList[A]` | Persistent singly-linked list |
156
+ | `Tree[A]` | Immutable binary tree (functor only) |
157
+ | `frozendict[K, V]` | Persistent HAMT dictionary |
158
+
159
+ ### Monad Transformers
160
+
161
+ A transformer combines effects by wrapping one monad inside another.
162
+
163
+ ```
164
+ ReaderT[F, Ctx, A] = Ctx -> F[A] (environment + F's effects)
165
+ StateT[F, S, A] = S -> F[(S, A)] (state + F's effects)
166
+ EitherT[F, E, A] = F[Either[E, A]] (errors + F's effects)
167
+ OptionT[F, A] = F[Option[A]] (absence + F's effects)
168
+ WriterT[F, W, A] = F[(A, W)] (output + F's effects)
169
+ ```
170
+
171
+ **Why transformers?** Monads don't compose automatically. If you need
172
+ config + errors + logging, you'd manually unwrap 3 nested layers at
173
+ every step. Transformers flatten that into one `bind`:
174
+
175
+ ```python
176
+ # Without transformer — nested pattern matching at every step:
177
+ result = fetch_user(id) # Either[Err, Option[User]]
178
+ match result:
179
+ case Left(e):
180
+ ... # handle error
181
+ case Right(Nothing()):
182
+ ... # handle absence
183
+ case Right(Some(user)):
184
+ ... # finally, the value
185
+
186
+ # With OptionT — one flat pipeline:
187
+ pipeline = (
188
+ OptionT(fetch_user(id))
189
+ .bind(lambda user: OptionT(get_email(user)))
190
+ .map(lambda email: email.upper())
191
+ )
192
+ ```
193
+
194
+ ### Laws
195
+
196
+ Every implementation must satisfy these mathematical laws:
197
+
198
+ **Semigroup**
199
+
200
+ - Associativity: `(a + b) + c == a + (b + c)`
201
+
202
+ **Monoid**
203
+
204
+ - Left identity: `empty + a == a`
205
+ - Right identity: `a + empty == a`
206
+
207
+ **Functor**
208
+
209
+ - Identity: `fa.map(id) == fa`
210
+ - Composition: `fa.map(f).map(g) == fa.map(g ∘ f)`
211
+
212
+ **Applicative**
213
+
214
+ - Identity: `pure(id).ap(v) == v`
215
+ - Homomorphism: `pure(f).ap(pure(x)) == pure(f(x))`
216
+ - Interchange: `u.ap(pure(y)) == pure(λf. f(y)).ap(u)`
217
+ - Composition: `pure(∘).ap(u).ap(v).ap(w) == u.ap(v.ap(w))`
218
+
219
+ **Monad**
220
+
221
+ - Left identity: `pure(a).bind(f) == f(a)`
222
+ - Right identity: `m.bind(pure) == m`
223
+ - Associativity: `m.bind(f).bind(g) == m.bind(λx. f(x).bind(g))`
224
+
225
+ ## Why no IO type?
226
+
227
+ In Haskell, `IO` exists because the language is purely functional — there is
228
+ no way to perform side effects without wrapping them in the `IO` monad. The
229
+ type system enforces purity: if a function doesn't return `IO`, it cannot
230
+ touch the network, filesystem, or mutable state.
231
+
232
+ Python has no such constraint. Any function can perform side effects at any
233
+ time. An `IO` wrapper in Python would be:
234
+
235
+ 1. **Unenforceable** — nothing stops you from doing I/O outside the wrapper.
236
+ The type system can't prevent `print()` in a "pure" function.
237
+ 1. **Purely ceremonial** — it adds a wrapper you must manually construct and
238
+ unwrap, but provides no guarantee. It's a comment dressed as a type.
239
+ 1. **Redundant with async** — Python's `async/await` already separates
240
+ "description of a computation" from "execution of that computation,"
241
+ which is most of what `IO` provides in Haskell.
242
+
243
+ Instead, funstruct provides:
244
+
245
+ - **`Either[E, A]`** — for operations that might fail (the error is a value)
246
+ - **`Future[E, A]`** — for async operations that might fail (lazy, composable)
247
+ - **`@Try` / `@TryAsync`** — for wrapping exception-throwing code at boundaries
248
+
249
+ These give you the composition benefits of monadic pipelines where they
250
+ matter (error handling, async sequencing) without pretending Python is
251
+ something it isn't.