functorial 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. functorial-0.1.1/.claude/settings.local.json +7 -0
  2. functorial-0.1.1/.dir-locals.el +4 -0
  3. functorial-0.1.1/.flake8 +3 -0
  4. functorial-0.1.1/.gitignore +25 -0
  5. functorial-0.1.1/.pylintrc +46 -0
  6. functorial-0.1.1/.python-version +1 -0
  7. functorial-0.1.1/LICENSE +21 -0
  8. functorial-0.1.1/PKG-INFO +68 -0
  9. functorial-0.1.1/README.md +51 -0
  10. functorial-0.1.1/pyproject.toml +122 -0
  11. functorial-0.1.1/src/functorial/__about__.py +4 -0
  12. functorial-0.1.1/src/functorial/__init__.py +29 -0
  13. functorial-0.1.1/src/functorial/all.py +54 -0
  14. functorial-0.1.1/src/functorial/alternative.py +36 -0
  15. functorial-0.1.1/src/functorial/applicative.py +168 -0
  16. functorial-0.1.1/src/functorial/backwards.py +18 -0
  17. functorial-0.1.1/src/functorial/bicofunctor.py +48 -0
  18. functorial-0.1.1/src/functorial/bifunctor.py +52 -0
  19. functorial-0.1.1/src/functorial/bitraversable.py +41 -0
  20. functorial-0.1.1/src/functorial/cofunctor.py +70 -0
  21. functorial-0.1.1/src/functorial/const.py +199 -0
  22. functorial-0.1.1/src/functorial/dev/__init__.py +1 -0
  23. functorial-0.1.1/src/functorial/dev/phases.py +93 -0
  24. functorial-0.1.1/src/functorial/dict.py +76 -0
  25. functorial-0.1.1/src/functorial/either.py +165 -0
  26. functorial-0.1.1/src/functorial/examples/__init__.py +1 -0
  27. functorial-0.1.1/src/functorial/examples/divide_conquer.py +71 -0
  28. functorial-0.1.1/src/functorial/examples/huffman.py +208 -0
  29. functorial-0.1.1/src/functorial/foldable.py +227 -0
  30. functorial-0.1.1/src/functorial/functions.py +419 -0
  31. functorial-0.1.1/src/functorial/functor.py +89 -0
  32. functorial-0.1.1/src/functorial/identity.py +55 -0
  33. functorial-0.1.1/src/functorial/io.py +161 -0
  34. functorial-0.1.1/src/functorial/lazy.py +47 -0
  35. functorial-0.1.1/src/functorial/list.py +382 -0
  36. functorial-0.1.1/src/functorial/maybe.py +204 -0
  37. functorial-0.1.1/src/functorial/monad.py +126 -0
  38. functorial-0.1.1/src/functorial/monoids.py +524 -0
  39. functorial-0.1.1/src/functorial/ntuple.py +131 -0
  40. functorial-0.1.1/src/functorial/ops.py +44 -0
  41. functorial-0.1.1/src/functorial/optics/__init__.py +23 -0
  42. functorial-0.1.1/src/functorial/optics/affine_fold.py +106 -0
  43. functorial-0.1.1/src/functorial/optics/affine_traversal.py +122 -0
  44. functorial-0.1.1/src/functorial/optics/all.py +26 -0
  45. functorial-0.1.1/src/functorial/optics/choice.py +93 -0
  46. functorial-0.1.1/src/functorial/optics/cochoice.py +54 -0
  47. functorial-0.1.1/src/functorial/optics/costrong.py +46 -0
  48. functorial-0.1.1/src/functorial/optics/deprecated/cartesian.py +65 -0
  49. functorial-0.1.1/src/functorial/optics/deprecated/forget.py +136 -0
  50. functorial-0.1.1/src/functorial/optics/deprecated/star.py +76 -0
  51. functorial-0.1.1/src/functorial/optics/fold.py +120 -0
  52. functorial-0.1.1/src/functorial/optics/generics.py +96 -0
  53. functorial-0.1.1/src/functorial/optics/getter.py +48 -0
  54. functorial-0.1.1/src/functorial/optics/iso.py +159 -0
  55. functorial-0.1.1/src/functorial/optics/ix_fold.py +133 -0
  56. functorial-0.1.1/src/functorial/optics/ix_lens.py +202 -0
  57. functorial-0.1.1/src/functorial/optics/ix_traversal.py +128 -0
  58. functorial-0.1.1/src/functorial/optics/lens.py +178 -0
  59. functorial-0.1.1/src/functorial/optics/make_lens.py +139 -0
  60. functorial-0.1.1/src/functorial/optics/optic.py +413 -0
  61. functorial-0.1.1/src/functorial/optics/prism.py +113 -0
  62. functorial-0.1.1/src/functorial/optics/profunctors.py +678 -0
  63. functorial-0.1.1/src/functorial/optics/re_.py +129 -0
  64. functorial-0.1.1/src/functorial/optics/review.py +156 -0
  65. functorial-0.1.1/src/functorial/optics/setter.py +64 -0
  66. functorial-0.1.1/src/functorial/optics/strong.py +75 -0
  67. functorial-0.1.1/src/functorial/optics/traversal.py +93 -0
  68. functorial-0.1.1/src/functorial/optics/vl_optics.py +462 -0
  69. functorial-0.1.1/src/functorial/pair.py +101 -0
  70. functorial-0.1.1/src/functorial/profunctor.py +61 -0
  71. functorial-0.1.1/src/functorial/py.typed +0 -0
  72. functorial-0.1.1/src/functorial/reader.py +94 -0
  73. functorial-0.1.1/src/functorial/set.py +45 -0
  74. functorial-0.1.1/src/functorial/singleton.py +214 -0
  75. functorial-0.1.1/src/functorial/state.py +110 -0
  76. functorial-0.1.1/src/functorial/traversable.py +112 -0
  77. functorial-0.1.1/src/functorial/trees.py +820 -0
  78. functorial-0.1.1/src/functorial/unit.py +10 -0
  79. functorial-0.1.1/src/functorial/utils.py +137 -0
  80. functorial-0.1.1/src/functorial/wrappers.py +52 -0
  81. functorial-0.1.1/src/functorial/writer.py +127 -0
  82. functorial-0.1.1/tests/__init__.py +1 -0
  83. functorial-0.1.1/tests/test_examples.py +594 -0
  84. functorial-0.1.1/tests/test_ixoptics.py +389 -0
  85. functorial-0.1.1/tests/test_optics.py +491 -0
  86. functorial-0.1.1/uv.lock +130 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(ls:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ ;;; Directory Local Variables -*- no-byte-compile: t -*-
2
+ ;;; For more information see (info "(emacs) Directory Variables")
3
+
4
+ ((python-mode . ((pyvenv-activate . "/Users/genovese/.local/venvs/default14"))))
@@ -0,0 +1,3 @@
1
+ [flake8]
2
+ max-line-length = 120
3
+ ignore = E201, E202, E241, E266, E272, E302, E305, E731, W503, W504
@@ -0,0 +1,25 @@
1
+ # -*- mode: fundamental; -*-
2
+
3
+ # Python-generated files
4
+ __pycache__/
5
+ *.py[oc]
6
+ build/
7
+ dist/
8
+ wheels/
9
+ *.egg-info
10
+ .hypothesis
11
+
12
+ # Virtual environments
13
+ .venv
14
+
15
+ # Ignore emacs backup files (at least, the ones I generate)
16
+ .*~
17
+ # Ignore scratch file
18
+ .bonz
19
+ .save
20
+ # Ignore mac specific junk
21
+ .DS_Store
22
+
23
+ # Temporary Notes file
24
+ Notes.md
25
+ TODO
@@ -0,0 +1,46 @@
1
+ [DESIGN]
2
+
3
+ # Maximum number of return statements in a function (see R0915)
4
+ max-returns = 10
5
+
6
+ # Maximum number of boolean expressions in an if statement (see R0916).
7
+ max-bool-expr = 8
8
+
9
+ # Minimum number of public methods for a class (see R0903).
10
+ min-public-methods = 0
11
+
12
+ [FORMAT]
13
+
14
+ # Maximum number of characters on a single line.
15
+ max-line-length=120
16
+
17
+ [MESSAGES CONTROL]
18
+
19
+ # Disable the message, report, category or checker with the given id(s). You
20
+ # can either give multiple identifiers separated by comma (,) or put this
21
+ # option multiple times (only on the command line, not in the configuration
22
+ # file where it should appear only once). You can also use "--disable=all" to
23
+ # disable everything first and then re-enable specific checks. For example, if
24
+ # you want to run only the similarities checker, you can use "--disable=all
25
+ # --enable=similarities". If you want to run only the classes checker, but have
26
+ # no Warning level messages displayed, use "--disable=all --enable=classes
27
+ # --disable=W".
28
+ disable=raw-checker-failed,
29
+ bad-inline-option,
30
+ locally-disabled,
31
+ file-ignored,
32
+ suppressed-message,
33
+ useless-suppression,
34
+ deprecated-pragma,
35
+ use-symbolic-message-instead,
36
+ use-implicit-booleaness-not-comparison-to-string,
37
+ use-implicit-booleaness-not-comparison-to-zero,
38
+ too-few-public-methods,
39
+ unnecessary-ellipsis,
40
+ unnecessary-lambda-assignment
41
+
42
+
43
+ [VARIABLES]
44
+
45
+ # List of names allowed to shadow builtins
46
+ allowed-redefined-builtins=map
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Christopher R. Genovese
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.5
2
+ Name: functorial
3
+ Version: 0.1.1
4
+ Summary: Functional Programming concepts playground
5
+ Author-email: "Christopher R. Genovese" <genovese@cmu.edu>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # functorial
19
+
20
+ Functional-programming concepts for Python: typeclasses (Functor, Applicative,
21
+ Monad, Foldable, Traversable, Alternative, ...), standard FP data types
22
+ (`Maybe`, `Either`, `List`, `Dict`, `NTuple`, trees, `IO`, `Reader`, `Writer`,
23
+ `State`), and a profunctor-based optics library (lenses, prisms, traversals,
24
+ folds, isos, and their indexed variants).
25
+
26
+ This is an evolving, educational project — the API is still settling and not
27
+ everything on the roadmap is implemented yet. Feedback and issues are welcome.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install functorial
33
+ # or
34
+ uv add functorial
35
+ ```
36
+
37
+ Requires Python 3.12+.
38
+
39
+ ## Quick taste
40
+
41
+ ```python
42
+ from functorial.maybe import Some, Nothing
43
+ from functorial.optics.lens import lens
44
+ from functorial.optics.getter import view
45
+ from functorial.optics.setter import over
46
+
47
+ # Maybe: a Functor/Applicative/Monad/Traversable
48
+ Some(3).map(lambda x: x + 1) # Some(4)
49
+ Nothing().map(lambda x: x + 1) # Nothing()
50
+
51
+ # Optics: compose lenses with @, act on them with view/over
52
+ fst = lens(lambda s: s[0], lambda s, b: (b, s[1]))
53
+ snd = lens(lambda s: s[1], lambda s, b: (s[0], b))
54
+
55
+ view(fst)((1, 2)) # 1
56
+ over(fst @ snd, str)(((1, 2), 3)) # ((1, '2'), 3)
57
+ ```
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ uv sync
63
+ uv run pytest
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,51 @@
1
+ # functorial
2
+
3
+ Functional-programming concepts for Python: typeclasses (Functor, Applicative,
4
+ Monad, Foldable, Traversable, Alternative, ...), standard FP data types
5
+ (`Maybe`, `Either`, `List`, `Dict`, `NTuple`, trees, `IO`, `Reader`, `Writer`,
6
+ `State`), and a profunctor-based optics library (lenses, prisms, traversals,
7
+ folds, isos, and their indexed variants).
8
+
9
+ This is an evolving, educational project — the API is still settling and not
10
+ everything on the roadmap is implemented yet. Feedback and issues are welcome.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install functorial
16
+ # or
17
+ uv add functorial
18
+ ```
19
+
20
+ Requires Python 3.12+.
21
+
22
+ ## Quick taste
23
+
24
+ ```python
25
+ from functorial.maybe import Some, Nothing
26
+ from functorial.optics.lens import lens
27
+ from functorial.optics.getter import view
28
+ from functorial.optics.setter import over
29
+
30
+ # Maybe: a Functor/Applicative/Monad/Traversable
31
+ Some(3).map(lambda x: x + 1) # Some(4)
32
+ Nothing().map(lambda x: x + 1) # Nothing()
33
+
34
+ # Optics: compose lenses with @, act on them with view/over
35
+ fst = lens(lambda s: s[0], lambda s, b: (b, s[1]))
36
+ snd = lens(lambda s: s[1], lambda s, b: (s[0], b))
37
+
38
+ view(fst)((1, 2)) # 1
39
+ over(fst @ snd, str)(((1, 2), 3)) # ((1, '2'), 3)
40
+ ```
41
+
42
+ ## Development
43
+
44
+ ```bash
45
+ uv sync
46
+ uv run pytest
47
+ ```
48
+
49
+ ## License
50
+
51
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,122 @@
1
+ [project]
2
+ name = "functorial"
3
+ dynamic = ["version"]
4
+ description = "Functional Programming concepts playground"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "Christopher R. Genovese", email = "genovese@cmu.edu" }
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Programming Language :: Python :: Implementation :: CPython",
18
+ "Programming Language :: Python :: Implementation :: PyPy",
19
+ ]
20
+ dependencies = []
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "hypothesis>=6.118.0",
25
+ "pytest>=8.3.3",
26
+ "ruff>=0.7.2",
27
+ ]
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.version]
34
+ path = "src/functorial/__about__.py"
35
+
36
+ [[tool.hatch.envs.all.matrix]]
37
+ python = ["3.12", "3.13", "3.14"]
38
+
39
+ [tool.black]
40
+ target-version = ["py312"]
41
+ line-length = 120
42
+ skip-string-normalization = true
43
+
44
+ [tool.ruff]
45
+ target-version = "py312"
46
+ line-length = 120
47
+
48
+ lint.select = [
49
+ "A",
50
+ "ARG",
51
+ "B",
52
+ "C",
53
+ "DTZ",
54
+ "E",
55
+ "EM",
56
+ "F",
57
+ "FBT",
58
+ "I",
59
+ "ICN",
60
+ "ISC",
61
+ "N",
62
+ "PLC",
63
+ "PLE",
64
+ "PLR",
65
+ "PLW",
66
+ "Q",
67
+ "RUF",
68
+ "S",
69
+ "T",
70
+ "TID",
71
+ "UP",
72
+ "W",
73
+ "YTT",
74
+ ]
75
+
76
+ lint.ignore = [
77
+ # My ignores
78
+ "I001", "EM101", "Q000", "E501", "N999", # Ruff doesn't support "E241", "E272", "E302", "E305", "W504",
79
+ # Allow non-abstract empty methods in abstract base classes
80
+ "B027",
81
+ # Allow boolean positional values in function calls, like `dict.get(... True)`
82
+ "FBT003",
83
+ # Ignore checks for possible passwords
84
+ "S105", "S106", "S107",
85
+ # Ignore complexity
86
+ "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
87
+ ]
88
+
89
+ lint.unfixable = [
90
+ # Don't touch unused imports
91
+ "F401",
92
+ ]
93
+
94
+ [tool.ruff.lint.per-file-ignores]
95
+ # Tests can use magic values, assertions, and relative imports
96
+ "tests/**/*" = ["PLR2004", "S101", "TID252"]
97
+
98
+ [tool.ruff.lint.isort]
99
+ section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"]
100
+ lines-between-types = 1
101
+ lines-after-imports = 1
102
+
103
+ [tool.pylint]
104
+ max-line-length = 120
105
+ max-bool-expr = 8
106
+ min-public-methods = 0
107
+ allowed-redefined-builtins = ["map"]
108
+ disable = [
109
+ "raw-checker-failed",
110
+ "bad-inline-option",
111
+ "locally-disabled",
112
+ "file-ignored",
113
+ "suppressed-message",
114
+ "useless-suppression",
115
+ "deprecated-pragma",
116
+ "use-symbolic-message-instead",
117
+ "use-implicit-booleaness-not-comparison-to-string",
118
+ "use-implicit-booleaness-not-comparison-to-zero",
119
+ "too-few-public-methods",
120
+ "unnecessary-ellipsis",
121
+ "unnecessary-lambda-assignment"
122
+ ]
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2024-present Christopher R. Genovese <genovese@cmu.edu>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.1.1"
@@ -0,0 +1,29 @@
1
+ __all__ = [
2
+ 'applicative',
3
+ 'bicofunctor',
4
+ 'bifunctor',
5
+ 'bitraversable',
6
+ 'cofunctor',
7
+ 'const',
8
+ 'dict',
9
+ 'either',
10
+ 'foldable',
11
+ 'functor',
12
+ 'identity',
13
+ 'list',
14
+ 'maybe',
15
+ 'monad',
16
+ 'monoids',
17
+ 'ntuple',
18
+ 'pair',
19
+ 'profunctor',
20
+ 'reader',
21
+ 'set',
22
+ 'state',
23
+ 'traversable',
24
+ 'trees',
25
+ 'functions',
26
+ 'ops',
27
+ 'optics',
28
+ 'utils',
29
+ ]
@@ -0,0 +1,54 @@
1
+ #
2
+ # Helper module to load all the names into the namespace
3
+ #
4
+ # Do 'from FP.all import *' to load all the needed objects.
5
+ #
6
+ # ruff: noqa: F401, F403, F405
7
+
8
+ from __future__ import annotations
9
+
10
+ from . import monoids
11
+ from . import optics
12
+
13
+ from .alternative import *
14
+ from .applicative import *
15
+ from .bicofunctor import *
16
+ from .bifunctor import *
17
+ from .cofunctor import *
18
+ from .foldable import *
19
+ from .functor import *
20
+ from .monad import *
21
+ from .profunctor import *
22
+ from .traversable import *
23
+
24
+ from .const import *
25
+ from .either import *
26
+ from .identity import *
27
+ from .list import *
28
+ from .maybe import *
29
+ from .ntuple import *
30
+ from .pair import *
31
+
32
+ from .dict import *
33
+ from .monoids import Monoid, munit, mcombine
34
+ from .reader import *
35
+ from .set import *
36
+ from .state import *
37
+ from .trees import *
38
+ from .writer import *
39
+
40
+ from .functions import *
41
+ from .io import *
42
+ from .ops import *
43
+ from .utils import *
44
+ from .wrappers import *
45
+
46
+ from .pair import pair # More powerful version over .functions.pair
47
+
48
+ from .optics.all import *
49
+
50
+ #
51
+ # Conveniences
52
+ #
53
+
54
+ c = compose
@@ -0,0 +1,36 @@
1
+ # trait Applicative f => Alternative (f : Type -> Type) where
2
+ # empty : f a
3
+ # alt : f a -> f a -> f a
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import abstractmethod
8
+ from typing import Protocol
9
+
10
+ from .applicative import Applicative
11
+
12
+ __all__ = ['Alternative', 'alt', 'guard']
13
+
14
+
15
+ #
16
+ # Alternative as a mixin
17
+ #
18
+
19
+ # ATTN: Should this have a type parameter or be handled like Applicative?
20
+
21
+ class Alternative[A](Applicative, Protocol):
22
+ @classmethod
23
+ def empty(cls) -> Alternative[A]:
24
+ raise NotImplementedError
25
+
26
+ @abstractmethod
27
+ def alt(self, fb: Alternative[A]) -> Alternative[A]:
28
+ ...
29
+
30
+ def alt[A](fa: Alternative[A], fb: Alternative[A]) -> Alternative[A]:
31
+ return fa.alt(fb)
32
+
33
+ def guard(f: type[Alternative], condition: bool) -> Alternative[tuple[()]]: # ATTN: type Unit = tuple[()]
34
+ return f.unit() if condition else f.empty()
35
+
36
+ # ATTN: Include some and many? Can we implement them?
@@ -0,0 +1,168 @@
1
+ # trait Functor f => Applicative (f : Type -> Type) where
2
+ # pure : a -> f a
3
+ # map2 : (a -> b -> c) -> f a -> f b -> f c -- lift2 := map2 h
4
+ # ap : f (a -> b) -> f a -> f b
5
+ #
6
+ # unit : f Unit -- Unit equiv ()
7
+ # combine : f a -> f b -> f (a, b)
8
+
9
+ from __future__ import annotations
10
+
11
+ from abc import abstractmethod
12
+ from collections.abc import Callable
13
+ from typing import Protocol, runtime_checkable
14
+
15
+ from .functor import Functor, map # pylint: disable=redefined-builtin
16
+ from .functions import compose, const, curry, identity, pair, fn_eval, eval_with
17
+
18
+ __all__ = [
19
+ 'Applicative', 'map2', 'combine', 'pure',
20
+ 'ap', 'lift2', 'ap_first', 'ap_second', 'rev_ap',
21
+ 'when', 'unless', # ATTN: needed?
22
+ 'IdentityA',
23
+ ]
24
+
25
+
26
+ #
27
+ # Applicative as a mixin
28
+ #
29
+
30
+ @runtime_checkable
31
+ class Applicative(Functor, Protocol):
32
+ @classmethod
33
+ def pure(cls, a):
34
+ raise NotImplementedError
35
+
36
+ @abstractmethod
37
+ def map2(self, g, fb):
38
+ ...
39
+
40
+ @classmethod
41
+ def unit(cls):
42
+ return cls.pure( () )
43
+
44
+ def combine(self, fb):
45
+ return self.map2(pair, fb)
46
+
47
+ def ap(self, fb):
48
+ return self.map2(fn_eval, fb)
49
+
50
+
51
+ def map2(g, fa, fb):
52
+ return fa.map2(g, fb)
53
+
54
+ def combine(fa, fb):
55
+ return fa.combine(fb)
56
+
57
+ def pure(fa, a):
58
+ return fa.pure(a)
59
+
60
+ def ap(fa_to_b: Applicative | Callable, fa: Applicative, *fs: Applicative, auto_curry=True) -> Applicative:
61
+ if not isinstance(fa_to_b, Applicative):
62
+ if auto_curry:
63
+ fa_to_b = fa.pure(curry(fa_to_b))
64
+ else:
65
+ fa_to_b = fa.pure(fa_to_b)
66
+ # elif auto_curry:
67
+ # fa_to_b = fa_to_b.map(curry) # ATTN: PROVISIONAL
68
+
69
+ fb = fa_to_b.ap(fa) # type: ignore
70
+ for fx in fs:
71
+ fb = fb.ap(fx)
72
+ return fb
73
+
74
+ # ATTN: by our emerging convention, this should be called map2_, though this name is good too
75
+ def lift2[A, B, C](f: Callable[[A, B], C]):
76
+ """Lifts a two-argument function to a mapping of Applicatives.
77
+
78
+ This is just the partial application map2(f, _, __).
79
+ The applicatives should be the same type (technically
80
+ one should be a subclass of the other).
81
+
82
+ """
83
+ def liftA2(fa: Applicative, fb: Applicative) -> Applicative:
84
+ if not issubclass(fa.__class__, fb.__class__) and not issubclass(fb.__class__, fa.__class__):
85
+ raise TypeError('lift2(f) should be applied to compatible applicatives.')
86
+ return fa.map2(f, fb)
87
+
88
+ return liftA2
89
+
90
+ map2_ = lift2 # Alias for lift2 that matches our naming convention
91
+
92
+ def rev_ap(fa: Applicative, fa_to_b: Applicative | Callable, auto_curry=True) -> Applicative:
93
+ """A variant of ap with the arguments reversed and effects resolved in the order given.
94
+
95
+ Note that rev_ap differs from flip(ap) in the order in which effects
96
+ are resolved. The latter would just remap the argument order into ap,
97
+ but this resolves fa then fa_to_b.
98
+
99
+ Unlike ap, this only takes two arguments, but it does do automatic currying
100
+ if auto_curry is True, which is the default.
101
+
102
+ Returns the resulting applicative.
103
+
104
+ """
105
+ if not isinstance(fa_to_b, Applicative):
106
+ if auto_curry:
107
+ fa_to_b = fa.pure(curry(fa_to_b))
108
+ else:
109
+ fa_to_b = fa.pure(fa_to_b)
110
+
111
+ return fa.map2(eval_with, fa_to_b)
112
+
113
+ # (<*) : f a -> f b -> f a
114
+ def ap_first(fa: Applicative, fb: Applicative) -> Applicative:
115
+ "Sequence actions, disgarding the value of the second argument."
116
+ return fa.map2(lambda a, b: a, fb)
117
+
118
+ # (*>) : f a -> f b -> f b
119
+ def ap_second(fa: Applicative, fb: Applicative) -> Applicative:
120
+ "Sequence actions, disgarding the value of the first argument."
121
+ return ap(map(compose(identity, const), fa), fb)
122
+
123
+ # ATTN: implement when and unless here? Are they useful at all for us, as we don't need it for??
124
+ # when : Applicative f => Bool -> f () -> f ()
125
+ def when(f: type[Applicative], condition: bool, true_case: Applicative) -> Applicative:
126
+ return map(const(()), true_case) if condition else f.pure(())
127
+
128
+ # unless : Applicative f => Bool -> f () -> f ()
129
+ def unless(f: type[Applicative], condition: bool, false_case: Applicative) -> Applicative:
130
+ return f.pure(()) if condition else map(const(()), false_case)
131
+
132
+ # A copy of the Identity Functor that is only an Applicative
133
+ # This is useful as a default applicative in infrastructure
134
+ # modules that would lead to circularity if loading Identity
135
+ # module. See also IdentityM in case a default Monad is needed.
136
+
137
+ class IdentityA[A](Applicative):
138
+ """A default Applicative that mimics Identity without Monad or Traversable.
139
+
140
+ This is useful in defaults only infrastructure modules in this package,
141
+ like Monad and Traversable, that Identity actually loads. Users should
142
+ not use this explicitly.
143
+
144
+ """
145
+ __match_args__ = ('_value',)
146
+
147
+ def __init__(self, x: A):
148
+ self._value = x
149
+
150
+ def __str__(self):
151
+ return f'IdentityA {self._value}'
152
+
153
+ def __repr__(self):
154
+ return f'IdentityA({self._value})'
155
+
156
+ @classmethod
157
+ def run(cls, fa: IdentityA[A]) -> A:
158
+ return fa._value
159
+
160
+ def map[B](self, g: Callable[[A], B]) -> IdentityA[B]:
161
+ return IdentityA(g(self._value))
162
+
163
+ @classmethod
164
+ def pure(cls, x: A) -> IdentityA[A]:
165
+ return IdentityA(x)
166
+
167
+ def map2[B, C](self, g: Callable[[A, B], C], fb: IdentityA[B]) -> IdentityA[C]:
168
+ return IdentityA(g(self._value, fb._value))
@@ -0,0 +1,18 @@
1
+
2
+ # Incomplete but working
3
+
4
+ from .applicative import Applicative
5
+ from .functions import flip
6
+
7
+ def Backwards(f: type[Applicative]):
8
+ class Backward_f(f):
9
+ "A Backward version of the Applicative f."
10
+
11
+ @classmethod
12
+ def pure(cls, a):
13
+ return cls(f.pure(a))
14
+
15
+ def map2(self, g, fb):
16
+ return Backward_f(f(fb).map2(flip(g), f(self)))
17
+
18
+ return Backward_f