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.
- funstruct-1.0.0/.claude/settings.local.json +8 -0
- funstruct-1.0.0/.github/workflows/ci.yml +33 -0
- funstruct-1.0.0/.github/workflows/publish.yml +18 -0
- funstruct-1.0.0/.gitignore +136 -0
- funstruct-1.0.0/.pre-commit-config.yaml +30 -0
- funstruct-1.0.0/CONTRIBUTING.md +11 -0
- funstruct-1.0.0/PKG-INFO +251 -0
- funstruct-1.0.0/README.md +238 -0
- funstruct-1.0.0/benchmarks/test_bench_collections.py +151 -0
- funstruct-1.0.0/docs/applicative/index.md +10 -0
- funstruct-1.0.0/docs/applicative/validated.md +11 -0
- funstruct-1.0.0/docs/collections/cons.md +11 -0
- funstruct-1.0.0/docs/collections/frozendict.md +7 -0
- funstruct-1.0.0/docs/collections/index.md +10 -0
- funstruct-1.0.0/docs/collections/tree.md +3 -0
- funstruct-1.0.0/docs/functor/index.md +3 -0
- funstruct-1.0.0/docs/index.md +150 -0
- funstruct-1.0.0/docs/monad/either.md +3 -0
- funstruct-1.0.0/docs/monad/future.md +3 -0
- funstruct-1.0.0/docs/monad/index.md +15 -0
- funstruct-1.0.0/docs/monad/option.md +3 -0
- funstruct-1.0.0/docs/monad/reader.md +3 -0
- funstruct-1.0.0/docs/monad/result.md +3 -0
- funstruct-1.0.0/docs/monad/state.md +20 -0
- funstruct-1.0.0/docs/monad/writer.md +3 -0
- funstruct-1.0.0/docs/monadtransformer/either_t.md +3 -0
- funstruct-1.0.0/docs/monadtransformer/index.md +13 -0
- funstruct-1.0.0/docs/monadtransformer/option_t.md +3 -0
- funstruct-1.0.0/docs/monadtransformer/reader_t.md +3 -0
- funstruct-1.0.0/docs/monadtransformer/state_t.md +26 -0
- funstruct-1.0.0/docs/monadtransformer/writer_t.md +3 -0
- funstruct-1.0.0/docs/typeclass/applicative.md +3 -0
- funstruct-1.0.0/docs/typeclass/index.md +21 -0
- funstruct-1.0.0/docs/typeclass/monad.md +3 -0
- funstruct-1.0.0/docs/typeclass/monadtransformer.md +3 -0
- funstruct-1.0.0/docs/typeclass/monoid.md +3 -0
- funstruct-1.0.0/docs/typeclass/semigroup.md +3 -0
- funstruct-1.0.0/docs/util/tailrec.md +24 -0
- funstruct-1.0.0/funstruct/__init__.py +1 -0
- funstruct-1.0.0/funstruct/applicative/__init__.py +1 -0
- funstruct-1.0.0/funstruct/applicative/validated.py +169 -0
- funstruct-1.0.0/funstruct/collections/__init__.py +1 -0
- {funstruct-0.1.2/funstruct → funstruct-1.0.0/funstruct/collections}/cons.py +262 -222
- funstruct-1.0.0/funstruct/collections/frozendict.py +367 -0
- funstruct-1.0.0/funstruct/collections/tree.py +148 -0
- funstruct-1.0.0/funstruct/functor/__init__.py +1 -0
- funstruct-1.0.0/funstruct/monad/__init__.py +11 -0
- funstruct-1.0.0/funstruct/monad/either.py +294 -0
- funstruct-1.0.0/funstruct/monad/future.py +78 -0
- funstruct-1.0.0/funstruct/monad/option.py +270 -0
- funstruct-1.0.0/funstruct/monad/reader.py +105 -0
- funstruct-1.0.0/funstruct/monad/result.py +298 -0
- funstruct-1.0.0/funstruct/monad/state.py +144 -0
- funstruct-1.0.0/funstruct/monad/writer.py +121 -0
- funstruct-1.0.0/funstruct/monadtransformer/__init__.py +30 -0
- funstruct-1.0.0/funstruct/monadtransformer/either_t.py +185 -0
- funstruct-1.0.0/funstruct/monadtransformer/option_t.py +232 -0
- funstruct-1.0.0/funstruct/monadtransformer/reader_t.py +191 -0
- funstruct-1.0.0/funstruct/monadtransformer/state_t.py +202 -0
- funstruct-1.0.0/funstruct/monadtransformer/writer_t.py +204 -0
- funstruct-1.0.0/funstruct/monoid/__init__.py +21 -0
- funstruct-1.0.0/funstruct/py.typed +0 -0
- funstruct-1.0.0/funstruct/semigroup/__init__.py +19 -0
- funstruct-1.0.0/funstruct/typeclasses/__init__.py +17 -0
- funstruct-1.0.0/funstruct/typeclasses/_applicative.py +59 -0
- funstruct-1.0.0/funstruct/typeclasses/_functor.py +59 -0
- funstruct-1.0.0/funstruct/typeclasses/_monad.py +83 -0
- funstruct-1.0.0/funstruct/typeclasses/_monad_transformer.py +112 -0
- funstruct-1.0.0/funstruct/typeclasses/_monoid.py +44 -0
- funstruct-1.0.0/funstruct/typeclasses/_semigroup.py +36 -0
- funstruct-1.0.0/funstruct/util/__init__.py +1 -0
- funstruct-1.0.0/funstruct/util/tailrec.py +69 -0
- funstruct-1.0.0/justfile +61 -0
- funstruct-1.0.0/mise.toml +9 -0
- funstruct-1.0.0/mkdocs.yml +67 -0
- funstruct-1.0.0/noxfile.py +31 -0
- funstruct-1.0.0/pyproject.toml +88 -0
- funstruct-1.0.0/tests/__init__.py +0 -0
- funstruct-1.0.0/tests/laws.py +187 -0
- funstruct-1.0.0/tests/test_cons.py +872 -0
- funstruct-1.0.0/tests/test_either.py +200 -0
- funstruct-1.0.0/tests/test_either_t.py +245 -0
- funstruct-1.0.0/tests/test_frozendict.py +617 -0
- funstruct-1.0.0/tests/test_future.py +379 -0
- funstruct-1.0.0/tests/test_law_violations.py +280 -0
- funstruct-1.0.0/tests/test_option.py +178 -0
- funstruct-1.0.0/tests/test_option_t.py +244 -0
- funstruct-1.0.0/tests/test_reader.py +187 -0
- funstruct-1.0.0/tests/test_reader_t.py +293 -0
- funstruct-1.0.0/tests/test_result.py +62 -0
- funstruct-1.0.0/tests/test_state.py +203 -0
- funstruct-1.0.0/tests/test_state_t.py +304 -0
- funstruct-1.0.0/tests/test_tailrec.py +83 -0
- funstruct-1.0.0/tests/test_tree.py +88 -0
- funstruct-1.0.0/tests/test_validated.py +347 -0
- funstruct-1.0.0/tests/test_writer.py +199 -0
- funstruct-1.0.0/tests/test_writer_t.py +254 -0
- funstruct-1.0.0/uv.lock +1507 -0
- funstruct-0.1.2/PKG-INFO +0 -28
- funstruct-0.1.2/README.md +0 -9
- funstruct-0.1.2/funstruct/frozendict.py +0 -268
- funstruct-0.1.2/pyproject.toml +0 -27
- {funstruct-0.1.2 → funstruct-1.0.0}/LICENSE +0 -0
- /funstruct-0.1.2/funstruct/__init__.py → /funstruct-1.0.0/docs/functor/.gitkeep +0 -0
|
@@ -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`
|
funstruct-1.0.0/PKG-INFO
ADDED
|
@@ -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.
|