metaboulie commited on
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 CHANGED
@@ -7,8 +7,11 @@
7
 
8
  import marimo
9
 
10
- __generated_with = "0.12.0"
11
- app = marimo.App(app_title="Category Theory and Functors")
 
 
 
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.2 | last modified: 2025-04-02 | author: [métaboulie](https://github.com/metaboulie)<br/>
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 Callable, TypeVar
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, 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
 
@@ -137,14 +156,14 @@ def _(mo):
137
  """
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`:
@@ -163,7 +182,7 @@ def _(mo):
163
  def _(mo):
164
  mo.md(
165
  """
166
- ## The List Wrapper
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, f: Callable[[A], B], fa: "List[A]") -> "List[B]":
182
- return List([f(x) for x in fa.value])
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(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.
@@ -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(f: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
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
- ## Case Study: RoseTree
287
 
288
- A **RoseTree** is a tree where:
289
 
290
- - Each node holds a **value**.
291
- - Each node has a **list of child nodes** (which are also RoseTrees).
292
 
293
- This structure is useful for representing hierarchical data, such as:
 
 
294
 
295
- - Abstract Syntax Trees (ASTs)
296
- - File system directories
297
- - Recursive computations
 
298
 
299
- We can implement `RoseTree` by extending the `Functor` class:
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:
321
- return f"Node: {self.value}, Children: {self.children}"
322
- ```
323
 
324
- - The function is applied **recursively** to each node's value.
325
- - The tree structure **remains unchanged**.
326
- - Only the values inside the tree are modified.
 
 
327
 
328
- > Try using `RoseTree` in the cell below.
 
 
329
  """
330
  )
331
  return
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
-
341
- A Functor implementation of `RoseTree`, allowing transformation of values while preserving the tree structure.
342
 
343
- **Attributes**
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
- - `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
 
354
- **Implementation logic:**
 
 
355
 
356
- - The function `f` is applied to the root node's `value`.
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
- value: A
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:
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(func: Callable[[A], B]) -> Callable[[Functor[A]], Functor[B]]
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
- We can implement a similar idea in Python:
416
-
417
- ```python
418
- fmap = lambda f, functor: functor.__class__.fmap(f, functor)
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
- You can now apply `inc` to any functor like `Wrapper`, `List`, or `RoseTree`:
 
 
 
430
 
431
- ```python
432
- # Applying `inc` to a Wrapper
433
- wrapper = Wrapper(5)
434
- inc(wrapper) # Wrapper(value=6)
435
 
436
- # Applying `inc` to a List
437
- list_wrapper = List([1, 2, 3])
438
- inc(list_wrapper) # List(value=[2, 3, 4])
 
 
 
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
- > Try using `fmap` in the cell below.
 
 
 
 
 
 
446
  """
447
  )
448
  return
449
 
450
 
451
- @app.cell
452
- def _(flist, pp, rosetree, wrapper):
453
- fmap = lambda f, functor: functor.__class__.fmap(f, functor)
454
- inc = lambda functor: fmap(lambda x: x + 1, functor)
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 [second law](https://github.com/quchen/articles/blob/master/second_functor_law.mo) as well.
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
- ```Python
494
- check_functor_law = lambda functor: repr(fmap(id, functor)) == repr(functor)
495
- ```
 
 
496
 
497
- We can verify the functor we've defined.
498
  """
499
  )
500
  return
@@ -507,12 +513,26 @@ def _():
507
  return compose, id
508
 
509
 
 
 
 
 
 
 
510
  @app.cell
511
- def _(fmap, id):
512
- check_functor_law = lambda functor: repr(fmap(id, functor)) == repr(functor)
 
 
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, 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
 
563
 
 
 
564
  pp(check_functor_law(EvilFunctor([1, 2, 3, 4])))
565
- return (EvilFunctor,)
566
 
567
 
568
- @app.cell(hide_code=True)
569
  def _(mo):
570
  mo.md(
571
- """
572
- ## Final definition of Functor
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
- - `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__)
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.const_fmap(rosetree, "λ"))
643
  pp(RoseTree.void(rosetree))
644
- pp(List.const_fmap(flist, "λ"))
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, 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)
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
- @app.cell
699
- def _(Maybe, pp):
700
- mint = Maybe(1)
701
- mnone = Maybe(None)
702
 
703
- pp(Maybe.fmap(lambda x: x + 1, mint))
704
- pp(Maybe.fmap(lambda x: x + 1, mnone))
705
- return mint, mnone
 
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 `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`.
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.: ${\displaystyle F(\operatorname {id} _{A})=\operatorname {id} _{F(A)}}$
896
- 2. Functors must distribute over morphism composition, i.e. ${\displaystyle F(f\circ g)=F(f)\circ F(g)}$
897
  """
898
  )
899
  return
@@ -903,22 +818,22 @@ def _(mo):
903
  def _(mo):
904
  mo.md(
905
  """
906
- Remember that we defined the `fmap`, `id` and `compose` as
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
- Let's prove that `fmap` is a functor.
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 f, functor: functor.fmap(f, functor)
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, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
949
- return Wrapper(f(a.value))
950
  ```
951
  """
952
  )
@@ -1009,8 +924,8 @@ def _(A, B, C, Callable, Wrapper, dataclass):
1009
 
1010
 
1011
  @app.cell
1012
- def _(WrapperCategory, fmap, id, pp, wrapper):
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 `func`, but also includes transformations between other mathematical structures.
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
- #### 1. **Identity Law**:
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
- ```python
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
- #### 2. **Composition Law**:
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
- ```python
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
- - [Haskellwiki. Category Theory](https://en.wikibooks.org/wiki/Haskell/Category_theory)
1203
- - [Haskellforall. The Category Design Pattern](https://www.haskellforall.com/2012/08/the-category-design-pattern.html)
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. Functor](https://wiki.haskell.org/index.php?title=Functor)
1211
- - [Haskellwiki. Typeclassopedia#Functor](https://wiki.haskell.org/index.php?title=Typeclassopedia#Functor)
1212
- - [Haskellwiki. Typeclassopedia#Category](https://wiki.haskell.org/index.php?title=Typeclassopedia#Category)
 
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
- + Migrate to `python3.13` for `05_functors`
 
 
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