Spaces:
Running
Running
Commit
·
3c4c5bc
1
Parent(s):
7139673
refactor(functors): `v0.1.3` of `functors` notebook
Browse files* restructure the notebook
* replace `f` in the function signatures with `g` to indicate regular functions and distinguish from functors
- functional_programming/05_functors.py +222 -314
- functional_programming/CHANGELOG.md +10 -1
functional_programming/05_functors.py
CHANGED
@@ -7,8 +7,11 @@
|
|
7 |
|
8 |
import marimo
|
9 |
|
10 |
-
__generated_with = "0.12.
|
11 |
-
app = marimo.App(
|
|
|
|
|
|
|
12 |
|
13 |
|
14 |
@app.cell(hide_code=True)
|
@@ -37,7 +40,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,7 +80,7 @@ def _(mo):
|
|
77 |
|
78 |
```python
|
79 |
from dataclasses import dataclass
|
80 |
-
from typing import
|
81 |
|
82 |
A = TypeVar("A")
|
83 |
B = TypeVar("B")
|
@@ -96,9 +99,6 @@ def _(mo):
|
|
96 |
### Mapping Functions Over Wrapped Data
|
97 |
|
98 |
To modify wrapped data while keeping it wrapped, we define an `fmap` method:
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
"""
|
103 |
)
|
104 |
return
|
@@ -111,14 +111,33 @@ def _(B, Callable, Functor, dataclass):
|
|
111 |
value: A
|
112 |
|
113 |
@classmethod
|
114 |
-
def fmap(cls,
|
115 |
-
return Wrapper(
|
116 |
return (Wrapper,)
|
117 |
|
118 |
|
119 |
@app.cell(hide_code=True)
|
120 |
def _(mo):
|
121 |
-
mo.md(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
return
|
123 |
|
124 |
|
@@ -137,14 +156,14 @@ def _(mo):
|
|
137 |
"""
|
138 |
We can analyze the type signature of `fmap` for `Wrapper`:
|
139 |
|
140 |
-
* `
|
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(
|
148 |
```
|
149 |
|
150 |
Essentially, `fmap`:
|
@@ -163,7 +182,7 @@ def _(mo):
|
|
163 |
def _(mo):
|
164 |
mo.md(
|
165 |
"""
|
166 |
-
## The List
|
167 |
|
168 |
We can define a `List` class to represent a wrapped list that supports `fmap`:
|
169 |
"""
|
@@ -178,8 +197,8 @@ def _(B, Callable, Functor, dataclass):
|
|
178 |
value: list[A]
|
179 |
|
180 |
@classmethod
|
181 |
-
def fmap(cls,
|
182 |
-
return List([
|
183 |
return (List,)
|
184 |
|
185 |
|
@@ -206,19 +225,19 @@ def _(mo):
|
|
206 |
The type signature of `fmap` for `List` is:
|
207 |
|
208 |
```python
|
209 |
-
fmap(
|
210 |
```
|
211 |
|
212 |
Similarly, for `Wrapper`:
|
213 |
|
214 |
```python
|
215 |
-
fmap(
|
216 |
```
|
217 |
|
218 |
Both follow the same pattern, which we can generalize as:
|
219 |
|
220 |
```python
|
221 |
-
fmap(
|
222 |
```
|
223 |
|
224 |
where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
|
@@ -256,24 +275,15 @@ def _(mo):
|
|
256 |
To define `Functor` in Python, we use an abstract base class:
|
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(
|
271 |
raise NotImplementedError
|
272 |
```
|
273 |
|
274 |
We can now extend custom wrappers, containers, or computation contexts with this `Functor` base class, implement the `fmap` method, and apply any function.
|
275 |
-
|
276 |
-
Next, let's implement a more complex data structure: [RoseTree](https://en.wikipedia.org/wiki/Rose_tree).
|
277 |
"""
|
278 |
)
|
279 |
return
|
@@ -282,96 +292,106 @@ def _(mo):
|
|
282 |
@app.cell(hide_code=True)
|
283 |
def _(mo):
|
284 |
mo.md(
|
285 |
-
"""
|
286 |
-
##
|
287 |
|
288 |
-
|
289 |
|
290 |
-
-
|
291 |
-
-
|
292 |
|
293 |
-
|
|
|
|
|
294 |
|
295 |
-
|
296 |
-
|
297 |
-
|
|
|
298 |
|
299 |
-
We can implement `RoseTree` by extending the `Functor` class:
|
300 |
|
301 |
-
|
302 |
-
|
303 |
-
|
|
|
|
|
304 |
|
305 |
-
|
306 |
-
B
|
|
|
307 |
|
308 |
-
|
309 |
-
|
|
|
310 |
|
311 |
-
value: A
|
312 |
-
children: list["RoseTree[A]"]
|
313 |
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
|
320 |
-
def __repr__(self) -> str:
|
321 |
-
return f"Node: {self.value}, Children: {self.children}"
|
322 |
-
```
|
323 |
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
327 |
|
328 |
-
|
|
|
|
|
329 |
"""
|
330 |
)
|
331 |
return
|
332 |
|
333 |
|
334 |
@app.cell(hide_code=True)
|
335 |
-
def _(
|
336 |
-
|
337 |
-
class RoseTree[A](Functor):
|
338 |
"""
|
339 |
-
###
|
340 |
-
|
341 |
-
A Functor implementation of `RoseTree`, allowing transformation of values while preserving the tree structure.
|
342 |
|
343 |
-
**
|
344 |
-
|
345 |
-
- `value (A)`: The value stored in the node.
|
346 |
-
- `children (list[RoseTree[A]])`: A list of child nodes forming the tree structure.
|
347 |
-
|
348 |
-
**Methods:**
|
349 |
|
350 |
-
-
|
|
|
351 |
|
352 |
-
|
353 |
|
354 |
-
|
|
|
|
|
355 |
|
356 |
-
|
357 |
-
- Each child in `children` recursively calls `fmap`, ensuring all values in the tree are mapped.
|
358 |
-
- The overall tree structure remains unchanged.
|
359 |
"""
|
|
|
|
|
360 |
|
361 |
-
|
362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
|
364 |
@classmethod
|
365 |
-
def fmap(cls,
|
|
|
|
|
|
|
|
|
|
|
|
|
366 |
return RoseTree(
|
367 |
-
|
368 |
)
|
369 |
|
370 |
def __repr__(self) -> str:
|
371 |
return f"Node: {self.value}, Children: {self.children}"
|
372 |
-
|
373 |
-
|
374 |
-
mo.md(RoseTree.__doc__)
|
375 |
return (RoseTree,)
|
376 |
|
377 |
|
@@ -402,7 +422,7 @@ def _(mo):
|
|
402 |
Translating to Python, we get:
|
403 |
|
404 |
```python
|
405 |
-
def fmap(
|
406 |
```
|
407 |
|
408 |
This means that `fmap`:
|
@@ -412,51 +432,42 @@ def _(mo):
|
|
412 |
- Takes a **functor** of type `Functor[A]` as input.
|
413 |
- Outputs a **functor** of type `Functor[B]`.
|
414 |
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
inc = lambda functor: fmap(lambda x: x + 1, functor)
|
420 |
-
```
|
421 |
-
|
422 |
-
- **`fmap`**: Lifts an ordinary function (`f`) to the functor world, allowing the function to operate on the wrapped value inside the functor.
|
423 |
-
- **`inc`**: A specific instance of `fmap` that operates on any functor. It takes a functor, applies the function `lambda x: x + 1` to every value inside it, and returns a new functor with the updated values.
|
424 |
-
|
425 |
-
Thus, **`fmap`** transforms an ordinary function into a **function that operates on functors**, and **`inc`** is a specific case where it increments the value inside the functor.
|
426 |
|
427 |
-
### Applying the `inc` Function to Various Functors
|
428 |
|
429 |
-
|
|
|
|
|
|
|
430 |
|
431 |
-
```python
|
432 |
-
# Applying `inc` to a Wrapper
|
433 |
-
wrapper = Wrapper(5)
|
434 |
-
inc(wrapper) # Wrapper(value=6)
|
435 |
|
436 |
-
|
437 |
-
|
438 |
-
|
|
|
|
|
|
|
439 |
|
440 |
-
# Applying `inc` to a RoseTree
|
441 |
-
tree = RoseTree(1, [RoseTree(2, []), RoseTree(3, [])])
|
442 |
-
inc(tree) # RoseTree(value=2, children=[RoseTree(value=3, children=[]), RoseTree(value=4, children=[])])
|
443 |
-
```
|
444 |
|
445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
446 |
"""
|
447 |
)
|
448 |
return
|
449 |
|
450 |
|
451 |
-
@app.cell
|
452 |
-
def _(
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
pp(inc(wrapper))
|
457 |
-
pp(inc(flist))
|
458 |
-
pp(inc(rosetree))
|
459 |
-
return fmap, inc
|
460 |
|
461 |
|
462 |
@app.cell(hide_code=True)
|
@@ -476,25 +487,20 @@ def _(mo):
|
|
476 |
2. `fmap` should also preserve **function composition**. Applying two composed functions `g` and `h` to a functor via `fmap` should give the same result as first applying `fmap` to `g` and then applying `fmap` to `h`.
|
477 |
|
478 |
/// admonition |
|
479 |
-
- Any `Functor` instance satisfying the first law `(fmap id = id)` will automatically satisfy the
|
480 |
///
|
|
|
|
|
|
|
481 |
|
482 |
-
### Functor Law Verification
|
483 |
-
|
484 |
-
We can define `id` and `compose` in `Python` as below:
|
485 |
-
|
486 |
-
```python
|
487 |
-
id = lambda x: x
|
488 |
-
compose = lambda f, g: lambda x: f(g(x))
|
489 |
-
```
|
490 |
-
|
491 |
-
We can add a helper function `check_functor_law` to verify that an instance satisfies the functor laws.
|
492 |
|
493 |
-
|
494 |
-
|
495 |
-
|
|
|
|
|
496 |
|
497 |
-
We can
|
498 |
"""
|
499 |
)
|
500 |
return
|
@@ -507,12 +513,26 @@ def _():
|
|
507 |
return compose, id
|
508 |
|
509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
510 |
@app.cell
|
511 |
-
def _(
|
512 |
-
check_functor_law = lambda functor: repr(fmap(id, functor)) == repr(
|
|
|
|
|
513 |
return (check_functor_law,)
|
514 |
|
515 |
|
|
|
|
|
|
|
|
|
|
|
|
|
516 |
@app.cell
|
517 |
def _(check_functor_law, flist, pp, rosetree, wrapper):
|
518 |
for functor in (wrapper, flist, rosetree):
|
@@ -522,187 +542,82 @@ def _(check_functor_law, flist, pp, rosetree, wrapper):
|
|
522 |
|
523 |
@app.cell(hide_code=True)
|
524 |
def _(mo):
|
525 |
-
mo.md(
|
526 |
-
"""
|
527 |
-
And here is an `EvilFunctor`. We can verify it's not a valid `Functor`.
|
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 |
-
```
|
542 |
-
"""
|
543 |
-
)
|
544 |
return
|
545 |
|
546 |
|
547 |
@app.cell
|
548 |
-
def _(B, Callable, Functor,
|
549 |
@dataclass
|
550 |
class EvilFunctor[A](Functor):
|
551 |
value: list[A]
|
552 |
|
553 |
@classmethod
|
554 |
def fmap(
|
555 |
-
cls,
|
556 |
) -> "EvilFunctor[B]":
|
557 |
return (
|
558 |
-
cls([fa.value[0]] * 2 + [
|
559 |
if fa.value
|
560 |
else []
|
561 |
)
|
|
|
562 |
|
563 |
|
|
|
|
|
564 |
pp(check_functor_law(EvilFunctor([1, 2, 3, 4])))
|
565 |
-
return
|
566 |
|
567 |
|
568 |
-
@app.cell
|
569 |
def _(mo):
|
570 |
mo.md(
|
571 |
-
"""
|
572 |
-
##
|
573 |
-
|
574 |
-
We can now draft the final definition of `Functor` with some utility functions.
|
575 |
-
|
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 |
-
)
|
592 |
-
return
|
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 |
-
|
602 |
-
A generic interface for types that support mapping over their values.
|
603 |
-
|
604 |
-
**Methods:**
|
605 |
|
606 |
-
- `
|
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 `
|
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__)
|
631 |
-
return (Functor,)
|
632 |
-
|
633 |
-
|
634 |
-
@app.cell(hide_code=True)
|
635 |
-
def _(mo):
|
636 |
-
mo.md("""> Try with utility functions in the cell below""")
|
637 |
return
|
638 |
|
639 |
|
640 |
@app.cell
|
641 |
def _(List, RoseTree, flist, pp, rosetree):
|
642 |
-
pp(RoseTree.
|
643 |
pp(RoseTree.void(rosetree))
|
644 |
-
pp(List.
|
645 |
pp(List.void(flist))
|
646 |
return
|
647 |
|
648 |
|
649 |
@app.cell(hide_code=True)
|
650 |
def _(mo):
|
651 |
-
mo.md(
|
652 |
-
"""
|
653 |
-
## Functors for Non-Iterable Types
|
654 |
-
|
655 |
-
In the previous examples, we implemented functors for **iterables**, like `List` and `RoseTree`, which are inherently **iterable types**. This is a natural fit for functors, as iterables can be mapped over.
|
656 |
-
|
657 |
-
However, **functors are not limited to iterables**. There are cases where we want to apply the concept of functors to types that are not inherently iterable, such as types that represent optional values, computations, or other data structures.
|
658 |
-
|
659 |
-
### The Maybe Functor
|
660 |
-
|
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,
|
671 |
@dataclass
|
672 |
-
class
|
673 |
-
value: None | A
|
674 |
-
|
675 |
@classmethod
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
def __repr__(self):
|
680 |
-
return "Nothing" if self.value is None else repr(self.value)
|
681 |
-
return (Maybe,)
|
682 |
-
|
683 |
-
|
684 |
-
@app.cell(hide_code=True)
|
685 |
-
def _(mo):
|
686 |
-
mo.md(
|
687 |
-
"""
|
688 |
-
**`Maybe`** is a functor that can either hold a value or be `Nothing` (equivalent to `None` in Python). The `fmap` method applies a function to the value inside the functor, if it exists. If the value is `None` (representing `Nothing`), `fmap` simply returns `None`.
|
689 |
-
|
690 |
-
By using `Maybe` as a functor, we gain the ability to apply transformations (`fmap`) to potentially absent values, without having to explicitly handle the `None` case every time.
|
691 |
-
|
692 |
-
> Try using `Maybe` in the cell below.
|
693 |
-
"""
|
694 |
-
)
|
695 |
-
return
|
696 |
-
|
697 |
|
698 |
-
@
|
699 |
-
def
|
700 |
-
|
701 |
-
mnone = Maybe(None)
|
702 |
|
703 |
-
|
704 |
-
|
705 |
-
|
|
|
706 |
|
707 |
|
708 |
@app.cell(hide_code=True)
|
@@ -851,7 +766,7 @@ def _(mo):
|
|
851 |
|
852 |
Remember that a functor has two parts: it maps objects in one category to objects in another and morphisms in the first category to morphisms in the second.
|
853 |
|
854 |
-
Functors in Python are from `Py` to `
|
855 |
|
856 |
Recall the definition of `Functor`:
|
857 |
|
@@ -892,8 +807,8 @@ def _(mo):
|
|
892 |
|
893 |
Once again there are a few axioms that functors have to obey.
|
894 |
|
895 |
-
1. Given an identity morphism $id_A$ on an object $A$, $F ( id_A )$ must be the identity morphism on $F ( A )$, i.e.: $
|
896 |
-
2. Functors must distribute over morphism composition, i.e. $
|
897 |
"""
|
898 |
)
|
899 |
return
|
@@ -903,22 +818,22 @@ def _(mo):
|
|
903 |
def _(mo):
|
904 |
mo.md(
|
905 |
"""
|
906 |
-
Remember that we defined the `
|
907 |
```python
|
908 |
-
fmap = lambda f, functor: functor.__class__.fmap(f, functor)
|
909 |
id = lambda x: x
|
910 |
compose = lambda f, g: lambda x: f(g(x))
|
911 |
```
|
912 |
|
913 |
-
|
914 |
|
915 |
-
First, let's define a `Category` for a specific `Functor`. We choose to define the `Category` for the `Wrapper` as `WrapperCategory` here for simplicity, but remember that `Wrapper` can be any `Functor`(i.e. `List`, `RoseTree`, `Maybe` and more):
|
916 |
-
|
917 |
-
**Notice that** in this case, we can actually view `fmap` as:
|
918 |
```python
|
919 |
-
fmap = lambda
|
920 |
```
|
921 |
|
|
|
|
|
|
|
|
|
922 |
We define `WrapperCategory` as:
|
923 |
|
924 |
```python
|
@@ -945,8 +860,8 @@ def _(mo):
|
|
945 |
value: A
|
946 |
|
947 |
@classmethod
|
948 |
-
def fmap(cls,
|
949 |
-
return Wrapper(
|
950 |
```
|
951 |
"""
|
952 |
)
|
@@ -1009,8 +924,8 @@ def _(A, B, C, Callable, Wrapper, dataclass):
|
|
1009 |
|
1010 |
|
1011 |
@app.cell
|
1012 |
-
def _(WrapperCategory,
|
1013 |
-
pp(fmap(id, wrapper) == WrapperCategory.id(wrapper))
|
1014 |
return
|
1015 |
|
1016 |
|
@@ -1020,7 +935,7 @@ def _(mo):
|
|
1020 |
"""
|
1021 |
## Length as a Functor
|
1022 |
|
1023 |
-
Remember that a functor is a transformation between two categories. It is not only limited to a functor from `Py` to `
|
1024 |
|
1025 |
Let’s prove that **`length`** can be viewed as a functor. Specifically, we will demonstrate that `length` is a functor from the **category of list concatenation** to the **category of integer addition**.
|
1026 |
|
@@ -1136,18 +1051,20 @@ def _(mo):
|
|
1136 |
|
1137 |
Now, let’s verify that `length` satisfies the two functor laws.
|
1138 |
|
1139 |
-
|
1140 |
-
The identity law states that applying the functor to the identity element of one category should give the identity element of the other category.
|
1141 |
|
1142 |
-
|
1143 |
-
> length(ListConcatenation.id()) == IntAddition.id()
|
1144 |
-
True
|
1145 |
-
```
|
1146 |
"""
|
1147 |
)
|
1148 |
return
|
1149 |
|
1150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1151 |
@app.cell(hide_code=True)
|
1152 |
def _(mo):
|
1153 |
mo.md("""This ensures that the length of an empty list (identity in the `ListConcatenation` category) is `0` (identity in the `IntAddition` category).""")
|
@@ -1158,31 +1075,16 @@ def _(mo):
|
|
1158 |
def _(mo):
|
1159 |
mo.md(
|
1160 |
"""
|
1161 |
-
|
1162 |
-
The composition law states that the functor should preserve composition. Applying the functor to a composed element should be the same as composing the functor applied to the individual elements.
|
1163 |
|
1164 |
-
|
1165 |
-
> lista = ListConcatenation([1, 2])
|
1166 |
-
> listb = ListConcatenation([3, 4])
|
1167 |
-
> length(ListConcatenation.compose(lista, listb)) == IntAddition.compose(
|
1168 |
-
> length(lista), length(listb)
|
1169 |
-
> )
|
1170 |
-
True
|
1171 |
-
```
|
1172 |
"""
|
1173 |
)
|
1174 |
return
|
1175 |
|
1176 |
|
1177 |
-
@app.cell(hide_code=True)
|
1178 |
-
def _(mo):
|
1179 |
-
mo.md("""This ensures that the length of the concatenation of two lists is the same as the sum of the lengths of the individual lists.""")
|
1180 |
-
return
|
1181 |
-
|
1182 |
-
|
1183 |
@app.cell
|
1184 |
def _(IntAddition, ListConcatenation, length, pp):
|
1185 |
-
pp(length(ListConcatenation.id()) == IntAddition.id())
|
1186 |
lista = ListConcatenation([1, 2])
|
1187 |
listb = ListConcatenation([3, 4])
|
1188 |
pp(
|
@@ -1192,6 +1094,12 @@ def _(IntAddition, ListConcatenation, length, pp):
|
|
1192 |
return lista, listb
|
1193 |
|
1194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1195 |
@app.cell(hide_code=True)
|
1196 |
def _(mo):
|
1197 |
mo.md(
|
@@ -1199,17 +1107,17 @@ def _(mo):
|
|
1199 |
# Further reading
|
1200 |
|
1201 |
- [The Trivial Monad](http://blog.sigfpe.com/2007/04/trivial-monad.html)
|
1202 |
-
- [
|
1203 |
-
- [Haskellforall
|
1204 |
-
- [Haskellforall. The Functor Design Pattern](https://www.haskellforall.com/2012/09/the-functor-design-pattern.html)
|
1205 |
|
1206 |
/// attention | ATTENTION
|
1207 |
The functor design pattern doesn't work at all if you aren't using categories in the first place. This is why you should structure your tools using the compositional category design pattern so that you can take advantage of functors to easily mix your tools together.
|
1208 |
///
|
1209 |
|
1210 |
-
- [Haskellwiki
|
1211 |
-
- [Haskellwiki
|
1212 |
-
- [Haskellwiki
|
|
|
1213 |
"""
|
1214 |
)
|
1215 |
return
|
|
|
7 |
|
8 |
import marimo
|
9 |
|
10 |
+
__generated_with = "0.12.5"
|
11 |
+
app = marimo.App(
|
12 |
+
app_title="Category Theory and Functors",
|
13 |
+
css_file="/Users/chanhuizhihou/Library/Application Support/mtheme/themes/gruvbox.css",
|
14 |
+
)
|
15 |
|
16 |
|
17 |
@app.cell(hide_code=True)
|
|
|
40 |
/// details | Notebook metadata
|
41 |
type: info
|
42 |
|
43 |
+
version: 0.1.3 | last modified: 2025-04-08 | author: [métaboulie](https://github.com/metaboulie)<br/>
|
44 |
reviewer: [Haleshot](https://github.com/Haleshot)
|
45 |
|
46 |
///
|
|
|
80 |
|
81 |
```python
|
82 |
from dataclasses import dataclass
|
83 |
+
from typing import TypeVar
|
84 |
|
85 |
A = TypeVar("A")
|
86 |
B = TypeVar("B")
|
|
|
99 |
### Mapping Functions Over Wrapped Data
|
100 |
|
101 |
To modify wrapped data while keeping it wrapped, we define an `fmap` method:
|
|
|
|
|
|
|
102 |
"""
|
103 |
)
|
104 |
return
|
|
|
111 |
value: A
|
112 |
|
113 |
@classmethod
|
114 |
+
def fmap(cls, g: Callable[[A], B], fa: "Wrapper[A]") -> "Wrapper[B]":
|
115 |
+
return Wrapper(g(fa.value))
|
116 |
return (Wrapper,)
|
117 |
|
118 |
|
119 |
@app.cell(hide_code=True)
|
120 |
def _(mo):
|
121 |
+
mo.md(
|
122 |
+
r"""
|
123 |
+
/// attention
|
124 |
+
|
125 |
+
To distinguish between regular types and functors, we use the prefix `f` to indicate `Functor`.
|
126 |
+
|
127 |
+
For instance,
|
128 |
+
|
129 |
+
- `a: A` is a regular variable of type `A`
|
130 |
+
- `g: Callable[[A], B]` is a regular function from type `A` to `B`
|
131 |
+
- `fa: Functor[A]` is a *Functor* wrapping a value of type `A`
|
132 |
+
- `fg: Functor[Callable[[A], B]]` is a *Functor* wrapping a function from type `A` to `B`
|
133 |
+
|
134 |
+
and we will avoid using `f` to represent a function
|
135 |
+
|
136 |
+
///
|
137 |
+
|
138 |
+
> Try with Wrapper below
|
139 |
+
"""
|
140 |
+
)
|
141 |
return
|
142 |
|
143 |
|
|
|
156 |
"""
|
157 |
We can analyze the type signature of `fmap` for `Wrapper`:
|
158 |
|
159 |
+
* `g` is of type `Callable[[A], B]`
|
160 |
* `fa` is of type `Wrapper[A]`
|
161 |
* The return value is of type `Wrapper[B]`
|
162 |
|
163 |
Thus, in Python's type system, we can express the type signature of `fmap` as:
|
164 |
|
165 |
```python
|
166 |
+
fmap(g: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]:
|
167 |
```
|
168 |
|
169 |
Essentially, `fmap`:
|
|
|
182 |
def _(mo):
|
183 |
mo.md(
|
184 |
"""
|
185 |
+
## The List Functor
|
186 |
|
187 |
We can define a `List` class to represent a wrapped list that supports `fmap`:
|
188 |
"""
|
|
|
197 |
value: list[A]
|
198 |
|
199 |
@classmethod
|
200 |
+
def fmap(cls, g: Callable[[A], B], fa: "List[A]") -> "List[B]":
|
201 |
+
return List([g(x) for x in fa.value])
|
202 |
return (List,)
|
203 |
|
204 |
|
|
|
225 |
The type signature of `fmap` for `List` is:
|
226 |
|
227 |
```python
|
228 |
+
fmap(g: Callable[[A], B], fa: List[A]) -> List[B]
|
229 |
```
|
230 |
|
231 |
Similarly, for `Wrapper`:
|
232 |
|
233 |
```python
|
234 |
+
fmap(g: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]
|
235 |
```
|
236 |
|
237 |
Both follow the same pattern, which we can generalize as:
|
238 |
|
239 |
```python
|
240 |
+
fmap(g: Callable[[A], B], fa: Functor[A]) -> Functor[B]
|
241 |
```
|
242 |
|
243 |
where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
|
|
|
275 |
To define `Functor` in Python, we use an abstract base class:
|
276 |
|
277 |
```python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
@dataclass
|
279 |
class Functor[A](ABC):
|
280 |
@classmethod
|
281 |
@abstractmethod
|
282 |
+
def fmap(g: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
|
283 |
raise NotImplementedError
|
284 |
```
|
285 |
|
286 |
We can now extend custom wrappers, containers, or computation contexts with this `Functor` base class, implement the `fmap` method, and apply any function.
|
|
|
|
|
287 |
"""
|
288 |
)
|
289 |
return
|
|
|
292 |
@app.cell(hide_code=True)
|
293 |
def _(mo):
|
294 |
mo.md(
|
295 |
+
r"""
|
296 |
+
## The Maybe Functor
|
297 |
|
298 |
+
**`Maybe`** is a functor that can either hold a value (`Just(value)`) or be `Nothing` (equivalent to `None` in Python).
|
299 |
|
300 |
+
- It the value exists, `fmap` applies the function to this value inside the functor.
|
301 |
+
- If the value is `None`, `fmap` simply returns `None`.
|
302 |
|
303 |
+
/// admonition
|
304 |
+
By using `Maybe` as a functor, we gain the ability to apply transformations (`fmap`) to potentially absent values, without having to explicitly handle the `None` case every time.
|
305 |
+
///
|
306 |
|
307 |
+
We can implement the `Maybe` functor as:
|
308 |
+
"""
|
309 |
+
)
|
310 |
+
return
|
311 |
|
|
|
312 |
|
313 |
+
@app.cell
|
314 |
+
def _(B, Callable, Functor, dataclass):
|
315 |
+
@dataclass
|
316 |
+
class Maybe[A](Functor):
|
317 |
+
value: None | A
|
318 |
|
319 |
+
@classmethod
|
320 |
+
def fmap(cls, g: Callable[[A], B], fa: "Maybe[A]") -> "Maybe[B]":
|
321 |
+
return cls(None) if fa.value is None else cls(g(fa.value))
|
322 |
|
323 |
+
def __repr__(self):
|
324 |
+
return "Nothing" if self.value is None else f"Just({self.value!r})"
|
325 |
+
return (Maybe,)
|
326 |
|
|
|
|
|
327 |
|
328 |
+
@app.cell
|
329 |
+
def _(Maybe, pp):
|
330 |
+
pp(Maybe.fmap(lambda x: x + 1, Maybe(1)))
|
331 |
+
pp(Maybe.fmap(lambda x: x + 1, Maybe(None)))
|
332 |
+
return
|
333 |
|
|
|
|
|
|
|
334 |
|
335 |
+
@app.cell(hide_code=True)
|
336 |
+
def _(mo):
|
337 |
+
mo.md(
|
338 |
+
r"""
|
339 |
+
## More Functor instances (optional)
|
340 |
|
341 |
+
In this section, we will explore more *Functor* instances to help you build up a better comprehension.
|
342 |
+
|
343 |
+
The main reference is [Data.Functor](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Functor.html)
|
344 |
"""
|
345 |
)
|
346 |
return
|
347 |
|
348 |
|
349 |
@app.cell(hide_code=True)
|
350 |
+
def _(mo):
|
351 |
+
mo.md(
|
|
|
352 |
"""
|
353 |
+
### The [RoseTree](https://en.wikipedia.org/wiki/Rose_tree) Functor
|
|
|
|
|
354 |
|
355 |
+
A **RoseTree** is a tree where:
|
|
|
|
|
|
|
|
|
|
|
356 |
|
357 |
+
- Each node holds a **value**.
|
358 |
+
- Each node has a **list of child nodes** (which are also RoseTrees).
|
359 |
|
360 |
+
This structure is useful for representing hierarchical data, such as:
|
361 |
|
362 |
+
- Abstract Syntax Trees (ASTs)
|
363 |
+
- File system directories
|
364 |
+
- Recursive computations
|
365 |
|
366 |
+
The implementation is:
|
|
|
|
|
367 |
"""
|
368 |
+
)
|
369 |
+
return
|
370 |
|
371 |
+
|
372 |
+
@app.cell
|
373 |
+
def _(B, Callable, Functor, dataclass):
|
374 |
+
@dataclass
|
375 |
+
class RoseTree[A](Functor):
|
376 |
+
value: A # The value stored in the node.
|
377 |
+
children: list[
|
378 |
+
"RoseTree[A]"
|
379 |
+
] # A list of child nodes forming the tree structure.
|
380 |
|
381 |
@classmethod
|
382 |
+
def fmap(cls, g: Callable[[A], B], fa: "RoseTree[A]") -> "RoseTree[B]":
|
383 |
+
"""
|
384 |
+
Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
|
385 |
+
|
386 |
+
1. `g` is applied to the root node's `value`.
|
387 |
+
2. Each child in `children` recursively calls `fmap`.
|
388 |
+
"""
|
389 |
return RoseTree(
|
390 |
+
g(fa.value), [cls.fmap(g, child) for child in fa.children]
|
391 |
)
|
392 |
|
393 |
def __repr__(self) -> str:
|
394 |
return f"Node: {self.value}, Children: {self.children}"
|
|
|
|
|
|
|
395 |
return (RoseTree,)
|
396 |
|
397 |
|
|
|
422 |
Translating to Python, we get:
|
423 |
|
424 |
```python
|
425 |
+
def fmap(g: Callable[[A], B]) -> Callable[[Functor[A]], Functor[B]]
|
426 |
```
|
427 |
|
428 |
This means that `fmap`:
|
|
|
432 |
- Takes a **functor** of type `Functor[A]` as input.
|
433 |
- Outputs a **functor** of type `Functor[B]`.
|
434 |
|
435 |
+
Inspired by this, we can implement an `inc` function which takes a functor, applies the function `lambda x: x + 1` to every value inside it, and returns a new functor with the updated values.
|
436 |
+
"""
|
437 |
+
)
|
438 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
|
|
|
440 |
|
441 |
+
@app.cell
|
442 |
+
def _():
|
443 |
+
inc = lambda functor: functor.fmap(lambda x: x + 1, functor)
|
444 |
+
return (inc,)
|
445 |
|
|
|
|
|
|
|
|
|
446 |
|
447 |
+
@app.cell
|
448 |
+
def _(flist, inc, pp, rosetree, wrapper):
|
449 |
+
pp(inc(wrapper))
|
450 |
+
pp(inc(flist))
|
451 |
+
pp(inc(rosetree))
|
452 |
+
return
|
453 |
|
|
|
|
|
|
|
|
|
454 |
|
455 |
+
@app.cell(hide_code=True)
|
456 |
+
def _(mo):
|
457 |
+
mo.md(
|
458 |
+
r"""
|
459 |
+
/// admonition | exercise
|
460 |
+
Implement other generic functions and apply them to different *Functor* instances.
|
461 |
+
///
|
462 |
"""
|
463 |
)
|
464 |
return
|
465 |
|
466 |
|
467 |
+
@app.cell(hide_code=True)
|
468 |
+
def _(mo):
|
469 |
+
mo.md(r"""# Functor laws and utility functions""")
|
470 |
+
return
|
|
|
|
|
|
|
|
|
|
|
471 |
|
472 |
|
473 |
@app.cell(hide_code=True)
|
|
|
487 |
2. `fmap` should also preserve **function composition**. Applying two composed functions `g` and `h` to a functor via `fmap` should give the same result as first applying `fmap` to `g` and then applying `fmap` to `h`.
|
488 |
|
489 |
/// admonition |
|
490 |
+
- Any `Functor` instance satisfying the first law `(fmap id = id)` will [automatically satisfy the second law](https://github.com/quchen/articles/blob/master/second_functor_law.md) as well.
|
491 |
///
|
492 |
+
"""
|
493 |
+
)
|
494 |
+
return
|
495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
496 |
|
497 |
+
@app.cell(hide_code=True)
|
498 |
+
def _(mo):
|
499 |
+
mo.md(
|
500 |
+
r"""
|
501 |
+
### Functor Law Verification
|
502 |
|
503 |
+
We can define `id` and `compose` in `Python` as:
|
504 |
"""
|
505 |
)
|
506 |
return
|
|
|
513 |
return compose, id
|
514 |
|
515 |
|
516 |
+
@app.cell(hide_code=True)
|
517 |
+
def _(mo):
|
518 |
+
mo.md(r"""We can add a helper function `check_functor_law` to verify that an instance satisfies the functor laws:""")
|
519 |
+
return
|
520 |
+
|
521 |
+
|
522 |
@app.cell
|
523 |
+
def _(id):
|
524 |
+
check_functor_law = lambda functor: repr(functor.fmap(id, functor)) == repr(
|
525 |
+
functor
|
526 |
+
)
|
527 |
return (check_functor_law,)
|
528 |
|
529 |
|
530 |
+
@app.cell(hide_code=True)
|
531 |
+
def _(mo):
|
532 |
+
mo.md(r"""We can verify the functor we've defined:""")
|
533 |
+
return
|
534 |
+
|
535 |
+
|
536 |
@app.cell
|
537 |
def _(check_functor_law, flist, pp, rosetree, wrapper):
|
538 |
for functor in (wrapper, flist, rosetree):
|
|
|
542 |
|
543 |
@app.cell(hide_code=True)
|
544 |
def _(mo):
|
545 |
+
mo.md("""And here is an `EvilFunctor`. We can verify it's not a valid `Functor`.""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
546 |
return
|
547 |
|
548 |
|
549 |
@app.cell
|
550 |
+
def _(B, Callable, Functor, dataclass):
|
551 |
@dataclass
|
552 |
class EvilFunctor[A](Functor):
|
553 |
value: list[A]
|
554 |
|
555 |
@classmethod
|
556 |
def fmap(
|
557 |
+
cls, g: Callable[[A], B], fa: "EvilFunctor[A]"
|
558 |
) -> "EvilFunctor[B]":
|
559 |
return (
|
560 |
+
cls([fa.value[0]] * 2 + [g(x) for x in fa.value[1:]])
|
561 |
if fa.value
|
562 |
else []
|
563 |
)
|
564 |
+
return (EvilFunctor,)
|
565 |
|
566 |
|
567 |
+
@app.cell
|
568 |
+
def _(EvilFunctor, check_functor_law, pp):
|
569 |
pp(check_functor_law(EvilFunctor([1, 2, 3, 4])))
|
570 |
+
return
|
571 |
|
572 |
|
573 |
+
@app.cell
|
574 |
def _(mo):
|
575 |
mo.md(
|
576 |
+
r"""
|
577 |
+
## Utility functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
578 |
|
579 |
+
- `const(fa: "Functor[A]", b: B) -> Functor[B]`
|
|
|
|
|
|
|
580 |
Replaces all values inside a functor with a constant `b`, preserving the original structure.
|
581 |
|
582 |
- `void(fa: "Functor[A]") -> Functor[None]`
|
583 |
+
Equivalent to `const(fa, None)`, transforming all values in a functor into `None`.
|
584 |
"""
|
585 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
586 |
return
|
587 |
|
588 |
|
589 |
@app.cell
|
590 |
def _(List, RoseTree, flist, pp, rosetree):
|
591 |
+
pp(RoseTree.const(rosetree, "λ"))
|
592 |
pp(RoseTree.void(rosetree))
|
593 |
+
pp(List.const(flist, "λ"))
|
594 |
pp(List.void(flist))
|
595 |
return
|
596 |
|
597 |
|
598 |
@app.cell(hide_code=True)
|
599 |
def _(mo):
|
600 |
+
mo.md("""# Formal implementation of Functor""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
601 |
return
|
602 |
|
603 |
|
604 |
@app.cell
|
605 |
+
def _(ABC, B, Callable, abstractmethod, dataclass):
|
606 |
@dataclass
|
607 |
+
class Functor[A](ABC):
|
|
|
|
|
608 |
@classmethod
|
609 |
+
@abstractmethod
|
610 |
+
def fmap(cls, g: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
|
611 |
+
return NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
612 |
|
613 |
+
@classmethod
|
614 |
+
def const(cls, fa: "Functor[A]", b: B) -> "Functor[B]":
|
615 |
+
return cls.fmap(lambda _: b, fa)
|
|
|
616 |
|
617 |
+
@classmethod
|
618 |
+
def void(cls, fa: "Functor[A]") -> "Functor[None]":
|
619 |
+
return cls.const(fa, None)
|
620 |
+
return (Functor,)
|
621 |
|
622 |
|
623 |
@app.cell(hide_code=True)
|
|
|
766 |
|
767 |
Remember that a functor has two parts: it maps objects in one category to objects in another and morphisms in the first category to morphisms in the second.
|
768 |
|
769 |
+
Functors in Python are from `Py` to `Func`, where `Func` is the subcategory of `Py` defined on just that functor's types. E.g. the RoseTree functor goes from `Py` to `RoseTree`, where `RoseTree` is the category containing only RoseTree types, that is, `RoseTree[T]` for any type `T`. The morphisms in `RoseTree` are functions defined on RoseTree types, that is, functions `Callable[[RoseTree[T]], RoseTree[U]]` for types `T`, `U`.
|
770 |
|
771 |
Recall the definition of `Functor`:
|
772 |
|
|
|
807 |
|
808 |
Once again there are a few axioms that functors have to obey.
|
809 |
|
810 |
+
1. Given an identity morphism $id_A$ on an object $A$, $F ( id_A )$ must be the identity morphism on $F ( A )$, i.e.: $F({id} _{A})={id} _{F(A)}$
|
811 |
+
2. Functors must distribute over morphism composition, i.e. $F(f\circ g)=F(f)\circ F(g)$
|
812 |
"""
|
813 |
)
|
814 |
return
|
|
|
818 |
def _(mo):
|
819 |
mo.md(
|
820 |
"""
|
821 |
+
Remember that we defined the `id` and `compose` as
|
822 |
```python
|
|
|
823 |
id = lambda x: x
|
824 |
compose = lambda f, g: lambda x: f(g(x))
|
825 |
```
|
826 |
|
827 |
+
We can define `fmap` as:
|
828 |
|
|
|
|
|
|
|
829 |
```python
|
830 |
+
fmap = lambda g, functor: functor.fmap(g, functor)
|
831 |
```
|
832 |
|
833 |
+
Let's prove that `fmap` is a functor.
|
834 |
+
|
835 |
+
First, let's define a `Category` for a specific `Functor`. We choose to define the `Category` for the `Wrapper` as `WrapperCategory` here for simplicity, but remember that `Wrapper` can be any `Functor`(i.e. `List`, `RoseTree`, `Maybe` and more):
|
836 |
+
|
837 |
We define `WrapperCategory` as:
|
838 |
|
839 |
```python
|
|
|
860 |
value: A
|
861 |
|
862 |
@classmethod
|
863 |
+
def fmap(cls, g: Callable[[A], B], fa: "Wrapper[A]") -> "Wrapper[B]":
|
864 |
+
return Wrapper(g(fa.value))
|
865 |
```
|
866 |
"""
|
867 |
)
|
|
|
924 |
|
925 |
|
926 |
@app.cell
|
927 |
+
def _(WrapperCategory, id, pp, wrapper):
|
928 |
+
pp(wrapper.fmap(id, wrapper) == WrapperCategory.id(wrapper))
|
929 |
return
|
930 |
|
931 |
|
|
|
935 |
"""
|
936 |
## Length as a Functor
|
937 |
|
938 |
+
Remember that a functor is a transformation between two categories. It is not only limited to a functor from `Py` to `Func`, but also includes transformations between other mathematical structures.
|
939 |
|
940 |
Let’s prove that **`length`** can be viewed as a functor. Specifically, we will demonstrate that `length` is a functor from the **category of list concatenation** to the **category of integer addition**.
|
941 |
|
|
|
1051 |
|
1052 |
Now, let’s verify that `length` satisfies the two functor laws.
|
1053 |
|
1054 |
+
**Identity Law**
|
|
|
1055 |
|
1056 |
+
The identity law states that applying the functor to the identity element of one category should give the identity element of the other category.
|
|
|
|
|
|
|
1057 |
"""
|
1058 |
)
|
1059 |
return
|
1060 |
|
1061 |
|
1062 |
+
@app.cell
|
1063 |
+
def _(IntAddition, ListConcatenation, length, pp):
|
1064 |
+
pp(length(ListConcatenation.id()) == IntAddition.id())
|
1065 |
+
return
|
1066 |
+
|
1067 |
+
|
1068 |
@app.cell(hide_code=True)
|
1069 |
def _(mo):
|
1070 |
mo.md("""This ensures that the length of an empty list (identity in the `ListConcatenation` category) is `0` (identity in the `IntAddition` category).""")
|
|
|
1075 |
def _(mo):
|
1076 |
mo.md(
|
1077 |
"""
|
1078 |
+
**Composition Law**
|
|
|
1079 |
|
1080 |
+
The composition law states that the functor should preserve composition. Applying the functor to a composed element should be the same as composing the functor applied to the individual elements.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1081 |
"""
|
1082 |
)
|
1083 |
return
|
1084 |
|
1085 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1086 |
@app.cell
|
1087 |
def _(IntAddition, ListConcatenation, length, pp):
|
|
|
1088 |
lista = ListConcatenation([1, 2])
|
1089 |
listb = ListConcatenation([3, 4])
|
1090 |
pp(
|
|
|
1094 |
return lista, listb
|
1095 |
|
1096 |
|
1097 |
+
@app.cell(hide_code=True)
|
1098 |
+
def _(mo):
|
1099 |
+
mo.md("""This ensures that the length of the concatenation of two lists is the same as the sum of the lengths of the individual lists.""")
|
1100 |
+
return
|
1101 |
+
|
1102 |
+
|
1103 |
@app.cell(hide_code=True)
|
1104 |
def _(mo):
|
1105 |
mo.md(
|
|
|
1107 |
# Further reading
|
1108 |
|
1109 |
- [The Trivial Monad](http://blog.sigfpe.com/2007/04/trivial-monad.html)
|
1110 |
+
- [Haskellforall: The Category Design Pattern](https://www.haskellforall.com/2012/08/the-category-design-pattern.html)
|
1111 |
+
- [Haskellforall: The Functor Design Pattern](https://www.haskellforall.com/2012/09/the-functor-design-pattern.html)
|
|
|
1112 |
|
1113 |
/// attention | ATTENTION
|
1114 |
The functor design pattern doesn't work at all if you aren't using categories in the first place. This is why you should structure your tools using the compositional category design pattern so that you can take advantage of functors to easily mix your tools together.
|
1115 |
///
|
1116 |
|
1117 |
+
- [Haskellwiki: Functor](https://wiki.haskell.org/index.php?title=Functor)
|
1118 |
+
- [Haskellwiki: Typeclassopedia#Functor](https://wiki.haskell.org/index.php?title=Typeclassopedia#Functor)
|
1119 |
+
- [Haskellwiki: Typeclassopedia#Category](https://wiki.haskell.org/index.php?title=Typeclassopedia#Category)
|
1120 |
+
- [Haskellwiki: Category Theory](https://en.wikibooks.org/wiki/Haskell/Category_theory)
|
1121 |
"""
|
1122 |
)
|
1123 |
return
|
functional_programming/CHANGELOG.md
CHANGED
@@ -1,8 +1,17 @@
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
## 2025-04-02
|
4 |
|
5 |
-
|
|
|
|
|
6 |
|
7 |
+ Replace all occurrences of
|
8 |
|
|
|
1 |
# Changelog of the functional-programming course
|
2 |
|
3 |
+
## 2025-04-08
|
4 |
+
|
5 |
+
**functors.py**
|
6 |
+
|
7 |
+
* restructure the notebook
|
8 |
+
* replace `f` in the function signatures with `g` to indicate regular functions and distinguish from functors
|
9 |
+
|
10 |
## 2025-04-02
|
11 |
|
12 |
+
**functors.py**
|
13 |
+
|
14 |
+
+ Migrate to `python3.13`
|
15 |
|
16 |
+ Replace all occurrences of
|
17 |
|