funstruct 1.0.2__tar.gz → 2.0.0a1__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.
- {funstruct-1.0.2 → funstruct-2.0.0a1}/.gitignore +2 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/.pre-commit-config.yaml +6 -0
- funstruct-2.0.0a1/PKG-INFO +496 -0
- funstruct-2.0.0a1/README.md +482 -0
- funstruct-2.0.0a1/benchmarks/test_bench_typeclasses.py +203 -0
- funstruct-2.0.0a1/demoplayground/index.html +927 -0
- funstruct-2.0.0a1/demos/01_result_pipeline.py +72 -0
- funstruct-2.0.0a1/demos/02_do_notation.py +132 -0
- funstruct-2.0.0a1/demos/03_tagless_final_intro.py +108 -0
- funstruct-2.0.0a1/demos/04_tagless_final_db.py +180 -0
- funstruct-2.0.0a1/demos/05_json_encoder.py +200 -0
- funstruct-2.0.0a1/demos/06_generic_functions.py +170 -0
- funstruct-2.0.0a1/demos/07_reader_vs_cake.py +202 -0
- funstruct-2.0.0a1/demos/08_custom_types.py +310 -0
- funstruct-2.0.0a1/demos/09_lenses.py +103 -0
- funstruct-2.0.0a1/demos/10_writer_stacktrace.py +93 -0
- funstruct-2.0.0a1/demos/11_state_counter.py +111 -0
- funstruct-2.0.0a1/demos/12_trait_bounds.py +211 -0
- funstruct-2.0.0a1/demos/13_dsl.py +250 -0
- funstruct-2.0.0a1/demos/14_dsl_initial_vs_final.py +235 -0
- funstruct-2.0.0a1/demos/15_typeclass_pattern_json.py +239 -0
- funstruct-2.0.0a1/demos/README.md +33 -0
- funstruct-2.0.0a1/demos/transformers/01_the_problem.py +75 -0
- funstruct-2.0.0a1/demos/transformers/02_option_t.py +77 -0
- funstruct-2.0.0a1/demos/transformers/03_alternative.py +82 -0
- funstruct-2.0.0a1/demos/transformers/README.md +12 -0
- funstruct-2.0.0a1/docs/applicative/index.md +4 -0
- funstruct-2.0.0a1/docs/applicative/validated.md +3 -0
- funstruct-2.0.0a1/docs/applicative/ziplist.md +3 -0
- funstruct-2.0.0a1/docs/collections/cons.md +3 -0
- funstruct-2.0.0a1/docs/collections/frozendict.md +3 -0
- funstruct-2.0.0a1/docs/collections/index.md +5 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/either_t.md +3 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/index.md +9 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/option_t.md +3 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/reader_t.md +3 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/state_t.md +3 -0
- funstruct-2.0.0a1/docs/experimental/monadtransformer/writer_t.md +3 -0
- funstruct-2.0.0a1/docs/experimental/optics/index.md +3 -0
- funstruct-2.0.0a1/docs/guides/antipatterns.md +229 -0
- funstruct-2.0.0a1/docs/guides/async.md +138 -0
- funstruct-2.0.0a1/docs/guides/creating_a_data_type.md +271 -0
- funstruct-2.0.0a1/docs/guides/do_notation.md +175 -0
- funstruct-2.0.0a1/docs/guides/evaluation_strategy.md +92 -0
- funstruct-2.0.0a1/docs/guides/generic_programming.md +158 -0
- funstruct-2.0.0a1/docs/guides/monads.md +197 -0
- funstruct-2.0.0a1/docs/guides/railway_oriented_programming.md +155 -0
- funstruct-2.0.0a1/docs/guides/tagless_final.md +201 -0
- funstruct-2.0.0a1/docs/guides/typeclass_pattern_json.md +155 -0
- funstruct-2.0.0a1/docs/guides/typeclass_pattern_ordering.md +171 -0
- funstruct-2.0.0a1/docs/index.md +1 -0
- funstruct-2.0.0a1/docs/monad/index.md +9 -0
- funstruct-2.0.0a1/docs/monad/state.md +3 -0
- funstruct-2.0.0a1/docs/playground.html +927 -0
- funstruct-2.0.0a1/docs/typeclass/alternative.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/applicative.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/bifunctor.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/foldable.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/functor.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/index.md +12 -0
- funstruct-2.0.0a1/docs/typeclass/monad.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/monad_error.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/monoid.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/semigroup.md +3 -0
- funstruct-2.0.0a1/docs/typeclass/traversable.md +3 -0
- funstruct-2.0.0a1/docs/typeclasses.svg +87 -0
- funstruct-2.0.0a1/docs/util/created_at.md +3 -0
- funstruct-2.0.0a1/docs/util/tailrec.md +3 -0
- funstruct-2.0.0a1/funstruct/applicative/validated/__init__.py +115 -0
- funstruct-2.0.0a1/funstruct/applicative/validated/instances.py +72 -0
- funstruct-2.0.0a1/funstruct/applicative/ziplist/__init__.py +67 -0
- funstruct-2.0.0a1/funstruct/applicative/ziplist/instances.py +34 -0
- funstruct-2.0.0a1/funstruct/collections/cons/__init__.py +388 -0
- funstruct-2.0.0a1/funstruct/collections/cons/instances.py +64 -0
- funstruct-1.0.2/funstruct/collections/frozendict.py → funstruct-2.0.0a1/funstruct/collections/frozendict/__init__.py +89 -45
- funstruct-2.0.0a1/funstruct/collections/frozendict/instances.py +28 -0
- funstruct-1.0.2/funstruct/collections/tree.py → funstruct-2.0.0a1/funstruct/collections/tree/__init__.py +64 -21
- funstruct-2.0.0a1/funstruct/collections/tree/instances.py +45 -0
- funstruct-2.0.0a1/funstruct/experimental/__init__.py +4 -0
- funstruct-2.0.0a1/funstruct/experimental/monadtransformer/__init__.py +57 -0
- funstruct-1.0.2/funstruct/typeclasses/_monad_transformer.py → funstruct-2.0.0a1/funstruct/experimental/monadtransformer/_typeclass.py +57 -20
- {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/either_t.py +95 -42
- {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/option_t.py +85 -39
- {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/reader_t.py +64 -34
- {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/state_t.py +71 -42
- {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/writer_t.py +81 -43
- funstruct-2.0.0a1/funstruct/experimental/optics/__init__.py +29 -0
- funstruct-2.0.0a1/funstruct/experimental/optics/_lens.py +101 -0
- funstruct-2.0.0a1/funstruct/monad/either/__init__.py +226 -0
- funstruct-2.0.0a1/funstruct/monad/either/instances.py +56 -0
- funstruct-2.0.0a1/funstruct/monad/future/__init__.py +82 -0
- funstruct-2.0.0a1/funstruct/monad/future/instances.py +20 -0
- funstruct-1.0.2/funstruct/monad/option.py → funstruct-2.0.0a1/funstruct/monad/option/__init__.py +49 -94
- funstruct-2.0.0a1/funstruct/monad/option/instances.py +57 -0
- funstruct-1.0.2/funstruct/monad/reader.py → funstruct-2.0.0a1/funstruct/monad/reader/__init__.py +24 -39
- funstruct-2.0.0a1/funstruct/monad/reader/instances.py +25 -0
- funstruct-2.0.0a1/funstruct/monad/result/__init__.py +418 -0
- funstruct-2.0.0a1/funstruct/monad/result/instances.py +71 -0
- funstruct-2.0.0a1/funstruct/monad/state/__init__.py +113 -0
- funstruct-2.0.0a1/funstruct/monad/state/instances.py +24 -0
- funstruct-2.0.0a1/funstruct/monad/writer/__init__.py +160 -0
- funstruct-2.0.0a1/funstruct/monad/writer/instances.py +46 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/monoid/__init__.py +1 -0
- funstruct-2.0.0a1/funstruct/typeclasses/__init__.py +37 -0
- funstruct-2.0.0a1/funstruct/typeclasses/alternative.py +24 -0
- funstruct-2.0.0a1/funstruct/typeclasses/applicative.py +36 -0
- funstruct-2.0.0a1/funstruct/typeclasses/bifunctor.py +25 -0
- funstruct-2.0.0a1/funstruct/typeclasses/foldable.py +25 -0
- funstruct-2.0.0a1/funstruct/typeclasses/functor.py +21 -0
- funstruct-2.0.0a1/funstruct/typeclasses/mixins/__init__.py +9 -0
- funstruct-2.0.0a1/funstruct/typeclasses/mixins/auto_register.py +51 -0
- funstruct-2.0.0a1/funstruct/typeclasses/mixins/data_type.py +43 -0
- funstruct-2.0.0a1/funstruct/typeclasses/mixins/dot_notation.py +62 -0
- funstruct-2.0.0a1/funstruct/typeclasses/mixins/type_constructor.py +46 -0
- funstruct-2.0.0a1/funstruct/typeclasses/monad.py +33 -0
- funstruct-2.0.0a1/funstruct/typeclasses/monad_error.py +27 -0
- funstruct-1.0.2/funstruct/typeclasses/_monoid.py → funstruct-2.0.0a1/funstruct/typeclasses/monoid.py +1 -1
- funstruct-2.0.0a1/funstruct/typeclasses/traversable.py +26 -0
- funstruct-2.0.0a1/funstruct/typeclasses/typeclass.py +30 -0
- funstruct-2.0.0a1/funstruct/typeclasses/utils/__init__.py +14 -0
- funstruct-2.0.0a1/funstruct/typeclasses/utils/registry.py +112 -0
- funstruct-2.0.0a1/funstruct/util/_reawaitable.py +50 -0
- funstruct-2.0.0a1/funstruct/util/created_at.py +97 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/util/tailrec.py +33 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/justfile +12 -2
- funstruct-2.0.0a1/mkdocs.yml +90 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/noxfile.py +21 -1
- {funstruct-1.0.2 → funstruct-2.0.0a1}/pyproject.toml +20 -4
- funstruct-2.0.0a1/scripts/generate_diagram.py +320 -0
- funstruct-2.0.0a1/scripts/generate_playground.py +191 -0
- {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_either_t.py +61 -11
- funstruct-2.0.0a1/tests/experimental/test_optics/__init__.py +0 -0
- funstruct-2.0.0a1/tests/experimental/test_optics/test_optics_frozendict.py +105 -0
- {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_option_t.py +180 -75
- {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_reader_t.py +43 -17
- {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_state_t.py +44 -21
- {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_writer_t.py +21 -7
- funstruct-2.0.0a1/tests/laws.py +242 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_cons.py +158 -3
- funstruct-2.0.0a1/tests/test_do_bind_equivalence.py +210 -0
- funstruct-2.0.0a1/tests/test_either.py +320 -0
- funstruct-2.0.0a1/tests/test_foldable.py +53 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_frozendict.py +234 -4
- funstruct-2.0.0a1/tests/test_future.py +676 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_law_violations.py +11 -7
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_option.py +122 -9
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_reader.py +21 -7
- funstruct-2.0.0a1/tests/test_result.py +287 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_state.py +17 -3
- funstruct-2.0.0a1/tests/test_summon.py +248 -0
- funstruct-2.0.0a1/tests/test_tagless_final.py +192 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_tailrec.py +36 -1
- funstruct-2.0.0a1/tests/test_trait_bounds.py +209 -0
- funstruct-2.0.0a1/tests/test_traversable.py +101 -0
- funstruct-2.0.0a1/tests/test_tree.py +176 -0
- funstruct-2.0.0a1/tests/test_validated.py +481 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_writer.py +6 -6
- funstruct-2.0.0a1/tests/test_ziplist.py +102 -0
- funstruct-2.0.0a1/typeclasses.svg +1 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/uv.lock +74 -1
- funstruct-1.0.2/.claude/settings.local.json +0 -8
- funstruct-1.0.2/PKG-INFO +0 -253
- funstruct-1.0.2/README.md +0 -239
- funstruct-1.0.2/docs/applicative/index.md +0 -10
- funstruct-1.0.2/docs/applicative/validated.md +0 -11
- funstruct-1.0.2/docs/collections/cons.md +0 -11
- funstruct-1.0.2/docs/collections/frozendict.md +0 -7
- funstruct-1.0.2/docs/collections/index.md +0 -10
- funstruct-1.0.2/docs/functor/index.md +0 -3
- funstruct-1.0.2/docs/index.md +0 -150
- funstruct-1.0.2/docs/monad/index.md +0 -15
- funstruct-1.0.2/docs/monad/state.md +0 -20
- funstruct-1.0.2/docs/monadtransformer/either_t.md +0 -3
- funstruct-1.0.2/docs/monadtransformer/index.md +0 -13
- funstruct-1.0.2/docs/monadtransformer/option_t.md +0 -3
- funstruct-1.0.2/docs/monadtransformer/reader_t.md +0 -3
- funstruct-1.0.2/docs/monadtransformer/state_t.md +0 -26
- funstruct-1.0.2/docs/monadtransformer/writer_t.md +0 -3
- funstruct-1.0.2/docs/typeclass/applicative.md +0 -3
- funstruct-1.0.2/docs/typeclass/index.md +0 -21
- funstruct-1.0.2/docs/typeclass/monad.md +0 -3
- funstruct-1.0.2/docs/typeclass/monadtransformer.md +0 -3
- funstruct-1.0.2/docs/typeclass/monoid.md +0 -3
- funstruct-1.0.2/docs/typeclass/semigroup.md +0 -3
- funstruct-1.0.2/docs/util/tailrec.md +0 -24
- funstruct-1.0.2/funstruct/applicative/validated.py +0 -182
- funstruct-1.0.2/funstruct/collections/cons.py +0 -701
- funstruct-1.0.2/funstruct/monad/either.py +0 -294
- funstruct-1.0.2/funstruct/monad/future.py +0 -78
- funstruct-1.0.2/funstruct/monad/result.py +0 -327
- funstruct-1.0.2/funstruct/monad/state.py +0 -144
- funstruct-1.0.2/funstruct/monad/writer.py +0 -121
- funstruct-1.0.2/funstruct/monadtransformer/__init__.py +0 -30
- funstruct-1.0.2/funstruct/typeclasses/__init__.py +0 -17
- funstruct-1.0.2/funstruct/typeclasses/_applicative.py +0 -59
- funstruct-1.0.2/funstruct/typeclasses/_functor.py +0 -59
- funstruct-1.0.2/funstruct/typeclasses/_monad.py +0 -83
- funstruct-1.0.2/mkdocs.yml +0 -71
- funstruct-1.0.2/tests/laws.py +0 -187
- funstruct-1.0.2/tests/test_either.py +0 -200
- funstruct-1.0.2/tests/test_future.py +0 -379
- funstruct-1.0.2/tests/test_result.py +0 -62
- funstruct-1.0.2/tests/test_tree.py +0 -88
- funstruct-1.0.2/tests/test_validated.py +0 -347
- {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/ci.yml +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/docs.yml +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/publish.yml +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/CONTRIBUTING.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/LICENSE +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/benchmarks/test_bench_collections.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/collections/tree.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/either.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/future.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/option.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/reader.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/result.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/writer.md +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/applicative/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/collections/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/functor/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/monad/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/py.typed +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/semigroup/__init__.py +0 -0
- /funstruct-1.0.2/funstruct/typeclasses/_semigroup.py → /funstruct-2.0.0a1/funstruct/typeclasses/semigroup.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/util/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/mise.toml +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/__init__.py +0 -0
- /funstruct-1.0.2/docs/functor/.gitkeep → /funstruct-2.0.0a1/tests/experimental/__init__.py +0 -0
- {funstruct-1.0.2 → funstruct-2.0.0a1}/uv.toml +0 -0
|
@@ -22,6 +22,12 @@ repos:
|
|
|
22
22
|
- mdformat-mkdocs
|
|
23
23
|
- repo: local
|
|
24
24
|
hooks:
|
|
25
|
+
- id: generate-diagram
|
|
26
|
+
name: generate typeclass diagram
|
|
27
|
+
entry: uv run python scripts/generate_diagram.py
|
|
28
|
+
language: system
|
|
29
|
+
pass_filenames: false
|
|
30
|
+
files: ^funstruct/typeclasses/
|
|
25
31
|
- id: typecheck
|
|
26
32
|
name: typecheck
|
|
27
33
|
entry: bash -c "just check"
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: funstruct
|
|
3
|
+
Version: 2.0.0a1
|
|
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 functional programming library for Python.
|
|
18
|
+
Typeclasses, collections, monads, algebraic data types, etc.
|
|
19
|
+
|
|
20
|
+
Heavily influenced by the Haskell and Scala languages, as well
|
|
21
|
+
[Scalaz](https://github.com/scalaz/scalaz) and
|
|
22
|
+
[Cats](https://typelevel.org/cats/).
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install funstruct || uv add funstruct
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Functional Primer
|
|
31
|
+
|
|
32
|
+
### Principles
|
|
33
|
+
|
|
34
|
+
**Immutability** — all funstruct data types are immutable. `Some(10).map(f)`
|
|
35
|
+
returns a NEW `Some(20)`, never modifies the original. `frozendict.put(k, v)`
|
|
36
|
+
returns a new dict. This eliminates shared-state bugs and makes code predictable.
|
|
37
|
+
|
|
38
|
+
**Pure functions** — functions that always return the same output for the
|
|
39
|
+
same input, with no side effects. `map`, `bind`, `fold` are pure.
|
|
40
|
+
Side effects (`@Try`, `@TryAsync`, `AsyncResult`) are pushed to boundaries.
|
|
41
|
+
Python can't enforce this, so it remains a recommendation. See below about IO
|
|
42
|
+
type.
|
|
43
|
+
|
|
44
|
+
**Composition over control flow** — instead of `if/else` chains and
|
|
45
|
+
`try/except` blocks, compose operations with `map`, `bind`, and `do`.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Imperative (control flow)
|
|
49
|
+
user = get_user(id)
|
|
50
|
+
if user is None:
|
|
51
|
+
return None
|
|
52
|
+
email = get_email(user)
|
|
53
|
+
if email is None:
|
|
54
|
+
return None
|
|
55
|
+
return email.upper()
|
|
56
|
+
|
|
57
|
+
# Functional (composition)
|
|
58
|
+
get_user(id).bind(get_email).map(str.upper)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Separation of data and behavior** — in OOP, classes bundle data and
|
|
62
|
+
methods together. In FP, they're separate: data is inert, behavior is
|
|
63
|
+
defined externally. This means you can add new operations to existing
|
|
64
|
+
types without modifying them.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# OOP: data + behavior bundled in the class
|
|
68
|
+
class User:
|
|
69
|
+
def __init__(self, name, age): ...
|
|
70
|
+
def validate(self): ... # behavior ON the data
|
|
71
|
+
def save(self): ... # more behavior ON the data
|
|
72
|
+
def to_json(self): ... # yet more behavior ON the data
|
|
73
|
+
|
|
74
|
+
# FP: data is plain, behavior is external via typeclasses
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class User:
|
|
77
|
+
name: str
|
|
78
|
+
age: int
|
|
79
|
+
# no methods — just data
|
|
80
|
+
|
|
81
|
+
# Behavior defined separately, works across ANY type with an instance
|
|
82
|
+
jsonify(user) # via JSONWrite[User]
|
|
83
|
+
summon(Monad, Result).map(Ok(user), f) # via Monad[Result]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Algebraic data types (ADTs)** — types with a fixed set of variants:
|
|
87
|
+
`Option = Some | Nothing`, `Result = Ok | Err`, `Either = Right | Left`.
|
|
88
|
+
Pattern matching exhaustively handles all cases.
|
|
89
|
+
|
|
90
|
+
### Architecture
|
|
91
|
+
|
|
92
|
+
Three distinct class hierarchies, connected by instances:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
BaseTypeclass DataType
|
|
96
|
+
(abstract capabilities) (concrete data)
|
|
97
|
+
───────────────── ────────────────
|
|
98
|
+
Semigroup → Monoid Option[A]
|
|
99
|
+
Foldable → Traversable Either[E, A]
|
|
100
|
+
Bifunctor Result[A], AsyncResult[A]
|
|
101
|
+
Functor → Applicative CList[A], Tree[A]
|
|
102
|
+
├→ Alternative frozendict[K, V]
|
|
103
|
+
└→ Monad → MonadError State[S, A], Reader[R, A]
|
|
104
|
+
Writer[W, A], Future[A]
|
|
105
|
+
Validated[E, A], ZipList[A]
|
|
106
|
+
|
|
107
|
+
INSTANCES (connect them)
|
|
108
|
+
────────────────────────
|
|
109
|
+
_OptionMonad(Monad, for_type=Option) — auto-registered
|
|
110
|
+
_ResultMonadError(MonadError, for_type=Result)
|
|
111
|
+
_CListAlternative(Alternative, for_type=CList)
|
|
112
|
+
_EitherBifunctor(Bifunctor, for_type=Either)
|
|
113
|
+
...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **`BaseTypeclass`** — root of all typeclasses. Provides AutoRegister
|
|
117
|
+
(`for_type=` keyword for automatic instance registration).
|
|
118
|
+
- **`DataType`** — root of all data types. Provides TypeConstructor
|
|
119
|
+
(auto `_type_constructor` detection) and DotNotation (dot-syntax dispatch).
|
|
120
|
+
- **Instances** — separate classes that implement a typeclass for a data type.
|
|
121
|
+
Only implement primitives (pure + bind); derived ops (map, ap) come from
|
|
122
|
+
the typeclass hierarchy.
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
#### Diagrams
|
|
127
|
+
|
|
128
|
+
**Semigroup** — associative combine (`+` being the canonical 'combine' operation)
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
A ─┐
|
|
132
|
+
├──( + )──> A
|
|
133
|
+
A ─┘
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Monoid** — semigroup with an identity element
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
A ─┐
|
|
140
|
+
├──( + )──> A (+ identity = A)
|
|
141
|
+
A ─┘
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Functor** — transform the value inside a context
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
F[A] ---( f: A -> B )---> F[B]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Applicative** — apply a function in context to a value in context
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
F[A → B] ─┐
|
|
154
|
+
├──ap──> F[B]
|
|
155
|
+
F[A] ──────┘
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Monad** — sequence computations that produce new contexts
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
F[A] ---( f: A -> F[B] )---> F[B]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Heavily influenced by [Scalaz](https://github.com/scalaz/scalaz) and
|
|
165
|
+
[Cats](https://typelevel.org/cats/).
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Python 3.12+ syntax for clarity. Library supports ≥3.10.
|
|
169
|
+
# In v2, typeclasses are INSTANCE classes (self = instance, fa = data).
|
|
170
|
+
# Data types extend DataType, NOT typeclasses.
|
|
171
|
+
|
|
172
|
+
# ── Value-level typeclasses (instantiated per use) ──
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True)
|
|
175
|
+
class Semigroup[A]:
|
|
176
|
+
typ: type
|
|
177
|
+
combine: Callable[[A, A], A]
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True)
|
|
180
|
+
class Monoid[A](Semigroup[A]):
|
|
181
|
+
empty: A
|
|
182
|
+
|
|
183
|
+
# ── Typeclass hierarchy (instance classes) ──
|
|
184
|
+
# F is the type constructor (Option, Result, etc.)
|
|
185
|
+
# A, B are value types; E is the error type
|
|
186
|
+
|
|
187
|
+
# pseudo-code type signatures; see exact impls
|
|
188
|
+
class Functor[F](BaseTypeclass):
|
|
189
|
+
def map(self, fa: F[A], f: Callable[[A], B]) -> F[B]: ...
|
|
190
|
+
|
|
191
|
+
class Applicative[F](Functor[F]):
|
|
192
|
+
def pure(self, value: A) -> F[A]: ...
|
|
193
|
+
def ap(self, ff: F[Callable[[A], B]], fa: F[A]) -> F[B]: ...
|
|
194
|
+
def map(self, fa: F[A], f: Callable[[A], B]) -> F[B]: ... # derived
|
|
195
|
+
def map2(self, fa: F[A], fb: F[B], f: Callable[[A, B], C]) -> F[C]: ...
|
|
196
|
+
def product(self, fa: F[A], fb: F[B]) -> F[tuple[A, B]]: ...
|
|
197
|
+
|
|
198
|
+
class Alternative[F](Applicative[F]):
|
|
199
|
+
def empty(self) -> F[A]: ...
|
|
200
|
+
def or_else(self, fa: F[A], fb: F[A]) -> F[A]: ...
|
|
201
|
+
|
|
202
|
+
class Monad[F](Applicative[F]):
|
|
203
|
+
def bind(self, fa: F[A], f: Callable[[A], F[B]]) -> F[B]: ...
|
|
204
|
+
def map(self, fa: F[A], f: Callable[[A], B]) -> F[B]: ... # derived
|
|
205
|
+
def ap(self, ff: F[Callable[[A], B]], fa: F[A]) -> F[B]: ... # derived
|
|
206
|
+
|
|
207
|
+
class MonadError[F, E](Monad[F]):
|
|
208
|
+
def raise_error(self, error: E) -> F[A]: ...
|
|
209
|
+
def handle_error_with(self, fa: F[A], f: Callable[[E], F[A]]) -> F[A]: ...
|
|
210
|
+
|
|
211
|
+
class Bifunctor[F](BaseTypeclass):
|
|
212
|
+
def bimap(self, fa: F[A, B], f: Callable[[A], C], g: Callable[[B], D]) -> F[C, D]: ...
|
|
213
|
+
def left_map(self, fa: F[A, B], f: Callable[[A], C]) -> F[C, B]: ... # derived
|
|
214
|
+
|
|
215
|
+
class Foldable[F](BaseTypeclass):
|
|
216
|
+
def fold_left(self, fa: F[A], acc: B, f: Callable[[B, A], B]) -> B: ...
|
|
217
|
+
def fold_right(self, fa: F[A], acc: B, f: Callable[[A, B], B]) -> B: ...
|
|
218
|
+
|
|
219
|
+
class Traversable[F](Foldable[F]):
|
|
220
|
+
def traverse(self, fa: F[A], f: Callable[[A], G[B]], G: Applicative) -> G[F[B]]: ...
|
|
221
|
+
def sequence(self, fga: F[G[A]], G: Applicative) -> G[F[A]]: ... # derived
|
|
222
|
+
|
|
223
|
+
# ── Data types (extend DataType, not typeclasses) ──
|
|
224
|
+
|
|
225
|
+
class Option(DataType, Generic[A]): ... # Some(value) | Nothing()
|
|
226
|
+
class Either(DataType, Generic[E, A]): ... # Right(value) | Left(error)
|
|
227
|
+
class Result(DataType, Generic[A]): ... # Ok(value) | Err(exception)
|
|
228
|
+
|
|
229
|
+
# ── Instances (connect typeclasses to data types) ──
|
|
230
|
+
|
|
231
|
+
class _OptionMonad(Monad, for_type=Option):
|
|
232
|
+
def pure(self, value: A) -> Option[A]:
|
|
233
|
+
return Some(value)
|
|
234
|
+
def bind(self, fa: Option[A], f: Callable[[A], Option[B]]) -> Option[B]:
|
|
235
|
+
match fa:
|
|
236
|
+
case Some(v): return f(v)
|
|
237
|
+
case Nothing(): return fa
|
|
238
|
+
# map, ap, product, then, map2 — all inherited from Monad hierarchy
|
|
239
|
+
|
|
240
|
+
# ── Experimental (monad transformers) ──
|
|
241
|
+
|
|
242
|
+
class MonadTransformer(ABC):
|
|
243
|
+
def bind(self, fa: MT[F, A], f: Callable[[A], MT[F, B]]) -> MT[F, B]: ...
|
|
244
|
+
def map(self, fa: MT[F, A], f: Callable[[A], B]) -> MT[F, B]: ...
|
|
245
|
+
def pure(cls, value: A, monad: type[F]) -> MT[F, A]: ...
|
|
246
|
+
def lift_f(cls, inner: F[A]) -> MT[F, A]: ...
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Instances (which data types implement which typeclasses)
|
|
250
|
+
|
|
251
|
+
| Data Type | Typeclasses |
|
|
252
|
+
| ------------------ | ------------------------------- |
|
|
253
|
+
| `Option[A]` | Monad, Alternative |
|
|
254
|
+
| `Either[E, A]` | MonadError, Bifunctor |
|
|
255
|
+
| `Result[A]` | MonadError, Bifunctor |
|
|
256
|
+
| `AsyncResult[A]` | MonadError, Bifunctor |
|
|
257
|
+
| `CList[A]` | Monad, Traversable, Alternative |
|
|
258
|
+
| `Tree[A]` | Functor, Foldable |
|
|
259
|
+
| `frozendict[K, V]` | Functor, Foldable |
|
|
260
|
+
| `Validated[E, A]` | Applicative, Bifunctor |
|
|
261
|
+
| `ZipList[A]` | Applicative |
|
|
262
|
+
| `State[S, A]` | Monad |
|
|
263
|
+
| `Reader[R, A]` | Monad |
|
|
264
|
+
| `Writer[W, A]` | Monad |
|
|
265
|
+
| `Future[A]` | Monad |
|
|
266
|
+
|
|
267
|
+
### Data Types
|
|
268
|
+
|
|
269
|
+
| Type | What it models |
|
|
270
|
+
| ------------------ | ----------------------------------------------- |
|
|
271
|
+
| `Option[A]` | Value might not exist |
|
|
272
|
+
| `Either[E, A]` | Value or typed error |
|
|
273
|
+
| `Result[A]` | Computation that can fail (`Ok`/`Err`) + `@Try` |
|
|
274
|
+
| `AsyncResult[A]` | Async computation that can fail + `@TryAsync` |
|
|
275
|
+
| `State[S, A]` | Stateful computation |
|
|
276
|
+
| `Reader[Ctx, A]` | Shared environment |
|
|
277
|
+
| `Writer[W, A]` | Accumulated output |
|
|
278
|
+
| `Validated[E, A]` | Error accumulation (applicative, not monad) |
|
|
279
|
+
| `Future[A]` | Lazy async computation |
|
|
280
|
+
| `CList[A]` | Persistent singly-linked list |
|
|
281
|
+
| `Tree[A]` | Immutable binary tree (functor only) |
|
|
282
|
+
| `frozendict[K, V]` | Persistent HAMT dictionary |
|
|
283
|
+
|
|
284
|
+
### Laws
|
|
285
|
+
|
|
286
|
+
Every implementation must satisfy these mathematical laws:
|
|
287
|
+
|
|
288
|
+
**Semigroup**
|
|
289
|
+
|
|
290
|
+
- Associativity: `(a + b) + c == a + (b + c)`
|
|
291
|
+
|
|
292
|
+
**Monoid**
|
|
293
|
+
|
|
294
|
+
- Left identity: `empty + a == a`
|
|
295
|
+
- Right identity: `a + empty == a`
|
|
296
|
+
|
|
297
|
+
**Functor**
|
|
298
|
+
|
|
299
|
+
- Identity: `fa.map(id) == fa`
|
|
300
|
+
- Composition: `fa.map(f).map(g) == fa.map(g ∘ f)`
|
|
301
|
+
|
|
302
|
+
**Applicative**
|
|
303
|
+
|
|
304
|
+
- Identity: `pure(id).ap(v) == v`
|
|
305
|
+
- Homomorphism: `pure(f).ap(pure(x)) == pure(f(x))`
|
|
306
|
+
- Interchange: `u.ap(pure(y)) == pure(λf. f(y)).ap(u)`
|
|
307
|
+
- Composition: `pure(∘).ap(u).ap(v).ap(w) == u.ap(v.ap(w))`
|
|
308
|
+
- Type preservation: `pure`, `map`, `ap` return the correct concrete type
|
|
309
|
+
|
|
310
|
+
**Monad**
|
|
311
|
+
|
|
312
|
+
- Left identity: `pure(a).bind(f) == f(a)`
|
|
313
|
+
- Right identity: `m.bind(pure) == m`
|
|
314
|
+
- Associativity: `m.bind(f).bind(g) == m.bind(λx. f(x).bind(g))`
|
|
315
|
+
|
|
316
|
+
**Alternative**
|
|
317
|
+
|
|
318
|
+
- Right identity: `fa.or_else(empty) == fa`
|
|
319
|
+
- Left identity: `empty.or_else(fa) == fa`
|
|
320
|
+
- Associativity: `a.or_else(b).or_else(c) == a.or_else(b.or_else(c))`
|
|
321
|
+
|
|
322
|
+
**Bifunctor**
|
|
323
|
+
|
|
324
|
+
- Identity: `bimap(id, id) == id`
|
|
325
|
+
- Composition: `bimap(f1 ∘ f2, g1 ∘ g2) == bimap(f1, g1) ∘ bimap(f2, g2)`
|
|
326
|
+
|
|
327
|
+
**Traversable**
|
|
328
|
+
|
|
329
|
+
- Identity: `traverse(fa, pure, G) == pure(fa)`
|
|
330
|
+
- Composition: `traverse(fa, f ∘ g, G) == traverse(traverse(fa, g, G), f, H)`
|
|
331
|
+
|
|
332
|
+
## Syntax notes
|
|
333
|
+
|
|
334
|
+
**Dot syntax** — the default, for everyday code:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from funstruct.monad.option import Some, Nothing
|
|
338
|
+
from funstruct.monad.result import Ok, Err
|
|
339
|
+
|
|
340
|
+
Some(10).map(lambda x: x * 2).bind(lambda x: Some(x + 1)) # Some(21)
|
|
341
|
+
Ok(10).map(str) # Ok('10')
|
|
342
|
+
Nothing().map(lambda x: x + 1) # Nothing()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**** dot notation is syntatic sugar over the following...
|
|
346
|
+
|
|
347
|
+
**Typeclass instances** — for generic, effect-polymorphic programs:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from funstruct.typeclasses import Monad, MonadError, summon
|
|
351
|
+
from funstruct.monad.option import Option, Some
|
|
352
|
+
from funstruct.monad.result import Result, Ok, Err
|
|
353
|
+
|
|
354
|
+
# F: Monad = the typeclass instance (constraint / trait bound)
|
|
355
|
+
# fa: F[A] = a value in the monadic context (Some(21), Ok(21), etc.)
|
|
356
|
+
def double(F: Monad, fa):
|
|
357
|
+
return F.map(fa, lambda x: x * 2)
|
|
358
|
+
|
|
359
|
+
double(summon(Monad, Option), Some(21)) # Some(42)
|
|
360
|
+
double(summon(Monad, Result), Ok(21)) # Ok(42)
|
|
361
|
+
|
|
362
|
+
# F: MonadError adds raise_error + handle_error_with
|
|
363
|
+
def safe_divide(F: MonadError, a: float, b: float):
|
|
364
|
+
if b == 0:
|
|
365
|
+
return F.raise_error(ValueError("division by zero"))
|
|
366
|
+
return F.pure(a / b)
|
|
367
|
+
|
|
368
|
+
safe_divide(summon(MonadError, Result), 10, 2) # Ok(5.0)
|
|
369
|
+
safe_divide(summon(MonadError, Result), 10, 0) # Err(ValueError(...))
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Data types are plain — they don't inherit from typeclasses. Typeclass
|
|
373
|
+
instances are separate classes that implement the operations. `summon`
|
|
374
|
+
resolves the right instance from a registry. Dot syntax is sugar on
|
|
375
|
+
top — `Some(10).map(f)` delegates to `summon(Monad, Option).map(Some(10), f)`
|
|
376
|
+
internally.
|
|
377
|
+
|
|
378
|
+
## Why no IO type?
|
|
379
|
+
|
|
380
|
+
In Haskell, `IO` exists because the language is purely functional — there is
|
|
381
|
+
no way to perform side effects without wrapping them in the `IO` monad. The
|
|
382
|
+
type system enforces purity: if a function doesn't return `IO`, it cannot
|
|
383
|
+
touch the network, filesystem, or mutable state.
|
|
384
|
+
|
|
385
|
+
Python has no such constraint. Any function can perform side effects at any
|
|
386
|
+
time. An `IO` wrapper in Python would be:
|
|
387
|
+
|
|
388
|
+
1. **Unenforceable** — nothing stops you from doing I/O outside the wrapper.
|
|
389
|
+
The type system can't prevent `print()` in a "pure" function.
|
|
390
|
+
1. **Purely ceremonial** — it adds a wrapper you must manually construct and
|
|
391
|
+
unwrap, but provides no guarantee. It's a comment dressed as a type.
|
|
392
|
+
1. **Redundant with async** — Python's `async/await` already separates
|
|
393
|
+
"description of a computation" from "execution of that computation,"
|
|
394
|
+
which is most of what `IO` provides in Haskell.
|
|
395
|
+
|
|
396
|
+
Instead, funstruct provides:
|
|
397
|
+
|
|
398
|
+
- **`Either[E, A]`** / **`Result[A]`** — for operations that might fail
|
|
399
|
+
- **`Future[A]`** / **`AsyncResult[A]`** — for async operations (with or without error handling)
|
|
400
|
+
- **`@Try` / `@TryAsync`** — for wrapping exception-throwing code at boundaries
|
|
401
|
+
|
|
402
|
+
These give you the composition benefits of monadic pipelines where they
|
|
403
|
+
matter (error handling, async sequencing) without pretending Python is
|
|
404
|
+
something it isn't.
|
|
405
|
+
|
|
406
|
+
## Higher-kinded types
|
|
407
|
+
|
|
408
|
+
In Haskell and Scala, higher-kinded types (HKTs) let you abstract over
|
|
409
|
+
type constructors — writing one generic `sequence` that works for any
|
|
410
|
+
`Traversable` + `Applicative` combination.
|
|
411
|
+
|
|
412
|
+
Python's type system does not support HKTs natively. funstruct achieves
|
|
413
|
+
the same effect at runtime through the typeclass instance pattern:
|
|
414
|
+
|
|
415
|
+
- **Type constructors** are represented by the class itself (`Option`,
|
|
416
|
+
`Result`, `Either`). Each data type sets `_type_constructor` so
|
|
417
|
+
variants resolve to their base: `tc_of(Some(42))` → `Option`.
|
|
418
|
+
- **Typeclass resolution** via `summon(Monad, Option)` returns the
|
|
419
|
+
registered instance, just like Scala's `summon[Monad[Option]]`.
|
|
420
|
+
- **Generic functions** use the instance directly:
|
|
421
|
+
`def double(F: Monad, fa): return F.map(fa, lambda x: x * 2)`
|
|
422
|
+
- **Dot syntax** delegates to summon internally:
|
|
423
|
+
`Some(10).map(f)` → `summon(Functor, Option).map(Some(10), f)`
|
|
424
|
+
|
|
425
|
+
This gives funstruct Haskell-style typeclass resolution and Scala-style
|
|
426
|
+
tagless final — without HKT encoding tricks, metaclass magic, or
|
|
427
|
+
compiler plugins. The tradeoff: trait bounds are enforced at runtime
|
|
428
|
+
(via `summon`), not at compile time.
|
|
429
|
+
|
|
430
|
+
## Experimental
|
|
431
|
+
|
|
432
|
+
Experimental modules live in `funstruct.experimental`. APIs may change.
|
|
433
|
+
|
|
434
|
+
### Monad Transformers
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
from funstruct.experimental.monadtransformer import (
|
|
438
|
+
ReaderT, StateT, EitherT, OptionT, WriterT,
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Transformers combine effects by wrapping one monad inside another.
|
|
443
|
+
For most use cases, plain monads with `do`-notation and `fold` are
|
|
444
|
+
sufficient. Reach for transformers only when you need to combine
|
|
445
|
+
multiple effects in a single pipeline.
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
ReaderT[F, Ctx, A] = Ctx -> F[A] (environment + F's effects)
|
|
449
|
+
StateT[F, S, A] = S -> F[(S, A)] (state + F's effects)
|
|
450
|
+
EitherT[F, E, A] = F[Either[E, A]] (errors + F's effects)
|
|
451
|
+
OptionT[F, A] = F[Option[A]] (absence + F's effects)
|
|
452
|
+
WriterT[F, W, A] = F[(A, W)] (output + F's effects)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Optics (Lenses)
|
|
456
|
+
|
|
457
|
+
```python
|
|
458
|
+
from funstruct.experimental.optics import Lens, at
|
|
459
|
+
from funstruct.collections.frozendict import frozendict
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Lenses let you read and update deeply nested immutable structures
|
|
463
|
+
without manually rebuilding the path at every level.
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
config = frozendict({
|
|
467
|
+
"app": {
|
|
468
|
+
"users": {
|
|
469
|
+
"alice": {"email": "alice@old.com", "role": "admin"},
|
|
470
|
+
},
|
|
471
|
+
"settings": {"version": 2},
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
email_lens = at("app") >> at("users") >> at("alice") >> at("email")
|
|
476
|
+
email_lens.get(config) # "alice@old.com"
|
|
477
|
+
email_lens.set(config, "alice@new.com") # rebuilds the path
|
|
478
|
+
email_lens.modify(config, str.upper) # "ALICE@OLD.COM"
|
|
479
|
+
|
|
480
|
+
version_lens = at("app") >> at("settings") >> at("version")
|
|
481
|
+
version_lens.modify(config, lambda v: v + 1) # bumps to 3
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Roadmap
|
|
485
|
+
|
|
486
|
+
- **Interactive demos** — browser-runnable examples via PyScript/Pyodide. Edit and run funstruct code directly in the docs.
|
|
487
|
+
- **Documentation site** — expanded static site (Astro/Next.js/etc) with guides, interactive demos, and API reference.
|
|
488
|
+
- **Functional collections** — persistent queue, deque, red-black tree, persistent stack, heap
|
|
489
|
+
- **Native collections (Rust/PyO3)** — Rust-backed CList, frozendict via `funstruct[native]`.
|
|
490
|
+
- **Typeclass derivation** — auto-generate Functor/Foldable instances from dataclass structure.
|
|
491
|
+
- **Parser combinators** — monadic parser library (`funstruct.experimental.parsing`).
|
|
492
|
+
- **Python 3.12+ minimum** — rewrite type signatures using `type X[A, B] = ...` aliases and `class Foo[A]:` syntax.
|
|
493
|
+
- **Free monad** — build program ASTs, interpret with different backends.
|
|
494
|
+
- **Effects system** — algebraic effects as an alternative to monad transformer stacks.
|
|
495
|
+
- **Stream** — infinite streams, lazy evaluation.
|
|
496
|
+
- **Pydantic integration** — more native integration with BaseModel, frozendict, lens, validated, etc
|