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.
Files changed (230) hide show
  1. {funstruct-1.0.2 → funstruct-2.0.0a1}/.gitignore +2 -0
  2. {funstruct-1.0.2 → funstruct-2.0.0a1}/.pre-commit-config.yaml +6 -0
  3. funstruct-2.0.0a1/PKG-INFO +496 -0
  4. funstruct-2.0.0a1/README.md +482 -0
  5. funstruct-2.0.0a1/benchmarks/test_bench_typeclasses.py +203 -0
  6. funstruct-2.0.0a1/demoplayground/index.html +927 -0
  7. funstruct-2.0.0a1/demos/01_result_pipeline.py +72 -0
  8. funstruct-2.0.0a1/demos/02_do_notation.py +132 -0
  9. funstruct-2.0.0a1/demos/03_tagless_final_intro.py +108 -0
  10. funstruct-2.0.0a1/demos/04_tagless_final_db.py +180 -0
  11. funstruct-2.0.0a1/demos/05_json_encoder.py +200 -0
  12. funstruct-2.0.0a1/demos/06_generic_functions.py +170 -0
  13. funstruct-2.0.0a1/demos/07_reader_vs_cake.py +202 -0
  14. funstruct-2.0.0a1/demos/08_custom_types.py +310 -0
  15. funstruct-2.0.0a1/demos/09_lenses.py +103 -0
  16. funstruct-2.0.0a1/demos/10_writer_stacktrace.py +93 -0
  17. funstruct-2.0.0a1/demos/11_state_counter.py +111 -0
  18. funstruct-2.0.0a1/demos/12_trait_bounds.py +211 -0
  19. funstruct-2.0.0a1/demos/13_dsl.py +250 -0
  20. funstruct-2.0.0a1/demos/14_dsl_initial_vs_final.py +235 -0
  21. funstruct-2.0.0a1/demos/15_typeclass_pattern_json.py +239 -0
  22. funstruct-2.0.0a1/demos/README.md +33 -0
  23. funstruct-2.0.0a1/demos/transformers/01_the_problem.py +75 -0
  24. funstruct-2.0.0a1/demos/transformers/02_option_t.py +77 -0
  25. funstruct-2.0.0a1/demos/transformers/03_alternative.py +82 -0
  26. funstruct-2.0.0a1/demos/transformers/README.md +12 -0
  27. funstruct-2.0.0a1/docs/applicative/index.md +4 -0
  28. funstruct-2.0.0a1/docs/applicative/validated.md +3 -0
  29. funstruct-2.0.0a1/docs/applicative/ziplist.md +3 -0
  30. funstruct-2.0.0a1/docs/collections/cons.md +3 -0
  31. funstruct-2.0.0a1/docs/collections/frozendict.md +3 -0
  32. funstruct-2.0.0a1/docs/collections/index.md +5 -0
  33. funstruct-2.0.0a1/docs/experimental/monadtransformer/either_t.md +3 -0
  34. funstruct-2.0.0a1/docs/experimental/monadtransformer/index.md +9 -0
  35. funstruct-2.0.0a1/docs/experimental/monadtransformer/option_t.md +3 -0
  36. funstruct-2.0.0a1/docs/experimental/monadtransformer/reader_t.md +3 -0
  37. funstruct-2.0.0a1/docs/experimental/monadtransformer/state_t.md +3 -0
  38. funstruct-2.0.0a1/docs/experimental/monadtransformer/writer_t.md +3 -0
  39. funstruct-2.0.0a1/docs/experimental/optics/index.md +3 -0
  40. funstruct-2.0.0a1/docs/guides/antipatterns.md +229 -0
  41. funstruct-2.0.0a1/docs/guides/async.md +138 -0
  42. funstruct-2.0.0a1/docs/guides/creating_a_data_type.md +271 -0
  43. funstruct-2.0.0a1/docs/guides/do_notation.md +175 -0
  44. funstruct-2.0.0a1/docs/guides/evaluation_strategy.md +92 -0
  45. funstruct-2.0.0a1/docs/guides/generic_programming.md +158 -0
  46. funstruct-2.0.0a1/docs/guides/monads.md +197 -0
  47. funstruct-2.0.0a1/docs/guides/railway_oriented_programming.md +155 -0
  48. funstruct-2.0.0a1/docs/guides/tagless_final.md +201 -0
  49. funstruct-2.0.0a1/docs/guides/typeclass_pattern_json.md +155 -0
  50. funstruct-2.0.0a1/docs/guides/typeclass_pattern_ordering.md +171 -0
  51. funstruct-2.0.0a1/docs/index.md +1 -0
  52. funstruct-2.0.0a1/docs/monad/index.md +9 -0
  53. funstruct-2.0.0a1/docs/monad/state.md +3 -0
  54. funstruct-2.0.0a1/docs/playground.html +927 -0
  55. funstruct-2.0.0a1/docs/typeclass/alternative.md +3 -0
  56. funstruct-2.0.0a1/docs/typeclass/applicative.md +3 -0
  57. funstruct-2.0.0a1/docs/typeclass/bifunctor.md +3 -0
  58. funstruct-2.0.0a1/docs/typeclass/foldable.md +3 -0
  59. funstruct-2.0.0a1/docs/typeclass/functor.md +3 -0
  60. funstruct-2.0.0a1/docs/typeclass/index.md +12 -0
  61. funstruct-2.0.0a1/docs/typeclass/monad.md +3 -0
  62. funstruct-2.0.0a1/docs/typeclass/monad_error.md +3 -0
  63. funstruct-2.0.0a1/docs/typeclass/monoid.md +3 -0
  64. funstruct-2.0.0a1/docs/typeclass/semigroup.md +3 -0
  65. funstruct-2.0.0a1/docs/typeclass/traversable.md +3 -0
  66. funstruct-2.0.0a1/docs/typeclasses.svg +87 -0
  67. funstruct-2.0.0a1/docs/util/created_at.md +3 -0
  68. funstruct-2.0.0a1/docs/util/tailrec.md +3 -0
  69. funstruct-2.0.0a1/funstruct/applicative/validated/__init__.py +115 -0
  70. funstruct-2.0.0a1/funstruct/applicative/validated/instances.py +72 -0
  71. funstruct-2.0.0a1/funstruct/applicative/ziplist/__init__.py +67 -0
  72. funstruct-2.0.0a1/funstruct/applicative/ziplist/instances.py +34 -0
  73. funstruct-2.0.0a1/funstruct/collections/cons/__init__.py +388 -0
  74. funstruct-2.0.0a1/funstruct/collections/cons/instances.py +64 -0
  75. funstruct-1.0.2/funstruct/collections/frozendict.py → funstruct-2.0.0a1/funstruct/collections/frozendict/__init__.py +89 -45
  76. funstruct-2.0.0a1/funstruct/collections/frozendict/instances.py +28 -0
  77. funstruct-1.0.2/funstruct/collections/tree.py → funstruct-2.0.0a1/funstruct/collections/tree/__init__.py +64 -21
  78. funstruct-2.0.0a1/funstruct/collections/tree/instances.py +45 -0
  79. funstruct-2.0.0a1/funstruct/experimental/__init__.py +4 -0
  80. funstruct-2.0.0a1/funstruct/experimental/monadtransformer/__init__.py +57 -0
  81. funstruct-1.0.2/funstruct/typeclasses/_monad_transformer.py → funstruct-2.0.0a1/funstruct/experimental/monadtransformer/_typeclass.py +57 -20
  82. {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/either_t.py +95 -42
  83. {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/option_t.py +85 -39
  84. {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/reader_t.py +64 -34
  85. {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/state_t.py +71 -42
  86. {funstruct-1.0.2/funstruct → funstruct-2.0.0a1/funstruct/experimental}/monadtransformer/writer_t.py +81 -43
  87. funstruct-2.0.0a1/funstruct/experimental/optics/__init__.py +29 -0
  88. funstruct-2.0.0a1/funstruct/experimental/optics/_lens.py +101 -0
  89. funstruct-2.0.0a1/funstruct/monad/either/__init__.py +226 -0
  90. funstruct-2.0.0a1/funstruct/monad/either/instances.py +56 -0
  91. funstruct-2.0.0a1/funstruct/monad/future/__init__.py +82 -0
  92. funstruct-2.0.0a1/funstruct/monad/future/instances.py +20 -0
  93. funstruct-1.0.2/funstruct/monad/option.py → funstruct-2.0.0a1/funstruct/monad/option/__init__.py +49 -94
  94. funstruct-2.0.0a1/funstruct/monad/option/instances.py +57 -0
  95. funstruct-1.0.2/funstruct/monad/reader.py → funstruct-2.0.0a1/funstruct/monad/reader/__init__.py +24 -39
  96. funstruct-2.0.0a1/funstruct/monad/reader/instances.py +25 -0
  97. funstruct-2.0.0a1/funstruct/monad/result/__init__.py +418 -0
  98. funstruct-2.0.0a1/funstruct/monad/result/instances.py +71 -0
  99. funstruct-2.0.0a1/funstruct/monad/state/__init__.py +113 -0
  100. funstruct-2.0.0a1/funstruct/monad/state/instances.py +24 -0
  101. funstruct-2.0.0a1/funstruct/monad/writer/__init__.py +160 -0
  102. funstruct-2.0.0a1/funstruct/monad/writer/instances.py +46 -0
  103. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/monoid/__init__.py +1 -0
  104. funstruct-2.0.0a1/funstruct/typeclasses/__init__.py +37 -0
  105. funstruct-2.0.0a1/funstruct/typeclasses/alternative.py +24 -0
  106. funstruct-2.0.0a1/funstruct/typeclasses/applicative.py +36 -0
  107. funstruct-2.0.0a1/funstruct/typeclasses/bifunctor.py +25 -0
  108. funstruct-2.0.0a1/funstruct/typeclasses/foldable.py +25 -0
  109. funstruct-2.0.0a1/funstruct/typeclasses/functor.py +21 -0
  110. funstruct-2.0.0a1/funstruct/typeclasses/mixins/__init__.py +9 -0
  111. funstruct-2.0.0a1/funstruct/typeclasses/mixins/auto_register.py +51 -0
  112. funstruct-2.0.0a1/funstruct/typeclasses/mixins/data_type.py +43 -0
  113. funstruct-2.0.0a1/funstruct/typeclasses/mixins/dot_notation.py +62 -0
  114. funstruct-2.0.0a1/funstruct/typeclasses/mixins/type_constructor.py +46 -0
  115. funstruct-2.0.0a1/funstruct/typeclasses/monad.py +33 -0
  116. funstruct-2.0.0a1/funstruct/typeclasses/monad_error.py +27 -0
  117. funstruct-1.0.2/funstruct/typeclasses/_monoid.py → funstruct-2.0.0a1/funstruct/typeclasses/monoid.py +1 -1
  118. funstruct-2.0.0a1/funstruct/typeclasses/traversable.py +26 -0
  119. funstruct-2.0.0a1/funstruct/typeclasses/typeclass.py +30 -0
  120. funstruct-2.0.0a1/funstruct/typeclasses/utils/__init__.py +14 -0
  121. funstruct-2.0.0a1/funstruct/typeclasses/utils/registry.py +112 -0
  122. funstruct-2.0.0a1/funstruct/util/_reawaitable.py +50 -0
  123. funstruct-2.0.0a1/funstruct/util/created_at.py +97 -0
  124. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/util/tailrec.py +33 -0
  125. {funstruct-1.0.2 → funstruct-2.0.0a1}/justfile +12 -2
  126. funstruct-2.0.0a1/mkdocs.yml +90 -0
  127. {funstruct-1.0.2 → funstruct-2.0.0a1}/noxfile.py +21 -1
  128. {funstruct-1.0.2 → funstruct-2.0.0a1}/pyproject.toml +20 -4
  129. funstruct-2.0.0a1/scripts/generate_diagram.py +320 -0
  130. funstruct-2.0.0a1/scripts/generate_playground.py +191 -0
  131. {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_either_t.py +61 -11
  132. funstruct-2.0.0a1/tests/experimental/test_optics/__init__.py +0 -0
  133. funstruct-2.0.0a1/tests/experimental/test_optics/test_optics_frozendict.py +105 -0
  134. {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_option_t.py +180 -75
  135. {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_reader_t.py +43 -17
  136. {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_state_t.py +44 -21
  137. {funstruct-1.0.2/tests → funstruct-2.0.0a1/tests/experimental}/test_writer_t.py +21 -7
  138. funstruct-2.0.0a1/tests/laws.py +242 -0
  139. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_cons.py +158 -3
  140. funstruct-2.0.0a1/tests/test_do_bind_equivalence.py +210 -0
  141. funstruct-2.0.0a1/tests/test_either.py +320 -0
  142. funstruct-2.0.0a1/tests/test_foldable.py +53 -0
  143. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_frozendict.py +234 -4
  144. funstruct-2.0.0a1/tests/test_future.py +676 -0
  145. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_law_violations.py +11 -7
  146. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_option.py +122 -9
  147. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_reader.py +21 -7
  148. funstruct-2.0.0a1/tests/test_result.py +287 -0
  149. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_state.py +17 -3
  150. funstruct-2.0.0a1/tests/test_summon.py +248 -0
  151. funstruct-2.0.0a1/tests/test_tagless_final.py +192 -0
  152. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_tailrec.py +36 -1
  153. funstruct-2.0.0a1/tests/test_trait_bounds.py +209 -0
  154. funstruct-2.0.0a1/tests/test_traversable.py +101 -0
  155. funstruct-2.0.0a1/tests/test_tree.py +176 -0
  156. funstruct-2.0.0a1/tests/test_validated.py +481 -0
  157. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/test_writer.py +6 -6
  158. funstruct-2.0.0a1/tests/test_ziplist.py +102 -0
  159. funstruct-2.0.0a1/typeclasses.svg +1 -0
  160. {funstruct-1.0.2 → funstruct-2.0.0a1}/uv.lock +74 -1
  161. funstruct-1.0.2/.claude/settings.local.json +0 -8
  162. funstruct-1.0.2/PKG-INFO +0 -253
  163. funstruct-1.0.2/README.md +0 -239
  164. funstruct-1.0.2/docs/applicative/index.md +0 -10
  165. funstruct-1.0.2/docs/applicative/validated.md +0 -11
  166. funstruct-1.0.2/docs/collections/cons.md +0 -11
  167. funstruct-1.0.2/docs/collections/frozendict.md +0 -7
  168. funstruct-1.0.2/docs/collections/index.md +0 -10
  169. funstruct-1.0.2/docs/functor/index.md +0 -3
  170. funstruct-1.0.2/docs/index.md +0 -150
  171. funstruct-1.0.2/docs/monad/index.md +0 -15
  172. funstruct-1.0.2/docs/monad/state.md +0 -20
  173. funstruct-1.0.2/docs/monadtransformer/either_t.md +0 -3
  174. funstruct-1.0.2/docs/monadtransformer/index.md +0 -13
  175. funstruct-1.0.2/docs/monadtransformer/option_t.md +0 -3
  176. funstruct-1.0.2/docs/monadtransformer/reader_t.md +0 -3
  177. funstruct-1.0.2/docs/monadtransformer/state_t.md +0 -26
  178. funstruct-1.0.2/docs/monadtransformer/writer_t.md +0 -3
  179. funstruct-1.0.2/docs/typeclass/applicative.md +0 -3
  180. funstruct-1.0.2/docs/typeclass/index.md +0 -21
  181. funstruct-1.0.2/docs/typeclass/monad.md +0 -3
  182. funstruct-1.0.2/docs/typeclass/monadtransformer.md +0 -3
  183. funstruct-1.0.2/docs/typeclass/monoid.md +0 -3
  184. funstruct-1.0.2/docs/typeclass/semigroup.md +0 -3
  185. funstruct-1.0.2/docs/util/tailrec.md +0 -24
  186. funstruct-1.0.2/funstruct/applicative/validated.py +0 -182
  187. funstruct-1.0.2/funstruct/collections/cons.py +0 -701
  188. funstruct-1.0.2/funstruct/monad/either.py +0 -294
  189. funstruct-1.0.2/funstruct/monad/future.py +0 -78
  190. funstruct-1.0.2/funstruct/monad/result.py +0 -327
  191. funstruct-1.0.2/funstruct/monad/state.py +0 -144
  192. funstruct-1.0.2/funstruct/monad/writer.py +0 -121
  193. funstruct-1.0.2/funstruct/monadtransformer/__init__.py +0 -30
  194. funstruct-1.0.2/funstruct/typeclasses/__init__.py +0 -17
  195. funstruct-1.0.2/funstruct/typeclasses/_applicative.py +0 -59
  196. funstruct-1.0.2/funstruct/typeclasses/_functor.py +0 -59
  197. funstruct-1.0.2/funstruct/typeclasses/_monad.py +0 -83
  198. funstruct-1.0.2/mkdocs.yml +0 -71
  199. funstruct-1.0.2/tests/laws.py +0 -187
  200. funstruct-1.0.2/tests/test_either.py +0 -200
  201. funstruct-1.0.2/tests/test_future.py +0 -379
  202. funstruct-1.0.2/tests/test_result.py +0 -62
  203. funstruct-1.0.2/tests/test_tree.py +0 -88
  204. funstruct-1.0.2/tests/test_validated.py +0 -347
  205. {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/ci.yml +0 -0
  206. {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/docs.yml +0 -0
  207. {funstruct-1.0.2 → funstruct-2.0.0a1}/.github/workflows/publish.yml +0 -0
  208. {funstruct-1.0.2 → funstruct-2.0.0a1}/CONTRIBUTING.md +0 -0
  209. {funstruct-1.0.2 → funstruct-2.0.0a1}/LICENSE +0 -0
  210. {funstruct-1.0.2 → funstruct-2.0.0a1}/benchmarks/test_bench_collections.py +0 -0
  211. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/collections/tree.md +0 -0
  212. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/either.md +0 -0
  213. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/future.md +0 -0
  214. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/option.md +0 -0
  215. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/reader.md +0 -0
  216. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/result.md +0 -0
  217. {funstruct-1.0.2 → funstruct-2.0.0a1}/docs/monad/writer.md +0 -0
  218. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/__init__.py +0 -0
  219. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/applicative/__init__.py +0 -0
  220. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/collections/__init__.py +0 -0
  221. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/functor/__init__.py +0 -0
  222. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/monad/__init__.py +0 -0
  223. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/py.typed +0 -0
  224. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/semigroup/__init__.py +0 -0
  225. /funstruct-1.0.2/funstruct/typeclasses/_semigroup.py → /funstruct-2.0.0a1/funstruct/typeclasses/semigroup.py +0 -0
  226. {funstruct-1.0.2 → funstruct-2.0.0a1}/funstruct/util/__init__.py +0 -0
  227. {funstruct-1.0.2 → funstruct-2.0.0a1}/mise.toml +0 -0
  228. {funstruct-1.0.2 → funstruct-2.0.0a1}/tests/__init__.py +0 -0
  229. /funstruct-1.0.2/docs/functor/.gitkeep → /funstruct-2.0.0a1/tests/experimental/__init__.py +0 -0
  230. {funstruct-1.0.2 → funstruct-2.0.0a1}/uv.toml +0 -0
@@ -134,3 +134,5 @@ coc-settings.json
134
134
  .vimspector.json
135
135
  notes.txt
136
136
  .benchmarks/
137
+ .claude
138
+ playground/
@@ -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
+ ![funstruct typeclass hierarchy](typeclasses.svg)
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