Spaces:
Running
Running
Commit
·
7139673
1
Parent(s):
8bc43c8
refactor(functor): migrate to python3.13
Browse files
functional_programming/05_functors.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
# /// script
|
2 |
-
# requires-python = ">=3.
|
3 |
# dependencies = [
|
4 |
# "marimo",
|
5 |
# ]
|
@@ -7,7 +7,7 @@
|
|
7 |
|
8 |
import marimo
|
9 |
|
10 |
-
__generated_with = "0.
|
11 |
app = marimo.App(app_title="Category Theory and Functors")
|
12 |
|
13 |
|
@@ -37,7 +37,7 @@ def _(mo):
|
|
37 |
/// details | Notebook metadata
|
38 |
type: info
|
39 |
|
40 |
-
version: 0.1.
|
41 |
reviewer: [Haleshot](https://github.com/Haleshot)
|
42 |
|
43 |
///
|
@@ -77,13 +77,13 @@ def _(mo):
|
|
77 |
|
78 |
```python
|
79 |
from dataclasses import dataclass
|
80 |
-
from typing import Callable,
|
81 |
|
82 |
A = TypeVar("A")
|
83 |
B = TypeVar("B")
|
84 |
|
85 |
@dataclass
|
86 |
-
class Wrapper
|
87 |
value: A
|
88 |
```
|
89 |
|
@@ -97,48 +97,38 @@ def _(mo):
|
|
97 |
|
98 |
To modify wrapped data while keeping it wrapped, we define an `fmap` method:
|
99 |
|
100 |
-
```python
|
101 |
-
@dataclass
|
102 |
-
class Wrapper(Functor, Generic[A]):
|
103 |
-
value: A
|
104 |
-
|
105 |
-
@classmethod
|
106 |
-
def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
|
107 |
-
return Wrapper(f(a.value))
|
108 |
-
```
|
109 |
|
110 |
-
Now, we can apply transformations without unwrapping:
|
111 |
|
112 |
-
```python
|
113 |
-
>>> Wrapper.fmap(lambda x: x + 1, wrapper)
|
114 |
-
Wrapper(value=2)
|
115 |
-
|
116 |
-
>>> Wrapper.fmap(lambda x: [x], wrapper)
|
117 |
-
Wrapper(value=[1])
|
118 |
-
```
|
119 |
-
|
120 |
-
> Try using the `Wrapper` in the cell below.
|
121 |
"""
|
122 |
)
|
123 |
return
|
124 |
|
125 |
|
126 |
@app.cell
|
127 |
-
def _(
|
128 |
@dataclass
|
129 |
-
class Wrapper
|
130 |
value: A
|
131 |
|
132 |
@classmethod
|
133 |
-
def fmap(cls, f: Callable[[A], B],
|
134 |
-
return Wrapper(f(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
|
|
|
|
|
137 |
wrapper = Wrapper(1)
|
138 |
|
139 |
pp(Wrapper.fmap(lambda x: x + 1, wrapper))
|
140 |
pp(Wrapper.fmap(lambda x: [x], wrapper))
|
141 |
-
return
|
142 |
|
143 |
|
144 |
@app.cell(hide_code=True)
|
@@ -148,13 +138,13 @@ def _(mo):
|
|
148 |
We can analyze the type signature of `fmap` for `Wrapper`:
|
149 |
|
150 |
* `f` is of type `Callable[[A], B]`
|
151 |
-
* `
|
152 |
* The return value is of type `Wrapper[B]`
|
153 |
|
154 |
Thus, in Python's type system, we can express the type signature of `fmap` as:
|
155 |
|
156 |
```python
|
157 |
-
fmap(f: Callable[[A], B],
|
158 |
```
|
159 |
|
160 |
Essentially, `fmap`:
|
@@ -176,46 +166,35 @@ def _(mo):
|
|
176 |
## The List Wrapper
|
177 |
|
178 |
We can define a `List` class to represent a wrapped list that supports `fmap`:
|
179 |
-
|
180 |
-
```python
|
181 |
-
@dataclass
|
182 |
-
class List(Functor, Generic[A]):
|
183 |
-
value: list[A]
|
184 |
-
|
185 |
-
@classmethod
|
186 |
-
def fmap(cls, f: Callable[[A], B], a: "List[A]") -> "List[B]":
|
187 |
-
return List([f(x) for x in a.value])
|
188 |
-
```
|
189 |
-
|
190 |
-
Now, we can apply transformations:
|
191 |
-
|
192 |
-
```python
|
193 |
-
>>> flist = List([1, 2, 3, 4])
|
194 |
-
>>> List.fmap(lambda x: x + 1, flist)
|
195 |
-
List(value=[2, 3, 4, 5])
|
196 |
-
>>> List.fmap(lambda x: [x], flist)
|
197 |
-
List(value=[[1], [2], [3], [4]])
|
198 |
-
```
|
199 |
"""
|
200 |
)
|
201 |
return
|
202 |
|
203 |
|
204 |
@app.cell
|
205 |
-
def _(
|
206 |
@dataclass
|
207 |
-
class List
|
208 |
value: list[A]
|
209 |
|
210 |
@classmethod
|
211 |
-
def fmap(cls, f: Callable[[A], B],
|
212 |
-
return List([f(x) for x in
|
|
|
213 |
|
214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
flist = List([1, 2, 3, 4])
|
216 |
pp(List.fmap(lambda x: x + 1, flist))
|
217 |
pp(List.fmap(lambda x: [x], flist))
|
218 |
-
return
|
219 |
|
220 |
|
221 |
@app.cell(hide_code=True)
|
@@ -227,19 +206,19 @@ def _(mo):
|
|
227 |
The type signature of `fmap` for `List` is:
|
228 |
|
229 |
```python
|
230 |
-
fmap(f: Callable[[A], B],
|
231 |
```
|
232 |
|
233 |
Similarly, for `Wrapper`:
|
234 |
|
235 |
```python
|
236 |
-
fmap(f: Callable[[A], B],
|
237 |
```
|
238 |
|
239 |
Both follow the same pattern, which we can generalize as:
|
240 |
|
241 |
```python
|
242 |
-
fmap(f: Callable[[A], B],
|
243 |
```
|
244 |
|
245 |
where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
|
@@ -278,17 +257,17 @@ def _(mo):
|
|
278 |
|
279 |
```python
|
280 |
from dataclasses import dataclass
|
281 |
-
from typing import Callable,
|
282 |
from abc import ABC, abstractmethod
|
283 |
|
284 |
A = TypeVar("A")
|
285 |
B = TypeVar("B")
|
286 |
|
287 |
@dataclass
|
288 |
-
class Functor
|
289 |
@classmethod
|
290 |
@abstractmethod
|
291 |
-
def fmap(f: Callable[[A], B],
|
292 |
raise NotImplementedError
|
293 |
```
|
294 |
|
@@ -321,21 +300,21 @@ def _(mo):
|
|
321 |
|
322 |
```python
|
323 |
from dataclasses import dataclass
|
324 |
-
from typing import Callable,
|
325 |
|
326 |
A = TypeVar("A")
|
327 |
B = TypeVar("B")
|
328 |
|
329 |
@dataclass
|
330 |
-
class RoseTree(Functor
|
331 |
-
|
332 |
value: A
|
333 |
children: list["RoseTree[A]"]
|
334 |
|
335 |
@classmethod
|
336 |
-
def fmap(cls, f: Callable[[A], B],
|
337 |
return RoseTree(
|
338 |
-
f(
|
339 |
)
|
340 |
|
341 |
def __repr__(self) -> str:
|
@@ -353,9 +332,9 @@ def _(mo):
|
|
353 |
|
354 |
|
355 |
@app.cell(hide_code=True)
|
356 |
-
def _(
|
357 |
@dataclass
|
358 |
-
class RoseTree
|
359 |
"""
|
360 |
### Doc: RoseTree
|
361 |
|
@@ -368,7 +347,7 @@ def _(A, B, Callable, Functor, Generic, dataclass, mo):
|
|
368 |
|
369 |
**Methods:**
|
370 |
|
371 |
-
- `fmap(f: Callable[[A], B],
|
372 |
|
373 |
Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
|
374 |
|
@@ -383,9 +362,9 @@ def _(A, B, Callable, Functor, Generic, dataclass, mo):
|
|
383 |
children: list["RoseTree[A]"]
|
384 |
|
385 |
@classmethod
|
386 |
-
def fmap(cls, f: Callable[[A], B],
|
387 |
return RoseTree(
|
388 |
-
f(
|
389 |
)
|
390 |
|
391 |
def __repr__(self) -> str:
|
@@ -549,14 +528,14 @@ def _(mo):
|
|
549 |
|
550 |
```python
|
551 |
@dataclass
|
552 |
-
class EvilFunctor
|
553 |
value: list[A]
|
554 |
|
555 |
@classmethod
|
556 |
-
def fmap(cls, f: Callable[[A], B],
|
557 |
return (
|
558 |
-
cls([
|
559 |
-
if
|
560 |
else []
|
561 |
)
|
562 |
```
|
@@ -566,18 +545,18 @@ def _(mo):
|
|
566 |
|
567 |
|
568 |
@app.cell
|
569 |
-
def _(
|
570 |
@dataclass
|
571 |
-
class EvilFunctor
|
572 |
value: list[A]
|
573 |
|
574 |
@classmethod
|
575 |
def fmap(
|
576 |
-
cls, f: Callable[[A], B],
|
577 |
) -> "EvilFunctor[B]":
|
578 |
return (
|
579 |
-
cls([
|
580 |
-
if
|
581 |
else []
|
582 |
)
|
583 |
|
@@ -597,16 +576,16 @@ def _(mo):
|
|
597 |
```Python
|
598 |
@classmethod
|
599 |
@abstractmethod
|
600 |
-
def fmap(cls, f: Callable[[A], B],
|
601 |
return NotImplementedError
|
602 |
|
603 |
@classmethod
|
604 |
-
def const_fmap(cls,
|
605 |
-
return cls.fmap(lambda _: b,
|
606 |
|
607 |
@classmethod
|
608 |
-
def void(cls,
|
609 |
-
return cls.const_fmap(
|
610 |
```
|
611 |
"""
|
612 |
)
|
@@ -614,9 +593,9 @@ def _(mo):
|
|
614 |
|
615 |
|
616 |
@app.cell(hide_code=True)
|
617 |
-
def _(
|
618 |
@dataclass
|
619 |
-
class Functor
|
620 |
"""
|
621 |
### Doc: Functor
|
622 |
|
@@ -624,28 +603,28 @@ def _(A, ABC, B, Callable, Generic, abstractmethod, dataclass, mo):
|
|
624 |
|
625 |
**Methods:**
|
626 |
|
627 |
-
- `fmap(f: Callable[[A], B],
|
628 |
Abstract method to apply a function to all values inside a functor.
|
629 |
|
630 |
-
- `const_fmap(
|
631 |
Replaces all values inside a functor with a constant `b`, preserving the original structure.
|
632 |
|
633 |
-
- `void(
|
634 |
-
Equivalent to `const_fmap(
|
635 |
"""
|
636 |
|
637 |
@classmethod
|
638 |
@abstractmethod
|
639 |
-
def fmap(cls, f: Callable[[A], B],
|
640 |
return NotImplementedError
|
641 |
|
642 |
@classmethod
|
643 |
-
def const_fmap(cls,
|
644 |
-
return cls.fmap(lambda _: b,
|
645 |
|
646 |
@classmethod
|
647 |
-
def void(cls,
|
648 |
-
return cls.const_fmap(
|
649 |
|
650 |
|
651 |
mo.md(Functor.__doc__)
|
@@ -682,35 +661,20 @@ def _(mo):
|
|
682 |
One example is the **`Maybe`** type from Haskell, which is used to represent computations that can either result in a value or no value (`Nothing`).
|
683 |
|
684 |
We can define the `Maybe` functor as below:
|
685 |
-
|
686 |
-
```python
|
687 |
-
@dataclass
|
688 |
-
class Maybe(Functor, Generic[A]):
|
689 |
-
value: None | A
|
690 |
-
|
691 |
-
@classmethod
|
692 |
-
def fmap(cls, f: Callable[[A], B], a: "Maybe[A]") -> "Maybe[B]":
|
693 |
-
return (
|
694 |
-
cls(None) if a.value is None else cls(f(a.value))
|
695 |
-
)
|
696 |
-
|
697 |
-
def __repr__(self):
|
698 |
-
return "Nothing" if self.value is None else repr(self.value)
|
699 |
-
```
|
700 |
"""
|
701 |
)
|
702 |
return
|
703 |
|
704 |
|
705 |
@app.cell
|
706 |
-
def _(
|
707 |
@dataclass
|
708 |
-
class Maybe
|
709 |
value: None | A
|
710 |
|
711 |
@classmethod
|
712 |
-
def fmap(cls, f: Callable[[A], B],
|
713 |
-
return cls(None) if
|
714 |
|
715 |
def __repr__(self):
|
716 |
return "Nothing" if self.value is None else repr(self.value)
|
@@ -815,7 +779,7 @@ def _(mo):
|
|
815 |
Remember that we defined the `id` and `compose` function above as:
|
816 |
|
817 |
```Python
|
818 |
-
def id(x:
|
819 |
return x
|
820 |
|
821 |
def compose(f: Callable[[B], C], g: Callable[[A], B]) -> Callable[[A], C]:
|
@@ -893,14 +857,14 @@ def _(mo):
|
|
893 |
|
894 |
```Python
|
895 |
@dataclass
|
896 |
-
class Functor
|
897 |
```
|
898 |
|
899 |
And RoseTree:
|
900 |
|
901 |
```Python
|
902 |
@dataclass
|
903 |
-
class RoseTree
|
904 |
```
|
905 |
|
906 |
**Here's the key part:** the _type constructor_ `RoseTree` takes any type `T` to a new type, `RoseTree[T]`. Also, `fmap` restricted to `RoseTree` types takes a function `Callable[[A], B]` to a function `Callable[[RoseTree[A]], RoseTree[B]]`.
|
@@ -977,7 +941,7 @@ def _(mo):
|
|
977 |
|
978 |
```Python
|
979 |
@dataclass
|
980 |
-
class Wrapper
|
981 |
value: A
|
982 |
|
983 |
@classmethod
|
@@ -1063,31 +1027,15 @@ def _(mo):
|
|
1063 |
### Category of List Concatenation
|
1064 |
|
1065 |
First, let’s define the category of list concatenation:
|
1066 |
-
|
1067 |
-
```python
|
1068 |
-
@dataclass
|
1069 |
-
class ListConcatenation(Generic[A]):
|
1070 |
-
value: list[A]
|
1071 |
-
|
1072 |
-
@staticmethod
|
1073 |
-
def id() -> "ListConcatenation[A]":
|
1074 |
-
return ListConcatenation([])
|
1075 |
-
|
1076 |
-
@staticmethod
|
1077 |
-
def compose(
|
1078 |
-
this: "ListConcatenation[A]", other: "ListConcatenation[A]"
|
1079 |
-
) -> "ListConcatenation[a]":
|
1080 |
-
return ListConcatenation(this.value + other.value)
|
1081 |
-
```
|
1082 |
"""
|
1083 |
)
|
1084 |
return
|
1085 |
|
1086 |
|
1087 |
@app.cell
|
1088 |
-
def _(A,
|
1089 |
@dataclass
|
1090 |
-
class ListConcatenation
|
1091 |
value: list[A]
|
1092 |
|
1093 |
@staticmethod
|
@@ -1120,20 +1068,6 @@ def _(mo):
|
|
1120 |
### Category of Integer Addition
|
1121 |
|
1122 |
Now, let's define the category of integer addition:
|
1123 |
-
|
1124 |
-
```python
|
1125 |
-
@dataclass
|
1126 |
-
class IntAddition:
|
1127 |
-
value: int
|
1128 |
-
|
1129 |
-
@staticmethod
|
1130 |
-
def id() -> "IntAddition":
|
1131 |
-
return IntAddition(0)
|
1132 |
-
|
1133 |
-
@staticmethod
|
1134 |
-
def compose(this: "IntAddition", other: "IntAddition") -> "IntAddition":
|
1135 |
-
return IntAddition(this.value + other.value)
|
1136 |
-
```
|
1137 |
"""
|
1138 |
)
|
1139 |
return
|
@@ -1296,9 +1230,9 @@ def _():
|
|
1296 |
@app.cell(hide_code=True)
|
1297 |
def _():
|
1298 |
from dataclasses import dataclass
|
1299 |
-
from typing import Callable,
|
1300 |
from pprint import pp
|
1301 |
-
return Callable,
|
1302 |
|
1303 |
|
1304 |
@app.cell(hide_code=True)
|
|
|
1 |
# /// script
|
2 |
+
# requires-python = ">=3.13"
|
3 |
# dependencies = [
|
4 |
# "marimo",
|
5 |
# ]
|
|
|
7 |
|
8 |
import marimo
|
9 |
|
10 |
+
__generated_with = "0.12.0"
|
11 |
app = marimo.App(app_title="Category Theory and Functors")
|
12 |
|
13 |
|
|
|
37 |
/// details | Notebook metadata
|
38 |
type: info
|
39 |
|
40 |
+
version: 0.1.2 | last modified: 2025-04-02 | author: [métaboulie](https://github.com/metaboulie)<br/>
|
41 |
reviewer: [Haleshot](https://github.com/Haleshot)
|
42 |
|
43 |
///
|
|
|
77 |
|
78 |
```python
|
79 |
from dataclasses import dataclass
|
80 |
+
from typing import Callable, TypeVar
|
81 |
|
82 |
A = TypeVar("A")
|
83 |
B = TypeVar("B")
|
84 |
|
85 |
@dataclass
|
86 |
+
class Wrapper[A]:
|
87 |
value: A
|
88 |
```
|
89 |
|
|
|
97 |
|
98 |
To modify wrapped data while keeping it wrapped, we define an `fmap` method:
|
99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
|
|
|
101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
"""
|
103 |
)
|
104 |
return
|
105 |
|
106 |
|
107 |
@app.cell
|
108 |
+
def _(B, Callable, Functor, dataclass):
|
109 |
@dataclass
|
110 |
+
class Wrapper[A](Functor):
|
111 |
value: A
|
112 |
|
113 |
@classmethod
|
114 |
+
def fmap(cls, f: Callable[[A], B], fa: "Wrapper[A]") -> "Wrapper[B]":
|
115 |
+
return Wrapper(f(fa.value))
|
116 |
+
return (Wrapper,)
|
117 |
+
|
118 |
+
|
119 |
+
@app.cell(hide_code=True)
|
120 |
+
def _(mo):
|
121 |
+
mo.md(r"""> Try with Wrapper below""")
|
122 |
+
return
|
123 |
|
124 |
|
125 |
+
@app.cell
|
126 |
+
def _(Wrapper, pp):
|
127 |
wrapper = Wrapper(1)
|
128 |
|
129 |
pp(Wrapper.fmap(lambda x: x + 1, wrapper))
|
130 |
pp(Wrapper.fmap(lambda x: [x], wrapper))
|
131 |
+
return (wrapper,)
|
132 |
|
133 |
|
134 |
@app.cell(hide_code=True)
|
|
|
138 |
We can analyze the type signature of `fmap` for `Wrapper`:
|
139 |
|
140 |
* `f` is of type `Callable[[A], B]`
|
141 |
+
* `fa` is of type `Wrapper[A]`
|
142 |
* The return value is of type `Wrapper[B]`
|
143 |
|
144 |
Thus, in Python's type system, we can express the type signature of `fmap` as:
|
145 |
|
146 |
```python
|
147 |
+
fmap(f: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]:
|
148 |
```
|
149 |
|
150 |
Essentially, `fmap`:
|
|
|
166 |
## The List Wrapper
|
167 |
|
168 |
We can define a `List` class to represent a wrapped list that supports `fmap`:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
"""
|
170 |
)
|
171 |
return
|
172 |
|
173 |
|
174 |
@app.cell
|
175 |
+
def _(B, Callable, Functor, dataclass):
|
176 |
@dataclass
|
177 |
+
class List[A](Functor):
|
178 |
value: list[A]
|
179 |
|
180 |
@classmethod
|
181 |
+
def fmap(cls, f: Callable[[A], B], fa: "List[A]") -> "List[B]":
|
182 |
+
return List([f(x) for x in fa.value])
|
183 |
+
return (List,)
|
184 |
|
185 |
|
186 |
+
@app.cell(hide_code=True)
|
187 |
+
def _(mo):
|
188 |
+
mo.md(r"""> Try with List below""")
|
189 |
+
return
|
190 |
+
|
191 |
+
|
192 |
+
@app.cell
|
193 |
+
def _(List, pp):
|
194 |
flist = List([1, 2, 3, 4])
|
195 |
pp(List.fmap(lambda x: x + 1, flist))
|
196 |
pp(List.fmap(lambda x: [x], flist))
|
197 |
+
return (flist,)
|
198 |
|
199 |
|
200 |
@app.cell(hide_code=True)
|
|
|
206 |
The type signature of `fmap` for `List` is:
|
207 |
|
208 |
```python
|
209 |
+
fmap(f: Callable[[A], B], fa: List[A]) -> List[B]
|
210 |
```
|
211 |
|
212 |
Similarly, for `Wrapper`:
|
213 |
|
214 |
```python
|
215 |
+
fmap(f: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]
|
216 |
```
|
217 |
|
218 |
Both follow the same pattern, which we can generalize as:
|
219 |
|
220 |
```python
|
221 |
+
fmap(f: Callable[[A], B], fa: Functor[A]) -> Functor[B]
|
222 |
```
|
223 |
|
224 |
where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
|
|
|
257 |
|
258 |
```python
|
259 |
from dataclasses import dataclass
|
260 |
+
from typing import Callable, TypeVar
|
261 |
from abc import ABC, abstractmethod
|
262 |
|
263 |
A = TypeVar("A")
|
264 |
B = TypeVar("B")
|
265 |
|
266 |
@dataclass
|
267 |
+
class Functor[A](ABC):
|
268 |
@classmethod
|
269 |
@abstractmethod
|
270 |
+
def fmap(f: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
|
271 |
raise NotImplementedError
|
272 |
```
|
273 |
|
|
|
300 |
|
301 |
```python
|
302 |
from dataclasses import dataclass
|
303 |
+
from typing import Callable, TypeVar
|
304 |
|
305 |
A = TypeVar("A")
|
306 |
B = TypeVar("B")
|
307 |
|
308 |
@dataclass
|
309 |
+
class RoseTree[A](Functor):
|
310 |
+
|
311 |
value: A
|
312 |
children: list["RoseTree[A]"]
|
313 |
|
314 |
@classmethod
|
315 |
+
def fmap(cls, f: Callable[[A], B], fa: "RoseTree[A]") -> "RoseTree[B]":
|
316 |
return RoseTree(
|
317 |
+
f(fa.value), [cls.fmap(f, child) for child in fa.children]
|
318 |
)
|
319 |
|
320 |
def __repr__(self) -> str:
|
|
|
332 |
|
333 |
|
334 |
@app.cell(hide_code=True)
|
335 |
+
def _(B, Callable, Functor, dataclass, mo):
|
336 |
@dataclass
|
337 |
+
class RoseTree[A](Functor):
|
338 |
"""
|
339 |
### Doc: RoseTree
|
340 |
|
|
|
347 |
|
348 |
**Methods:**
|
349 |
|
350 |
+
- `fmap(f: Callable[[A], B], fa: "RoseTree[A]") -> "RoseTree[B]"`
|
351 |
|
352 |
Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
|
353 |
|
|
|
362 |
children: list["RoseTree[A]"]
|
363 |
|
364 |
@classmethod
|
365 |
+
def fmap(cls, f: Callable[[A], B], fa: "RoseTree[A]") -> "RoseTree[B]":
|
366 |
return RoseTree(
|
367 |
+
f(fa.value), [cls.fmap(f, child) for child in fa.children]
|
368 |
)
|
369 |
|
370 |
def __repr__(self) -> str:
|
|
|
528 |
|
529 |
```python
|
530 |
@dataclass
|
531 |
+
class EvilFunctor[A](Functor):
|
532 |
value: list[A]
|
533 |
|
534 |
@classmethod
|
535 |
+
def fmap(cls, f: Callable[[A], B], fa: "EvilFunctor[A]") -> "EvilFunctor[B]":
|
536 |
return (
|
537 |
+
cls([fa.value[0]] * 2 + list(map(f, fa.value[1:])))
|
538 |
+
if fa.value
|
539 |
else []
|
540 |
)
|
541 |
```
|
|
|
545 |
|
546 |
|
547 |
@app.cell
|
548 |
+
def _(B, Callable, Functor, check_functor_law, dataclass, pp):
|
549 |
@dataclass
|
550 |
+
class EvilFunctor[A](Functor):
|
551 |
value: list[A]
|
552 |
|
553 |
@classmethod
|
554 |
def fmap(
|
555 |
+
cls, f: Callable[[A], B], fa: "EvilFunctor[A]"
|
556 |
) -> "EvilFunctor[B]":
|
557 |
return (
|
558 |
+
cls([fa.value[0]] * 2 + [f(x) for x in fa.value[1:]])
|
559 |
+
if fa.value
|
560 |
else []
|
561 |
)
|
562 |
|
|
|
576 |
```Python
|
577 |
@classmethod
|
578 |
@abstractmethod
|
579 |
+
def fmap(cls, f: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
|
580 |
return NotImplementedError
|
581 |
|
582 |
@classmethod
|
583 |
+
def const_fmap(cls, fa: "Functor[A]", b: B) -> "Functor[B]":
|
584 |
+
return cls.fmap(lambda _: b, fa)
|
585 |
|
586 |
@classmethod
|
587 |
+
def void(cls, fa: "Functor[A]") -> "Functor[None]":
|
588 |
+
return cls.const_fmap(fa, None)
|
589 |
```
|
590 |
"""
|
591 |
)
|
|
|
593 |
|
594 |
|
595 |
@app.cell(hide_code=True)
|
596 |
+
def _(ABC, B, Callable, abstractmethod, dataclass, mo):
|
597 |
@dataclass
|
598 |
+
class Functor[A](ABC):
|
599 |
"""
|
600 |
### Doc: Functor
|
601 |
|
|
|
603 |
|
604 |
**Methods:**
|
605 |
|
606 |
+
- `fmap(f: Callable[[A], B], fa: Functor[A]) -> Functor[B]`
|
607 |
Abstract method to apply a function to all values inside a functor.
|
608 |
|
609 |
+
- `const_fmap(fa: "Functor[A]", b: B) -> Functor[B]`
|
610 |
Replaces all values inside a functor with a constant `b`, preserving the original structure.
|
611 |
|
612 |
+
- `void(fa: "Functor[A]") -> Functor[None]`
|
613 |
+
Equivalent to `const_fmap(fa, None)`, transforming all values in a functor into `None`.
|
614 |
"""
|
615 |
|
616 |
@classmethod
|
617 |
@abstractmethod
|
618 |
+
def fmap(cls, f: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
|
619 |
return NotImplementedError
|
620 |
|
621 |
@classmethod
|
622 |
+
def const_fmap(cls, fa: "Functor[A]", b: B) -> "Functor[B]":
|
623 |
+
return cls.fmap(lambda _: b, fa)
|
624 |
|
625 |
@classmethod
|
626 |
+
def void(cls, fa: "Functor[A]") -> "Functor[None]":
|
627 |
+
return cls.const_fmap(fa, None)
|
628 |
|
629 |
|
630 |
mo.md(Functor.__doc__)
|
|
|
661 |
One example is the **`Maybe`** type from Haskell, which is used to represent computations that can either result in a value or no value (`Nothing`).
|
662 |
|
663 |
We can define the `Maybe` functor as below:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
664 |
"""
|
665 |
)
|
666 |
return
|
667 |
|
668 |
|
669 |
@app.cell
|
670 |
+
def _(B, Callable, Functor, dataclass):
|
671 |
@dataclass
|
672 |
+
class Maybe[A](Functor):
|
673 |
value: None | A
|
674 |
|
675 |
@classmethod
|
676 |
+
def fmap(cls, f: Callable[[A], B], fa: "Maybe[A]") -> "Maybe[B]":
|
677 |
+
return cls(None) if fa.value is None else cls(f(fa.value))
|
678 |
|
679 |
def __repr__(self):
|
680 |
return "Nothing" if self.value is None else repr(self.value)
|
|
|
779 |
Remember that we defined the `id` and `compose` function above as:
|
780 |
|
781 |
```Python
|
782 |
+
def id(x: A) -> A:
|
783 |
return x
|
784 |
|
785 |
def compose(f: Callable[[B], C], g: Callable[[A], B]) -> Callable[[A], C]:
|
|
|
857 |
|
858 |
```Python
|
859 |
@dataclass
|
860 |
+
class Functor[A](ABC)
|
861 |
```
|
862 |
|
863 |
And RoseTree:
|
864 |
|
865 |
```Python
|
866 |
@dataclass
|
867 |
+
class RoseTree[A](Functor)
|
868 |
```
|
869 |
|
870 |
**Here's the key part:** the _type constructor_ `RoseTree` takes any type `T` to a new type, `RoseTree[T]`. Also, `fmap` restricted to `RoseTree` types takes a function `Callable[[A], B]` to a function `Callable[[RoseTree[A]], RoseTree[B]]`.
|
|
|
941 |
|
942 |
```Python
|
943 |
@dataclass
|
944 |
+
class Wrapper[A](Functor):
|
945 |
value: A
|
946 |
|
947 |
@classmethod
|
|
|
1027 |
### Category of List Concatenation
|
1028 |
|
1029 |
First, let’s define the category of list concatenation:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1030 |
"""
|
1031 |
)
|
1032 |
return
|
1033 |
|
1034 |
|
1035 |
@app.cell
|
1036 |
+
def _(A, dataclass):
|
1037 |
@dataclass
|
1038 |
+
class ListConcatenation[A]:
|
1039 |
value: list[A]
|
1040 |
|
1041 |
@staticmethod
|
|
|
1068 |
### Category of Integer Addition
|
1069 |
|
1070 |
Now, let's define the category of integer addition:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1071 |
"""
|
1072 |
)
|
1073 |
return
|
|
|
1230 |
@app.cell(hide_code=True)
|
1231 |
def _():
|
1232 |
from dataclasses import dataclass
|
1233 |
+
from typing import Callable, TypeVar
|
1234 |
from pprint import pp
|
1235 |
+
return Callable, TypeVar, dataclass, pp
|
1236 |
|
1237 |
|
1238 |
@app.cell(hide_code=True)
|
functional_programming/CHANGELOG.md
CHANGED
@@ -1,5 +1,25 @@
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
## 2025-03-11
|
4 |
|
5 |
* Demo version of notebook `05_functors.py`
|
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
3 |
+
## 2025-04-02
|
4 |
+
|
5 |
+
+ Migrate to `python3.13` for `05_functors`
|
6 |
+
|
7 |
+
+ Replace all occurrences of
|
8 |
+
|
9 |
+
```python
|
10 |
+
class Functor(Generic[A])
|
11 |
+
```
|
12 |
+
|
13 |
+
with
|
14 |
+
|
15 |
+
```python
|
16 |
+
class Functor[A]
|
17 |
+
```
|
18 |
+
|
19 |
+
for conciseness
|
20 |
+
|
21 |
+
+ Use `fa` in function signatures instead of `a` when `fa` is a *Functor*
|
22 |
+
|
23 |
## 2025-03-11
|
24 |
|
25 |
* Demo version of notebook `05_functors.py`
|