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