Spaces:
Running
Running
Merge pull request #89 from metaboulie/fp/applicatives
Browse files
functional_programming/06_applicatives.py
ADDED
@@ -0,0 +1,1161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# /// script
|
2 |
+
# requires-python = ">=3.12"
|
3 |
+
# dependencies = [
|
4 |
+
# "marimo",
|
5 |
+
# ]
|
6 |
+
# ///
|
7 |
+
|
8 |
+
import marimo
|
9 |
+
|
10 |
+
__generated_with = "0.12.4"
|
11 |
+
app = marimo.App(app_title="Applicative programming with effects")
|
12 |
+
|
13 |
+
|
14 |
+
@app.cell(hide_code=True)
|
15 |
+
def _(mo):
|
16 |
+
mo.md(
|
17 |
+
r"""
|
18 |
+
# Applicative programming with effects
|
19 |
+
|
20 |
+
`Applicative Functor` encapsulates certain sorts of *effectful* computations in a functionally pure way, and encourages an *applicative* programming style.
|
21 |
+
|
22 |
+
Applicative is a functor with application, providing operations to
|
23 |
+
|
24 |
+
+ embed pure expressions (`pure`), and
|
25 |
+
+ sequence computations and combine their results (`apply`).
|
26 |
+
|
27 |
+
In this notebook, you will learn:
|
28 |
+
|
29 |
+
1. How to view `applicative` as multi-functor.
|
30 |
+
2. How to use `lift` to simplify chaining application.
|
31 |
+
3. How to bring *effects* to the functional pure world.
|
32 |
+
4. How to view `applicative` as lax monoidal functor.
|
33 |
+
|
34 |
+
/// details | Notebook metadata
|
35 |
+
type: info
|
36 |
+
|
37 |
+
version: 0.1.2 | last modified: 2025-04-07 | author: [métaboulie](https://github.com/metaboulie)<br/>
|
38 |
+
|
39 |
+
///
|
40 |
+
"""
|
41 |
+
)
|
42 |
+
return
|
43 |
+
|
44 |
+
|
45 |
+
@app.cell(hide_code=True)
|
46 |
+
def _(mo):
|
47 |
+
mo.md(
|
48 |
+
r"""
|
49 |
+
# The intuition: [Multifunctor](https://arxiv.org/pdf/2401.14286)
|
50 |
+
|
51 |
+
## Limitations of functor
|
52 |
+
|
53 |
+
Recall that functors abstract the idea of mapping a function over each element of a structure.
|
54 |
+
|
55 |
+
Suppose now that we wish to generalise this idea to allow functions with any number of arguments to be mapped, rather than being restricted to functions with a single argument. More precisely, suppose that we wish to define a hierarchy of `fmap` functions with the following types:
|
56 |
+
|
57 |
+
```haskell
|
58 |
+
fmap0 :: a -> f a
|
59 |
+
|
60 |
+
fmap1 :: (a -> b) -> f a -> f b
|
61 |
+
|
62 |
+
fmap2 :: (a -> b -> c) -> f a -> f b -> f c
|
63 |
+
|
64 |
+
fmap3 :: (a -> b -> c -> d) -> f a -> f b -> f c -> f d
|
65 |
+
```
|
66 |
+
|
67 |
+
And we have to declare a special version of the functor class for each case.
|
68 |
+
"""
|
69 |
+
)
|
70 |
+
return
|
71 |
+
|
72 |
+
|
73 |
+
@app.cell(hide_code=True)
|
74 |
+
def _(mo):
|
75 |
+
mo.md(
|
76 |
+
r"""
|
77 |
+
## Defining Multifunctor
|
78 |
+
|
79 |
+
/// admonition
|
80 |
+
we use prefix `f` rather than `ap` to indicate *Applicative Functor*
|
81 |
+
///
|
82 |
+
|
83 |
+
As a result, we may want to define a single `Multifunctor` such that:
|
84 |
+
|
85 |
+
1. Lift a regular n-argument function into the context of functors
|
86 |
+
|
87 |
+
```python
|
88 |
+
# lift a regular 3-argument function `g`
|
89 |
+
g: Callable[[A, B, C], D]
|
90 |
+
# into the context of functors
|
91 |
+
fg: Callable[[Functor[A], Functor[B], Functor[C]], Functor[D]]
|
92 |
+
```
|
93 |
+
|
94 |
+
3. Apply it to n functor-wrapped values
|
95 |
+
|
96 |
+
```python
|
97 |
+
# fa: Functor[A], fb: Functor[B], fc: Functor[C]
|
98 |
+
fg(fa, fb, fc)
|
99 |
+
```
|
100 |
+
|
101 |
+
5. Get a single functor-wrapped result
|
102 |
+
|
103 |
+
```python
|
104 |
+
fd: Functor[D]
|
105 |
+
```
|
106 |
+
|
107 |
+
We will define a function `lift` such that
|
108 |
+
|
109 |
+
```python
|
110 |
+
fd = lift(g, fa, fb, fc)
|
111 |
+
```
|
112 |
+
"""
|
113 |
+
)
|
114 |
+
return
|
115 |
+
|
116 |
+
|
117 |
+
@app.cell(hide_code=True)
|
118 |
+
def _(mo):
|
119 |
+
mo.md(
|
120 |
+
r"""
|
121 |
+
## Pure, apply and lift
|
122 |
+
|
123 |
+
Traditionally, applicative functors are presented through two core operations:
|
124 |
+
|
125 |
+
1. `pure`: embeds an object (value or function) into the applicative functor
|
126 |
+
|
127 |
+
```python
|
128 |
+
# a -> F a
|
129 |
+
pure: Callable[[A], Applicative[A]]
|
130 |
+
# for example, if `a` is
|
131 |
+
a: A
|
132 |
+
# then we can have `fa` as
|
133 |
+
fa: Applicative[A] = pure(a)
|
134 |
+
# or if we have a regular function `g`
|
135 |
+
g: Callable[[A], B]
|
136 |
+
# then we can have `fg` as
|
137 |
+
fg: Applicative[Callable[[A], B]] = pure(g)
|
138 |
+
```
|
139 |
+
|
140 |
+
2. `apply`: applies a function inside an applicative functor to a value inside an applicative functor
|
141 |
+
|
142 |
+
```python
|
143 |
+
# F (a -> b) -> F a -> F b
|
144 |
+
apply: Callable[[Applicative[Callable[[A], B]], Applicative[A]], Applicative[B]]
|
145 |
+
# and we can have
|
146 |
+
fd = apply(apply(apply(fg, fa), fb), fc)
|
147 |
+
```
|
148 |
+
|
149 |
+
|
150 |
+
As a result,
|
151 |
+
|
152 |
+
```python
|
153 |
+
lift(g, fa, fb, fc) = apply(apply(apply(pure(g), fa), fb), fc)
|
154 |
+
```
|
155 |
+
"""
|
156 |
+
)
|
157 |
+
return
|
158 |
+
|
159 |
+
|
160 |
+
@app.cell(hide_code=True)
|
161 |
+
def _(mo):
|
162 |
+
mo.md(
|
163 |
+
r"""
|
164 |
+
/// admonition | How to use *Applicative* in the manner of *Multifunctor*
|
165 |
+
|
166 |
+
1. Define `pure` and `apply` for an `Applicative` subclass
|
167 |
+
|
168 |
+
- We can define them much easier compared with `lift`.
|
169 |
+
|
170 |
+
2. Use the `lift` method
|
171 |
+
|
172 |
+
- We can use it much more convenient compared with the combination of `pure` and `apply`.
|
173 |
+
|
174 |
+
|
175 |
+
///
|
176 |
+
|
177 |
+
/// attention | You can suppress the chaining application of `apply` and `pure` as:
|
178 |
+
|
179 |
+
```python
|
180 |
+
apply(pure(g), fa) -> lift(g, fa)
|
181 |
+
apply(apply(pure(g), fa), fb) -> lift(g, fa, fb)
|
182 |
+
apply(apply(apply(pure(g), fa), fb), fc) -> lift(g, fa, fb, fc)
|
183 |
+
```
|
184 |
+
|
185 |
+
///
|
186 |
+
"""
|
187 |
+
)
|
188 |
+
return
|
189 |
+
|
190 |
+
|
191 |
+
@app.cell(hide_code=True)
|
192 |
+
def _(mo):
|
193 |
+
mo.md(
|
194 |
+
r"""
|
195 |
+
## Abstracting applicatives
|
196 |
+
|
197 |
+
We can now provide an initial abstraction definition of applicatives:
|
198 |
+
|
199 |
+
```python
|
200 |
+
@dataclass
|
201 |
+
class Applicative[A](Functor, ABC):
|
202 |
+
@classmethod
|
203 |
+
@abstractmethod
|
204 |
+
def pure(cls, a: A) -> "Applicative[A]":
|
205 |
+
return NotImplementedError
|
206 |
+
|
207 |
+
@classmethod
|
208 |
+
@abstractmethod
|
209 |
+
def apply(
|
210 |
+
cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
|
211 |
+
) -> "Applicative[B]":
|
212 |
+
return NotImplementedError
|
213 |
+
|
214 |
+
@classmethod
|
215 |
+
def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
|
216 |
+
curr = cls.pure(f)
|
217 |
+
if not args:
|
218 |
+
return curr
|
219 |
+
for arg in args:
|
220 |
+
curr = cls.apply(curr, arg)
|
221 |
+
return curr
|
222 |
+
```
|
223 |
+
|
224 |
+
/// attention | minimal implementation requirement
|
225 |
+
|
226 |
+
- `pure`
|
227 |
+
- `apply`
|
228 |
+
///
|
229 |
+
"""
|
230 |
+
)
|
231 |
+
return
|
232 |
+
|
233 |
+
|
234 |
+
@app.cell(hide_code=True)
|
235 |
+
def _(mo):
|
236 |
+
mo.md(r"""# Instances, laws and utility functions""")
|
237 |
+
return
|
238 |
+
|
239 |
+
|
240 |
+
@app.cell(hide_code=True)
|
241 |
+
def _(mo):
|
242 |
+
mo.md(
|
243 |
+
r"""
|
244 |
+
## Applicative instances
|
245 |
+
|
246 |
+
When we are actually implementing an *Applicative* instance, we can keep in mind that `pure` and `apply` fundamentally:
|
247 |
+
|
248 |
+
- embed an object (value or function) to the computational context
|
249 |
+
- apply a function inside the computation context to a value inside the computational context
|
250 |
+
"""
|
251 |
+
)
|
252 |
+
return
|
253 |
+
|
254 |
+
|
255 |
+
@app.cell(hide_code=True)
|
256 |
+
def _(mo):
|
257 |
+
mo.md(
|
258 |
+
r"""
|
259 |
+
### Wrapper
|
260 |
+
|
261 |
+
- `pure` should simply *wrap* an object, in the sense that:
|
262 |
+
|
263 |
+
```haskell
|
264 |
+
Wrapper.pure(1) => Wrapper(value=1)
|
265 |
+
```
|
266 |
+
|
267 |
+
- `apply` should apply a *wrapped* function to a *wrapped* value
|
268 |
+
|
269 |
+
The implementation is:
|
270 |
+
"""
|
271 |
+
)
|
272 |
+
return
|
273 |
+
|
274 |
+
|
275 |
+
@app.cell
|
276 |
+
def _(Applicative, dataclass):
|
277 |
+
@dataclass
|
278 |
+
class Wrapper[A](Applicative):
|
279 |
+
value: A
|
280 |
+
|
281 |
+
@classmethod
|
282 |
+
def pure(cls, a: A) -> "Wrapper[A]":
|
283 |
+
return cls(a)
|
284 |
+
|
285 |
+
@classmethod
|
286 |
+
def apply(
|
287 |
+
cls, fg: "Wrapper[Callable[[A], B]]", fa: "Wrapper[A]"
|
288 |
+
) -> "Wrapper[B]":
|
289 |
+
return cls(fg.value(fa.value))
|
290 |
+
return (Wrapper,)
|
291 |
+
|
292 |
+
|
293 |
+
@app.cell(hide_code=True)
|
294 |
+
def _(mo):
|
295 |
+
mo.md(r"""> try with Wrapper below""")
|
296 |
+
return
|
297 |
+
|
298 |
+
|
299 |
+
@app.cell
|
300 |
+
def _(Wrapper):
|
301 |
+
Wrapper.lift(
|
302 |
+
lambda a: lambda b: lambda c: a + b * c,
|
303 |
+
Wrapper(1),
|
304 |
+
Wrapper(2),
|
305 |
+
Wrapper(3),
|
306 |
+
)
|
307 |
+
return
|
308 |
+
|
309 |
+
|
310 |
+
@app.cell(hide_code=True)
|
311 |
+
def _(mo):
|
312 |
+
mo.md(
|
313 |
+
r"""
|
314 |
+
### List
|
315 |
+
|
316 |
+
- `pure` should wrap the object in a list, in the sense that:
|
317 |
+
|
318 |
+
```haskell
|
319 |
+
List.pure(1) => List(value=[1])
|
320 |
+
```
|
321 |
+
|
322 |
+
- `apply` should apply a list of functions to a list of values
|
323 |
+
- you can think of this as cartesian product, concatenating the result of applying every function to every value
|
324 |
+
|
325 |
+
The implementation is:
|
326 |
+
"""
|
327 |
+
)
|
328 |
+
return
|
329 |
+
|
330 |
+
|
331 |
+
@app.cell
|
332 |
+
def _(Applicative, dataclass, product):
|
333 |
+
@dataclass
|
334 |
+
class List[A](Applicative):
|
335 |
+
value: list[A]
|
336 |
+
|
337 |
+
@classmethod
|
338 |
+
def pure(cls, a: A) -> "List[A]":
|
339 |
+
return cls([a])
|
340 |
+
|
341 |
+
@classmethod
|
342 |
+
def apply(cls, fg: "List[Callable[[A], B]]", fa: "List[A]") -> "List[B]":
|
343 |
+
return cls([g(a) for g, a in product(fg.value, fa.value)])
|
344 |
+
return (List,)
|
345 |
+
|
346 |
+
|
347 |
+
@app.cell(hide_code=True)
|
348 |
+
def _(mo):
|
349 |
+
mo.md(r"""> try with List below""")
|
350 |
+
return
|
351 |
+
|
352 |
+
|
353 |
+
@app.cell
|
354 |
+
def _(List):
|
355 |
+
List.apply(
|
356 |
+
List([lambda a: a + 1, lambda a: a * 2]),
|
357 |
+
List([1, 2]),
|
358 |
+
)
|
359 |
+
return
|
360 |
+
|
361 |
+
|
362 |
+
@app.cell
|
363 |
+
def _(List):
|
364 |
+
List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
|
365 |
+
return
|
366 |
+
|
367 |
+
|
368 |
+
@app.cell(hide_code=True)
|
369 |
+
def _(mo):
|
370 |
+
mo.md(
|
371 |
+
r"""
|
372 |
+
### Maybe
|
373 |
+
|
374 |
+
- `pure` should wrap the object in a Maybe, in the sense that:
|
375 |
+
|
376 |
+
```haskell
|
377 |
+
Maybe.pure(1) => "Just 1"
|
378 |
+
Maybe.pure(None) => "Nothing"
|
379 |
+
```
|
380 |
+
|
381 |
+
- `apply` should apply a function maybe exist to a value maybe exist
|
382 |
+
- if the function is `None` or the value is `None`, simply returns `None`
|
383 |
+
- else apply the function to the value and wrap the result in `Just`
|
384 |
+
|
385 |
+
The implementation is:
|
386 |
+
"""
|
387 |
+
)
|
388 |
+
return
|
389 |
+
|
390 |
+
|
391 |
+
@app.cell
|
392 |
+
def _(Applicative, dataclass):
|
393 |
+
@dataclass
|
394 |
+
class Maybe[A](Applicative):
|
395 |
+
value: None | A
|
396 |
+
|
397 |
+
@classmethod
|
398 |
+
def pure(cls, a: A) -> "Maybe[A]":
|
399 |
+
return cls(a)
|
400 |
+
|
401 |
+
@classmethod
|
402 |
+
def apply(
|
403 |
+
cls, fg: "Maybe[Callable[[A], B]]", fa: "Maybe[A]"
|
404 |
+
) -> "Maybe[B]":
|
405 |
+
if fg.value is None or fa.value is None:
|
406 |
+
return cls(None)
|
407 |
+
|
408 |
+
return cls(fg.value(fa.value))
|
409 |
+
|
410 |
+
def __repr__(self):
|
411 |
+
return "Nothing" if self.value is None else f"Just({self.value!r})"
|
412 |
+
return (Maybe,)
|
413 |
+
|
414 |
+
|
415 |
+
@app.cell(hide_code=True)
|
416 |
+
def _(mo):
|
417 |
+
mo.md(r"""> try with Maybe below""")
|
418 |
+
return
|
419 |
+
|
420 |
+
|
421 |
+
@app.cell
|
422 |
+
def _(Maybe):
|
423 |
+
Maybe.lift(
|
424 |
+
lambda a: lambda b: a + b,
|
425 |
+
Maybe(1),
|
426 |
+
Maybe(2),
|
427 |
+
)
|
428 |
+
return
|
429 |
+
|
430 |
+
|
431 |
+
@app.cell
|
432 |
+
def _(Maybe):
|
433 |
+
Maybe.lift(
|
434 |
+
lambda a: lambda b: None,
|
435 |
+
Maybe(1),
|
436 |
+
Maybe(2),
|
437 |
+
)
|
438 |
+
return
|
439 |
+
|
440 |
+
|
441 |
+
@app.cell(hide_code=True)
|
442 |
+
def _(mo):
|
443 |
+
mo.md(
|
444 |
+
r"""
|
445 |
+
## Collect the list of response with sequenceL
|
446 |
+
|
447 |
+
One often wants to execute a list of commands and collect the list of their response, and we can define a function `sequenceL` for this
|
448 |
+
|
449 |
+
/// admonition
|
450 |
+
In a further notebook about `Traversable`, we will have a more generic `sequence` that execute a **sequence** of commands and collect the **sequence** of their response, which is not limited to `list`.
|
451 |
+
///
|
452 |
+
|
453 |
+
```python
|
454 |
+
@classmethod
|
455 |
+
def sequenceL(cls, fas: list["Applicative[A]"]) -> "Applicative[list[A]]":
|
456 |
+
if not fas:
|
457 |
+
return cls.pure([])
|
458 |
+
|
459 |
+
return cls.apply(
|
460 |
+
cls.fmap(lambda v: lambda vs: [v] + vs, fas[0]),
|
461 |
+
cls.sequenceL(fas[1:]),
|
462 |
+
)
|
463 |
+
```
|
464 |
+
|
465 |
+
Let's try `sequenceL` with the instances.
|
466 |
+
"""
|
467 |
+
)
|
468 |
+
return
|
469 |
+
|
470 |
+
|
471 |
+
@app.cell
|
472 |
+
def _(Wrapper):
|
473 |
+
Wrapper.sequenceL([Wrapper(1), Wrapper(2), Wrapper(3)])
|
474 |
+
return
|
475 |
+
|
476 |
+
|
477 |
+
@app.cell(hide_code=True)
|
478 |
+
def _(mo):
|
479 |
+
mo.md(
|
480 |
+
r"""
|
481 |
+
/// attention
|
482 |
+
For the `Maybe` Applicative, the presence of any `Nothing` causes the entire computation to return Nothing.
|
483 |
+
///
|
484 |
+
"""
|
485 |
+
)
|
486 |
+
return
|
487 |
+
|
488 |
+
|
489 |
+
@app.cell
|
490 |
+
def _(Maybe):
|
491 |
+
Maybe.sequenceL([Maybe(1), Maybe(2), Maybe(None), Maybe(3)])
|
492 |
+
return
|
493 |
+
|
494 |
+
|
495 |
+
@app.cell(hide_code=True)
|
496 |
+
def _(mo):
|
497 |
+
mo.md(r"""The result of `sequenceL` for `List Applicative` is the Cartesian product of the input lists, yielding all possible ordered combinations of elements from each list.""")
|
498 |
+
return
|
499 |
+
|
500 |
+
|
501 |
+
@app.cell
|
502 |
+
def _(List):
|
503 |
+
List.sequenceL([List([1, 2]), List([3]), List([5, 6, 7])])
|
504 |
+
return
|
505 |
+
|
506 |
+
|
507 |
+
@app.cell(hide_code=True)
|
508 |
+
def _(mo):
|
509 |
+
mo.md(
|
510 |
+
r"""
|
511 |
+
## Applicative laws
|
512 |
+
|
513 |
+
/// admonition | id and compose
|
514 |
+
|
515 |
+
Remember that
|
516 |
+
|
517 |
+
- `id = lambda x: x`
|
518 |
+
- `compose = lambda f: lambda g: lambda x: f(g(x))`
|
519 |
+
|
520 |
+
///
|
521 |
+
|
522 |
+
Traditionally, there are four laws that `Applicative` instances should satisfy. In some sense, they are all concerned with making sure that `pure` deserves its name:
|
523 |
+
|
524 |
+
- The identity law:
|
525 |
+
```python
|
526 |
+
# fa: Applicative[A]
|
527 |
+
apply(pure(id), fa) = fa
|
528 |
+
```
|
529 |
+
- Homomorphism:
|
530 |
+
```python
|
531 |
+
# a: A
|
532 |
+
# g: Callable[[A], B]
|
533 |
+
apply(pure(g), pure(a)) = pure(g(a))
|
534 |
+
```
|
535 |
+
Intuitively, applying a non-effectful function to a non-effectful argument in an effectful context is the same as just applying the function to the argument and then injecting the result into the context with pure.
|
536 |
+
- Interchange:
|
537 |
+
```python
|
538 |
+
# a: A
|
539 |
+
# fg: Applicative[Callable[[A], B]]
|
540 |
+
apply(fg, pure(a)) = apply(pure(lambda g: g(a)), fg)
|
541 |
+
```
|
542 |
+
Intuitively, this says that when evaluating the application of an effectful function to a pure argument, the order in which we evaluate the function and its argument doesn't matter.
|
543 |
+
- Composition:
|
544 |
+
```python
|
545 |
+
# fg: Applicative[Callable[[B], C]]
|
546 |
+
# fh: Applicative[Callable[[A], B]]
|
547 |
+
# fa: Applicative[A]
|
548 |
+
apply(fg, apply(fh, fa)) = lift(compose, fg, fh, fa)
|
549 |
+
```
|
550 |
+
This one is the trickiest law to gain intuition for. In some sense it is expressing a sort of associativity property of `apply`.
|
551 |
+
|
552 |
+
We can add 4 helper functions to `Applicative` to check whether an instance respects the laws or not:
|
553 |
+
|
554 |
+
```python
|
555 |
+
@dataclass
|
556 |
+
class Applicative[A](Functor, ABC):
|
557 |
+
|
558 |
+
@classmethod
|
559 |
+
def check_identity(cls, fa: "Applicative[A]"):
|
560 |
+
if cls.lift(id, fa) != fa:
|
561 |
+
raise ValueError("Instance violates identity law")
|
562 |
+
return True
|
563 |
+
|
564 |
+
@classmethod
|
565 |
+
def check_homomorphism(cls, a: A, f: Callable[[A], B]):
|
566 |
+
if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
|
567 |
+
raise ValueError("Instance violates homomorphism law")
|
568 |
+
return True
|
569 |
+
|
570 |
+
@classmethod
|
571 |
+
def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
|
572 |
+
if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
|
573 |
+
raise ValueError("Instance violates interchange law")
|
574 |
+
return True
|
575 |
+
|
576 |
+
@classmethod
|
577 |
+
def check_composition(
|
578 |
+
cls,
|
579 |
+
fg: "Applicative[Callable[[B], C]]",
|
580 |
+
fh: "Applicative[Callable[[A], B]]",
|
581 |
+
fa: "Applicative[A]",
|
582 |
+
):
|
583 |
+
if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
|
584 |
+
raise ValueError("Instance violates composition law")
|
585 |
+
return True
|
586 |
+
```
|
587 |
+
|
588 |
+
> Try to validate applicative laws below
|
589 |
+
"""
|
590 |
+
)
|
591 |
+
return
|
592 |
+
|
593 |
+
|
594 |
+
@app.cell
|
595 |
+
def _():
|
596 |
+
id = lambda x: x
|
597 |
+
compose = lambda f: lambda g: lambda x: f(g(x))
|
598 |
+
const = lambda a: lambda _: a
|
599 |
+
return compose, const, id
|
600 |
+
|
601 |
+
|
602 |
+
@app.cell
|
603 |
+
def _(List, Wrapper):
|
604 |
+
print("Checking Wrapper")
|
605 |
+
print(Wrapper.check_identity(Wrapper.pure(1)))
|
606 |
+
print(Wrapper.check_homomorphism(1, lambda x: x + 1))
|
607 |
+
print(Wrapper.check_interchange(1, Wrapper.pure(lambda x: x + 1)))
|
608 |
+
print(
|
609 |
+
Wrapper.check_composition(
|
610 |
+
Wrapper.pure(lambda x: x * 2),
|
611 |
+
Wrapper.pure(lambda x: x + 0.1),
|
612 |
+
Wrapper.pure(1),
|
613 |
+
)
|
614 |
+
)
|
615 |
+
|
616 |
+
print("\nChecking List")
|
617 |
+
print(List.check_identity(List.pure(1)))
|
618 |
+
print(List.check_homomorphism(1, lambda x: x + 1))
|
619 |
+
print(List.check_interchange(1, List.pure(lambda x: x + 1)))
|
620 |
+
print(
|
621 |
+
List.check_composition(
|
622 |
+
List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
|
623 |
+
)
|
624 |
+
)
|
625 |
+
return
|
626 |
+
|
627 |
+
|
628 |
+
@app.cell(hide_code=True)
|
629 |
+
def _(mo):
|
630 |
+
mo.md(
|
631 |
+
r"""
|
632 |
+
## Utility functions
|
633 |
+
|
634 |
+
/// attention | using `fmap`
|
635 |
+
`fmap` is defined automatically using `pure` and `apply`, so you can use `fmap` with any `Applicative`
|
636 |
+
///
|
637 |
+
|
638 |
+
```python
|
639 |
+
@dataclass
|
640 |
+
class Applicative[A](Functor, ABC):
|
641 |
+
@classmethod
|
642 |
+
def skip(
|
643 |
+
cls, fa: "Applicative[A]", fb: "Applicative[B]"
|
644 |
+
) -> "Applicative[B]":
|
645 |
+
'''
|
646 |
+
Sequences the effects of two Applicative computations,
|
647 |
+
but discards the result of the first.
|
648 |
+
'''
|
649 |
+
return cls.apply(cls.const(fa, id), fb)
|
650 |
+
|
651 |
+
@classmethod
|
652 |
+
def keep(
|
653 |
+
cls, fa: "Applicative[A]", fb: "Applicative[B]"
|
654 |
+
) -> "Applicative[B]":
|
655 |
+
'''
|
656 |
+
Sequences the effects of two Applicative computations,
|
657 |
+
but discard the result of the second.
|
658 |
+
'''
|
659 |
+
return cls.lift(const, fa, fb)
|
660 |
+
|
661 |
+
@classmethod
|
662 |
+
def revapp(
|
663 |
+
cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
|
664 |
+
) -> "Applicative[B]":
|
665 |
+
'''
|
666 |
+
The first computation produces values which are provided
|
667 |
+
as input to the function(s) produced by the second computation.
|
668 |
+
'''
|
669 |
+
return cls.lift(lambda a: lambda f: f(a), fa, fg)
|
670 |
+
```
|
671 |
+
|
672 |
+
- `skip` sequences the effects of two Applicative computations, but **discards the result of the first**. For example, if `m1` and `m2` are instances of type `Maybe[Int]`, then `Maybe.skip(m1, m2)` is `Nothing` whenever either `m1` or `m2` is `Nothing`; but if not, it will have the same value as `m2`.
|
673 |
+
- Likewise, `keep` sequences the effects of two computations, but **keeps only the result of the first**.
|
674 |
+
- `revapp` is similar to `apply`, but where the first computation produces value(s) which are provided as input to the function(s) produced by the second computation.
|
675 |
+
"""
|
676 |
+
)
|
677 |
+
return
|
678 |
+
|
679 |
+
|
680 |
+
@app.cell(hide_code=True)
|
681 |
+
def _(mo):
|
682 |
+
mo.md(
|
683 |
+
r"""
|
684 |
+
/// admonition | exercise
|
685 |
+
Try to use utility functions with different instances
|
686 |
+
///
|
687 |
+
"""
|
688 |
+
)
|
689 |
+
return
|
690 |
+
|
691 |
+
|
692 |
+
@app.cell(hide_code=True)
|
693 |
+
def _(mo):
|
694 |
+
mo.md(
|
695 |
+
r"""
|
696 |
+
# Formal implementation of Applicative
|
697 |
+
|
698 |
+
Now, we can give the formal implementation of `Applicative`
|
699 |
+
"""
|
700 |
+
)
|
701 |
+
return
|
702 |
+
|
703 |
+
|
704 |
+
@app.cell
|
705 |
+
def _(
|
706 |
+
ABC,
|
707 |
+
B,
|
708 |
+
Callable,
|
709 |
+
Functor,
|
710 |
+
abstractmethod,
|
711 |
+
compose,
|
712 |
+
const,
|
713 |
+
dataclass,
|
714 |
+
id,
|
715 |
+
):
|
716 |
+
@dataclass
|
717 |
+
class Applicative[A](Functor, ABC):
|
718 |
+
@classmethod
|
719 |
+
@abstractmethod
|
720 |
+
def pure(cls, a: A) -> "Applicative[A]":
|
721 |
+
"""Lift a value into the Structure."""
|
722 |
+
return NotImplementedError
|
723 |
+
|
724 |
+
@classmethod
|
725 |
+
@abstractmethod
|
726 |
+
def apply(
|
727 |
+
cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
|
728 |
+
) -> "Applicative[B]":
|
729 |
+
"""Sequential application."""
|
730 |
+
return NotImplementedError
|
731 |
+
|
732 |
+
@classmethod
|
733 |
+
def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
|
734 |
+
"""Lift a function of arbitrary arity to work with values in applicative context."""
|
735 |
+
curr = cls.pure(f)
|
736 |
+
|
737 |
+
if not args:
|
738 |
+
return curr
|
739 |
+
|
740 |
+
for arg in args:
|
741 |
+
curr = cls.apply(curr, arg)
|
742 |
+
|
743 |
+
return curr
|
744 |
+
|
745 |
+
@classmethod
|
746 |
+
def fmap(
|
747 |
+
cls, f: Callable[[A], B], fa: "Applicative[A]"
|
748 |
+
) -> "Applicative[B]":
|
749 |
+
return cls.lift(f, fa)
|
750 |
+
|
751 |
+
@classmethod
|
752 |
+
def sequenceL(cls, fas: list["Applicative[A]"]) -> "Applicative[list[A]]":
|
753 |
+
"""
|
754 |
+
Execute a list of commands and collect the list of their response.
|
755 |
+
"""
|
756 |
+
if not fas:
|
757 |
+
return cls.pure([])
|
758 |
+
|
759 |
+
return cls.apply(
|
760 |
+
cls.fmap(lambda v: lambda vs: [v] + vs, fas[0]),
|
761 |
+
cls.sequenceL(fas[1:]),
|
762 |
+
)
|
763 |
+
|
764 |
+
@classmethod
|
765 |
+
def skip(
|
766 |
+
cls, fa: "Applicative[A]", fb: "Applicative[B]"
|
767 |
+
) -> "Applicative[B]":
|
768 |
+
"""
|
769 |
+
Sequences the effects of two Applicative computations,
|
770 |
+
but discards the result of the first.
|
771 |
+
"""
|
772 |
+
return cls.apply(cls.const(fa, id), fb)
|
773 |
+
|
774 |
+
@classmethod
|
775 |
+
def keep(
|
776 |
+
cls, fa: "Applicative[A]", fb: "Applicative[B]"
|
777 |
+
) -> "Applicative[B]":
|
778 |
+
"""
|
779 |
+
Sequences the effects of two Applicative computations,
|
780 |
+
but discard the result of the second.
|
781 |
+
"""
|
782 |
+
return cls.lift(const, fa, fb)
|
783 |
+
|
784 |
+
@classmethod
|
785 |
+
def revapp(
|
786 |
+
cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
|
787 |
+
) -> "Applicative[B]":
|
788 |
+
"""
|
789 |
+
The first computation produces values which are provided
|
790 |
+
as input to the function(s) produced by the second computation.
|
791 |
+
"""
|
792 |
+
return cls.lift(lambda a: lambda f: f(a), fa, fg)
|
793 |
+
|
794 |
+
@classmethod
|
795 |
+
def check_identity(cls, fa: "Applicative[A]"):
|
796 |
+
if cls.lift(id, fa) != fa:
|
797 |
+
raise ValueError("Instance violates identity law")
|
798 |
+
return True
|
799 |
+
|
800 |
+
@classmethod
|
801 |
+
def check_homomorphism(cls, a: A, f: Callable[[A], B]):
|
802 |
+
if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
|
803 |
+
raise ValueError("Instance violates homomorphism law")
|
804 |
+
return True
|
805 |
+
|
806 |
+
@classmethod
|
807 |
+
def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
|
808 |
+
if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
|
809 |
+
raise ValueError("Instance violates interchange law")
|
810 |
+
return True
|
811 |
+
|
812 |
+
@classmethod
|
813 |
+
def check_composition(
|
814 |
+
cls,
|
815 |
+
fg: "Applicative[Callable[[B], C]]",
|
816 |
+
fh: "Applicative[Callable[[A], B]]",
|
817 |
+
fa: "Applicative[A]",
|
818 |
+
):
|
819 |
+
if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
|
820 |
+
raise ValueError("Instance violates composition law")
|
821 |
+
return True
|
822 |
+
return (Applicative,)
|
823 |
+
|
824 |
+
|
825 |
+
@app.cell(hide_code=True)
|
826 |
+
def _(mo):
|
827 |
+
mo.md(
|
828 |
+
r"""
|
829 |
+
# Effectful programming
|
830 |
+
|
831 |
+
Our original motivation for applicatives was the desire to generalise the idea of mapping to functions with multiple arguments. This is a valid interpretation of the concept of applicatives, but from the three instances we have seen it becomes clear that there is also another, more abstract view.
|
832 |
+
|
833 |
+
The arguments are no longer just plain values but may also have effects, such as the possibility of failure, having many ways to succeed, or performing input/output actions. In this manner, applicative functors can also be viewed as abstracting the idea of **applying pure functions to effectful arguments**, with the precise form of effects that are permitted depending on the nature of the underlying functor.
|
834 |
+
"""
|
835 |
+
)
|
836 |
+
return
|
837 |
+
|
838 |
+
|
839 |
+
@app.cell(hide_code=True)
|
840 |
+
def _(mo):
|
841 |
+
mo.md(
|
842 |
+
r"""
|
843 |
+
## The IO Applicative
|
844 |
+
|
845 |
+
We will try to define an `IO` applicative here.
|
846 |
+
|
847 |
+
As before, we first abstract how `pure` and `apply` should function.
|
848 |
+
|
849 |
+
- `pure` should wrap the object in an IO action, and make the object *callable* if it's not because we want to perform the action later:
|
850 |
+
|
851 |
+
```haskell
|
852 |
+
IO.pure(1) => IO(effect=lambda: 1)
|
853 |
+
IO.pure(f) => IO(effect=f)
|
854 |
+
```
|
855 |
+
|
856 |
+
- `apply` should perform an action that produces a value, then apply the function with the value
|
857 |
+
|
858 |
+
The implementation is:
|
859 |
+
"""
|
860 |
+
)
|
861 |
+
return
|
862 |
+
|
863 |
+
|
864 |
+
@app.cell
|
865 |
+
def _(Applicative, Callable, dataclass):
|
866 |
+
@dataclass
|
867 |
+
class IO(Applicative):
|
868 |
+
effect: Callable
|
869 |
+
|
870 |
+
def __call__(self):
|
871 |
+
return self.effect()
|
872 |
+
|
873 |
+
@classmethod
|
874 |
+
def pure(cls, a):
|
875 |
+
return cls(a) if isinstance(a, Callable) else IO(lambda: a)
|
876 |
+
|
877 |
+
@classmethod
|
878 |
+
def apply(cls, fg, fa):
|
879 |
+
return cls.pure(fg.effect(fa.effect()))
|
880 |
+
return (IO,)
|
881 |
+
|
882 |
+
|
883 |
+
@app.cell(hide_code=True)
|
884 |
+
def _(mo):
|
885 |
+
mo.md(r"""For example, a function that reads a given number of lines from the keyboard can be defined in applicative style as follows:""")
|
886 |
+
return
|
887 |
+
|
888 |
+
|
889 |
+
@app.cell
|
890 |
+
def _(IO):
|
891 |
+
def get_chars(n: int = 3):
|
892 |
+
return IO.sequenceL(
|
893 |
+
[IO.pure(input(f"input the {i}th str")) for i in range(1, n + 1)]
|
894 |
+
)
|
895 |
+
return (get_chars,)
|
896 |
+
|
897 |
+
|
898 |
+
@app.cell
|
899 |
+
def _():
|
900 |
+
# get_chars()()
|
901 |
+
return
|
902 |
+
|
903 |
+
|
904 |
+
@app.cell(hide_code=True)
|
905 |
+
def _(mo):
|
906 |
+
mo.md(r"""# From the perspective of category theory""")
|
907 |
+
return
|
908 |
+
|
909 |
+
|
910 |
+
@app.cell(hide_code=True)
|
911 |
+
def _(mo):
|
912 |
+
mo.md(
|
913 |
+
r"""
|
914 |
+
## Lax Monoidal Functor
|
915 |
+
|
916 |
+
An alternative, equivalent formulation of `Applicative` is given by
|
917 |
+
"""
|
918 |
+
)
|
919 |
+
return
|
920 |
+
|
921 |
+
|
922 |
+
@app.cell
|
923 |
+
def _(ABC, Functor, abstractmethod, dataclass):
|
924 |
+
@dataclass
|
925 |
+
class Monoidal[A](Functor, ABC):
|
926 |
+
@classmethod
|
927 |
+
@abstractmethod
|
928 |
+
def unit(cls) -> "Monoidal[Tuple[()]]":
|
929 |
+
pass
|
930 |
+
|
931 |
+
@classmethod
|
932 |
+
@abstractmethod
|
933 |
+
def tensor(
|
934 |
+
cls, this: "Monoidal[A]", other: "Monoidal[B]"
|
935 |
+
) -> "Monoidal[Tuple[A, B]]":
|
936 |
+
pass
|
937 |
+
return (Monoidal,)
|
938 |
+
|
939 |
+
|
940 |
+
@app.cell(hide_code=True)
|
941 |
+
def _(mo):
|
942 |
+
mo.md(
|
943 |
+
r"""
|
944 |
+
Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
|
945 |
+
|
946 |
+
- `unit` provides the identity element
|
947 |
+
- `tensor` combines two contexts into a product context
|
948 |
+
|
949 |
+
More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
|
950 |
+
"""
|
951 |
+
)
|
952 |
+
return
|
953 |
+
|
954 |
+
|
955 |
+
@app.cell(hide_code=True)
|
956 |
+
def _(mo):
|
957 |
+
mo.md(
|
958 |
+
r"""
|
959 |
+
Furthermore, to deserve the name "monoidal", instances of Monoidal ought to satisfy the following laws, which seem much more straightforward than the traditional Applicative laws:
|
960 |
+
|
961 |
+
- Left identity
|
962 |
+
|
963 |
+
`tensor(unit, v) ≅ v`
|
964 |
+
|
965 |
+
- Right identity
|
966 |
+
|
967 |
+
`tensor(u, unit) ≅ u`
|
968 |
+
|
969 |
+
- Associativity
|
970 |
+
|
971 |
+
`tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
|
972 |
+
"""
|
973 |
+
)
|
974 |
+
return
|
975 |
+
|
976 |
+
|
977 |
+
@app.cell(hide_code=True)
|
978 |
+
def _(mo):
|
979 |
+
mo.md(
|
980 |
+
r"""
|
981 |
+
/// admonition | ≅ indicates isomorphism
|
982 |
+
|
983 |
+
`≅` refers to *isomorphism* rather than equality.
|
984 |
+
|
985 |
+
In particular we consider `(x, ()) ≅ x ≅ ((), x)` and `((x, y), z) ≅ (x, (y, z))`
|
986 |
+
|
987 |
+
///
|
988 |
+
"""
|
989 |
+
)
|
990 |
+
return
|
991 |
+
|
992 |
+
|
993 |
+
@app.cell(hide_code=True)
|
994 |
+
def _(mo):
|
995 |
+
mo.md(
|
996 |
+
r"""
|
997 |
+
## Mutual definability of Monoidal and Applicative
|
998 |
+
|
999 |
+
We can implement `pure` and `apply` in terms of `unit` and `tensor`, and vice versa.
|
1000 |
+
|
1001 |
+
```python
|
1002 |
+
pure(a) = fmap((lambda _: a), unit)
|
1003 |
+
apply(fg, fa) = fmap((lambda pair: pair[0](pair[1])), tensor(fg, fa))
|
1004 |
+
```
|
1005 |
+
|
1006 |
+
```python
|
1007 |
+
unit() = pure(())
|
1008 |
+
tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)
|
1009 |
+
```
|
1010 |
+
"""
|
1011 |
+
)
|
1012 |
+
return
|
1013 |
+
|
1014 |
+
|
1015 |
+
@app.cell(hide_code=True)
|
1016 |
+
def _(mo):
|
1017 |
+
mo.md(
|
1018 |
+
r"""
|
1019 |
+
## Instance: ListMonoidal
|
1020 |
+
|
1021 |
+
- `unit` should simply return a empty tuple wrapper in a list
|
1022 |
+
|
1023 |
+
```haskell
|
1024 |
+
ListMonoidal.unit() => [()]
|
1025 |
+
```
|
1026 |
+
|
1027 |
+
- `tensor` should return the *cartesian product* of the items of 2 ListMonoidal instances
|
1028 |
+
|
1029 |
+
The implementation is:
|
1030 |
+
"""
|
1031 |
+
)
|
1032 |
+
return
|
1033 |
+
|
1034 |
+
|
1035 |
+
@app.cell
|
1036 |
+
def _(B, Callable, Monoidal, dataclass, product):
|
1037 |
+
@dataclass
|
1038 |
+
class ListMonoidal[A](Monoidal):
|
1039 |
+
items: list[A]
|
1040 |
+
|
1041 |
+
@classmethod
|
1042 |
+
def unit(cls) -> "ListMonoidal[Tuple[()]]":
|
1043 |
+
return cls([()])
|
1044 |
+
|
1045 |
+
@classmethod
|
1046 |
+
def tensor(
|
1047 |
+
cls, this: "ListMonoidal[A]", other: "ListMonoidal[B]"
|
1048 |
+
) -> "ListMonoidal[Tuple[A, B]]":
|
1049 |
+
return cls(list(product(this.items, other.items)))
|
1050 |
+
|
1051 |
+
@classmethod
|
1052 |
+
def fmap(
|
1053 |
+
cls, f: Callable[[A], B], ma: "ListMonoidal[A]"
|
1054 |
+
) -> "ListMonoidal[B]":
|
1055 |
+
return cls([f(a) for a in ma.items])
|
1056 |
+
return (ListMonoidal,)
|
1057 |
+
|
1058 |
+
|
1059 |
+
@app.cell(hide_code=True)
|
1060 |
+
def _(mo):
|
1061 |
+
mo.md(r"""> try with `ListMonoidal` below""")
|
1062 |
+
return
|
1063 |
+
|
1064 |
+
|
1065 |
+
@app.cell
|
1066 |
+
def _(ListMonoidal):
|
1067 |
+
xs = ListMonoidal([1, 2])
|
1068 |
+
ys = ListMonoidal(["a", "b"])
|
1069 |
+
ListMonoidal.tensor(xs, ys)
|
1070 |
+
return xs, ys
|
1071 |
+
|
1072 |
+
|
1073 |
+
@app.cell(hide_code=True)
|
1074 |
+
def _(mo):
|
1075 |
+
mo.md(r"""and we can prove that `tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)`:""")
|
1076 |
+
return
|
1077 |
+
|
1078 |
+
|
1079 |
+
@app.cell
|
1080 |
+
def _(List, xs, ys):
|
1081 |
+
List.lift(lambda fa: lambda fb: (fa, fb), List(xs.items), List(ys.items))
|
1082 |
+
return
|
1083 |
+
|
1084 |
+
|
1085 |
+
@app.cell(hide_code=True)
|
1086 |
+
def _(ABC, B, Callable, abstractmethod, dataclass):
|
1087 |
+
@dataclass
|
1088 |
+
class Functor[A](ABC):
|
1089 |
+
@classmethod
|
1090 |
+
@abstractmethod
|
1091 |
+
def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
|
1092 |
+
return NotImplementedError
|
1093 |
+
|
1094 |
+
@classmethod
|
1095 |
+
def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
|
1096 |
+
return cls.fmap(lambda _: b, a)
|
1097 |
+
|
1098 |
+
@classmethod
|
1099 |
+
def void(cls, a: "Functor[A]") -> "Functor[None]":
|
1100 |
+
return cls.const_fmap(a, None)
|
1101 |
+
return (Functor,)
|
1102 |
+
|
1103 |
+
|
1104 |
+
@app.cell(hide_code=True)
|
1105 |
+
def _():
|
1106 |
+
import marimo as mo
|
1107 |
+
return (mo,)
|
1108 |
+
|
1109 |
+
|
1110 |
+
@app.cell(hide_code=True)
|
1111 |
+
def _():
|
1112 |
+
from dataclasses import dataclass
|
1113 |
+
from abc import ABC, abstractmethod
|
1114 |
+
from typing import TypeVar, Union
|
1115 |
+
from collections.abc import Callable
|
1116 |
+
return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
|
1117 |
+
|
1118 |
+
|
1119 |
+
@app.cell(hide_code=True)
|
1120 |
+
def _():
|
1121 |
+
from itertools import product
|
1122 |
+
return (product,)
|
1123 |
+
|
1124 |
+
|
1125 |
+
@app.cell(hide_code=True)
|
1126 |
+
def _(TypeVar):
|
1127 |
+
A = TypeVar("A")
|
1128 |
+
B = TypeVar("B")
|
1129 |
+
C = TypeVar("C")
|
1130 |
+
return A, B, C
|
1131 |
+
|
1132 |
+
|
1133 |
+
@app.cell(hide_code=True)
|
1134 |
+
def _(mo):
|
1135 |
+
mo.md(
|
1136 |
+
r"""
|
1137 |
+
# Further reading
|
1138 |
+
|
1139 |
+
Notice that these reading sources are optional and non-trivial
|
1140 |
+
|
1141 |
+
- [Applicaive Programming with Effects](https://www.staff.city.ac.uk/~ross/papers/Applicative.html)
|
1142 |
+
- [Equivalence of Applicative Functors and
|
1143 |
+
Multifunctors](https://arxiv.org/pdf/2401.14286)
|
1144 |
+
- [Applicative functor](https://wiki.haskell.org/index.php?title=Applicative_functor)
|
1145 |
+
- [Control.Applicative](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#t:Applicative)
|
1146 |
+
- [Typeclassopedia#Applicative](https://wiki.haskell.org/index.php?title=Typeclassopedia#Applicative)
|
1147 |
+
- [Notions of computation as monoids](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/notions-of-computation-as-monoids/70019FC0F2384270E9F41B9719042528)
|
1148 |
+
- [Free Applicative Functors](https://arxiv.org/abs/1403.0749)
|
1149 |
+
- [The basics of applicative functors, put to practical work](http://www.serpentine.com/blog/2008/02/06/the-basics-of-applicative-functors-put-to-practical-work/)
|
1150 |
+
- [Abstracting with Applicatives](http://comonad.com/reader/2012/abstracting-with-applicatives/)
|
1151 |
+
- [Static analysis with Applicatives](https://gergo.erdi.hu/blog/2012-12-01-static_analysis_with_applicatives/)
|
1152 |
+
- [Explaining Applicative functor in categorical terms - monoidal functors](https://cstheory.stackexchange.com/questions/12412/explaining-applicative-functor-in-categorical-terms-monoidal-functors)
|
1153 |
+
- [Applicative, A Strong Lax Monoidal Functor](https://beuke.org/applicative/)
|
1154 |
+
- [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
|
1155 |
+
"""
|
1156 |
+
)
|
1157 |
+
return
|
1158 |
+
|
1159 |
+
|
1160 |
+
if __name__ == "__main__":
|
1161 |
+
app.run()
|
functional_programming/CHANGELOG.md
CHANGED
@@ -1,14 +1,21 @@
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
3 |
-
## 2025-
|
4 |
|
5 |
-
*
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
-
## 2025-
|
8 |
|
9 |
-
|
10 |
|
11 |
-
|
|
|
|
|
12 |
|
13 |
## 2025-03-16
|
14 |
|
@@ -34,3 +41,13 @@ Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Ha
|
|
34 |
- Rename `ListWrapper` to `List` for simplicity
|
35 |
- Remove the `Just` class
|
36 |
+ Rewrite proofs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
3 |
+
## 2025-04-07
|
4 |
|
5 |
+
* the `apply` method of `Maybe` *Applicative* should return `None` when `fg` or `fa` is `None`
|
6 |
+
+ add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`, `Maybe`, `List`
|
7 |
+
+ add description for utility functions of `Applicative`
|
8 |
+
* refine the implementation of `IO` *Applicative*
|
9 |
+
* reimplement `get_chars` with `IO.sequenceL`
|
10 |
+
+ add an example to show that `ListMonoidal` is equivalent to `List` *Applicative*
|
11 |
|
12 |
+
## 2025-04-06
|
13 |
|
14 |
+
- remove `sequenceL` from `Applicative` because it should be a classmethod but can't be generically implemented
|
15 |
|
16 |
+
## 2025-04-02
|
17 |
+
|
18 |
+
* `0.1.0` version of notebook `06_applicatives.py`
|
19 |
|
20 |
## 2025-03-16
|
21 |
|
|
|
41 |
- Rename `ListWrapper` to `List` for simplicity
|
42 |
- Remove the `Just` class
|
43 |
+ Rewrite proofs
|
44 |
+
|
45 |
+
## 2025-03-13
|
46 |
+
|
47 |
+
* `0.1.0` version of notebook `05_functors`
|
48 |
+
|
49 |
+
Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
|
50 |
+
|
51 |
+
## 2025-03-11
|
52 |
+
|
53 |
+
* Demo version of notebook `05_functors.py`
|