metaboulie commited on
Commit
d417768
·
1 Parent(s): 0aa6802

refactor(fp): v0.1.1 of notebook 05_functors.py

Browse files
functional_programming/05_functors.py CHANGED
@@ -8,7 +8,7 @@
8
  import marimo
9
 
10
  __generated_with = "0.11.17"
11
- app = marimo.App(app_title="Category Theory and Functors", css_file="")
12
 
13
 
14
  @app.cell(hide_code=True)
@@ -37,7 +37,7 @@ def _(mo):
37
  /// details | Notebook metadata
38
  type: info
39
 
40
- version: 0.1.0 | last modified: 2025-03-13 | author: [métaboulie](https://github.com/metaboulie)<br/>
41
  reviewer: [Haleshot](https://github.com/Haleshot)
42
 
43
  ///
@@ -79,12 +79,12 @@ def _(mo):
79
  from dataclasses import dataclass
80
  from typing import Callable, Generic, TypeVar
81
 
82
- a = TypeVar("a")
83
- b = TypeVar("b")
84
 
85
  @dataclass
86
- class Wrapper(Generic[a]):
87
- value: a
88
  ```
89
 
90
  Now, we can create an instance of wrapped data:
@@ -99,20 +99,21 @@ def _(mo):
99
 
100
  ```python
101
  @dataclass
102
- class Wrapper(Generic[a]):
103
- value: a
104
 
105
- def fmap(self, func: Callable[[a], b]) -> "Wrapper[b]":
106
- return Wrapper(func(self.value))
 
107
  ```
108
 
109
  Now, we can apply transformations without unwrapping:
110
 
111
  ```python
112
- >>> wrapped.fmap(lambda x: x + 1)
113
  Wrapper(value=2)
114
 
115
- >>> wrapped.fmap(lambda x: [x])
116
  Wrapper(value=[1])
117
  ```
118
 
@@ -123,19 +124,20 @@ def _(mo):
123
 
124
 
125
  @app.cell
126
- def _(Callable, Functor, Generic, a, b, dataclass):
127
  @dataclass
128
- class Wrapper(Functor, Generic[a]):
129
- value: a
130
 
131
- def fmap(self, func: Callable[[a], b]) -> "Wrapper[b]":
132
- return Wrapper(func(self.value))
133
-
134
- def __repr__(self):
135
- return repr(self.value)
136
 
137
 
138
  wrapper = Wrapper(1)
 
 
 
139
  return Wrapper, wrapper
140
 
141
 
@@ -145,21 +147,21 @@ def _(mo):
145
  """
146
  We can analyze the type signature of `fmap` for `Wrapper`:
147
 
148
- * `self` is of type `Wrapper[a]`
149
- * `func` is of type `Callable[[a], b]`
150
- * The return value is of type `Wrapper[b]`
151
 
152
  Thus, in Python's type system, we can express the type signature of `fmap` as:
153
 
154
  ```python
155
- def fmap(self: Wrapper[a], func: Callable[[a], b]) -> Wrapper[b]:
156
  ```
157
 
158
  Essentially, `fmap`:
159
 
160
- 1. Takes a `Wrapper[a]` instance and a function `Callable[[a], b]` as input.
161
  2. Applies the function to the value inside the wrapper.
162
- 3. Returns a new `Wrapper[b]` instance with the transformed value, leaving the original wrapper and its internal data unmodified.
163
 
164
  Now, let's examine `list` as a similar kind of wrapper.
165
  """
@@ -173,35 +175,47 @@ def _(mo):
173
  """
174
  ## The List Wrapper
175
 
176
- We can define a `ListWrapper` class to represent a wrapped list that supports `fmap`:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  """
178
  )
179
  return
180
 
181
 
182
  @app.cell
183
- def _(Callable, Functor, Generic, a, b, dataclass):
184
  @dataclass
185
- class ListWrapper(Functor, Generic[a]):
186
- value: list[a]
187
-
188
- def fmap(self, func: Callable[[a], b]) -> "ListWrapper[b]":
189
- return ListWrapper([func(x) for x in self.value])
190
-
191
- def __repr__(self):
192
- return repr(self.value)
193
 
 
 
 
194
 
195
- list_wrapper = ListWrapper([1, 2, 3, 4])
196
- return ListWrapper, list_wrapper
197
 
198
-
199
- @app.cell
200
- def _(ListWrapper, mo):
201
- with mo.redirect_stdout():
202
- print(ListWrapper(value=[2, 3, 4, 5]))
203
- print(ListWrapper(value=[[1], [2], [3], [4]]))
204
- return
205
 
206
 
207
  @app.cell(hide_code=True)
@@ -210,25 +224,25 @@ def _(mo):
210
  """
211
  ### Extracting the Type of `fmap`
212
 
213
- The type signature of `fmap` for `ListWrapper` is:
214
 
215
  ```python
216
- def fmap(self: ListWrapper[a], func: Callable[[a], b]) -> ListWrapper[b]
217
  ```
218
 
219
  Similarly, for `Wrapper`:
220
 
221
  ```python
222
- def fmap(self: Wrapper[a], func: Callable[[a], b]) -> Wrapper[b]
223
  ```
224
 
225
  Both follow the same pattern, which we can generalize as:
226
 
227
  ```python
228
- def fmap(self: Functor[a], func: Callable[[a], b]) -> Functor[b]
229
  ```
230
 
231
- where `Functor` can be `Wrapper`, `ListWrapper`, or any other wrapper type that follows the same structure.
232
 
233
  ### Functors in Haskell (optional)
234
 
@@ -267,13 +281,14 @@ def _(mo):
267
  from typing import Callable, Generic, TypeVar
268
  from abc import ABC, abstractmethod
269
 
270
- a = TypeVar("a")
271
- b = TypeVar("b")
272
 
273
  @dataclass
274
- class Functor(ABC, Generic[a]):
 
275
  @abstractmethod
276
- def fmap(self, func: Callable[[a], b]) -> "Functor[b]":
277
  raise NotImplementedError
278
  ```
279
 
@@ -308,21 +323,23 @@ def _(mo):
308
  from dataclasses import dataclass
309
  from typing import Callable, Generic, TypeVar
310
 
311
- a = TypeVar("a")
312
- b = TypeVar("b")
313
 
314
  @dataclass
315
  class RoseTree(Functor, Generic[a]):
316
- value: a
317
- children: list["RoseTree[a]"]
 
318
 
319
- def fmap(self, func: Callable[[a], b]) -> "RoseTree[b]":
 
320
  return RoseTree(
321
- func(self.value), [child.fmap(func) for child in self.children]
322
  )
323
 
324
  def __repr__(self) -> str:
325
- return f"RoseNode({self.value}, {self.children})"
326
  ```
327
 
328
  - The function is applied **recursively** to each node's value.
@@ -336,9 +353,9 @@ def _(mo):
336
 
337
 
338
  @app.cell(hide_code=True)
339
- def _(Callable, Functor, Generic, a, b, dataclass, mo):
340
  @dataclass
341
- class RoseTree(Functor, Generic[a]):
342
  """
343
  ### Doc: RoseTree
344
 
@@ -346,51 +363,47 @@ def _(Callable, Functor, Generic, a, b, dataclass, mo):
346
 
347
  **Attributes**
348
 
349
- - `value (a)`: The value stored in the node.
350
- - `children (list[RoseTree[a]])`: A list of child nodes forming the tree structure.
351
 
352
  **Methods:**
353
 
354
- - `fmap(func: Callable[[a], b]) -> RoseTree[b]`
355
- ```Python
356
- def fmap(RoseTree[a], (a -> b)) -> RoseTree[b]
357
- ```
358
- Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
359
 
360
  **Implementation logic:**
361
 
362
- - The function `func` is applied to the root node's `value`.
363
  - Each child in `children` recursively calls `fmap`, ensuring all values in the tree are mapped.
364
  - The overall tree structure remains unchanged.
365
-
366
- - `__repr__() -> str`: Returns a string representation of the node and its children.
367
  """
368
 
369
- value: a
370
- children: list["RoseTree[a]"]
371
 
372
- def fmap(self, func: Callable[[a], b]) -> "RoseTree[b]":
 
373
  return RoseTree(
374
- func(self.value), [child.fmap(func) for child in self.children]
375
  )
376
 
377
  def __repr__(self) -> str:
378
- return f"RoseNode({self.value}, {self.children})"
379
 
380
 
381
  mo.md(RoseTree.__doc__)
382
  return (RoseTree,)
383
 
384
 
385
- @app.cell(hide_code=True)
386
- def _(RoseTree, mo):
387
- ftree = RoseTree(1, [RoseTree(2, []), RoseTree(3, [RoseTree(4, [])])])
388
 
389
- with mo.redirect_stdout():
390
- print(ftree)
391
- print(ftree.fmap(lambda x: [x]))
392
- print(ftree.fmap(lambda x: RoseTree(x, [])))
393
- return (ftree,)
394
 
395
 
396
  @app.cell(hide_code=True)
@@ -410,43 +423,40 @@ def _(mo):
410
  Translating to Python, we get:
411
 
412
  ```python
413
- def fmap(func: Callable[[a], b]) -> Callable[[Functor[a]], Functor[b]]
414
  ```
415
 
416
  This means that `fmap`:
417
 
418
- - Takes an **ordinary function** `Callable[[a], b]` as input.
419
  - Outputs a function that:
420
- - Takes a **functor** of type `Functor[a]` as input.
421
- - Outputs a **functor** of type `Functor[b]`.
422
 
423
  We can implement a similar idea in Python:
424
 
425
  ```python
426
- # fmap(func: Callable[[a], b]) -> Callable[[Functor[a]], Functor[b]]
427
- fmap = lambda func: lambda f: f.fmap(lambda x: func(x))
428
-
429
- # inc([Functor[a]) -> Functor[b]
430
- inc = fmap(lambda x: x + 1)
431
  ```
432
 
433
- - **`fmap`**: Lifts an ordinary function (`lambda x: func(x)`) to the functor world, allowing the function to operate on the wrapped value inside the functor.
434
  - **`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.
435
 
436
  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.
437
 
438
  ### Applying the `inc` Function to Various Functors
439
 
440
- You can now apply `inc` to any functor like `Wrapper`, `ListWrapper`, or `RoseTree`:
441
 
442
  ```python
443
  # Applying `inc` to a Wrapper
444
  wrapper = Wrapper(5)
445
  inc(wrapper) # Wrapper(value=6)
446
 
447
- # Applying `inc` to a ListWrapper
448
- list_wrapper = ListWrapper([1, 2, 3])
449
- inc(list_wrapper) # ListWrapper(value=[2, 3, 4])
450
 
451
  # Applying `inc` to a RoseTree
452
  tree = RoseTree(1, [RoseTree(2, []), RoseTree(3, [])])
@@ -459,14 +469,14 @@ def _(mo):
459
  return
460
 
461
 
462
- @app.cell(hide_code=True)
463
- def _(ftree, list_wrapper, mo, wrapper):
464
- fmap = lambda func: lambda f: f.fmap(func)
465
- inc = fmap(lambda x: x + 1)
466
- with mo.redirect_stdout():
467
- print(inc(wrapper))
468
- print(inc(list_wrapper))
469
- print(inc(ftree))
470
  return fmap, inc
471
 
472
 
@@ -492,23 +502,17 @@ def _(mo):
492
 
493
  ### Functor Law Verification
494
 
495
- We can add a helper function `check_functor_law` in the `Functor` class to verify that an instance satisfies the functor laws.
496
 
497
- ```Python
498
  id = lambda x: x
 
 
499
 
500
- @dataclass
501
- class Functor(ABC, Generic[a]):
502
- @abstractmethod
503
- def fmap(self, func: Callable[[a], b]) -> "Functor[b]":
504
- return NotImplementedError
505
-
506
- def check_functor_law(self):
507
- return repr(self.fmap(id)) == repr(self)
508
 
509
- @abstractmethod
510
- def __repr__(self):
511
- return NotImplementedError
512
  ```
513
 
514
  We can verify the functor we've defined.
@@ -525,42 +529,61 @@ def _():
525
 
526
 
527
  @app.cell
528
- def _(ftree, list_wrapper, mo, wrapper):
529
- with mo.redirect_stdout():
530
- print(wrapper.check_functor_law())
531
- print(list_wrapper.check_functor_law())
532
- print(ftree.check_functor_law())
533
- return
 
 
 
 
534
 
535
 
536
  @app.cell(hide_code=True)
537
  def _(mo):
538
- mo.md("""And here is an `EvilFunctor`. We can verify it's not a valid `Functor`.""")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  return
540
 
541
 
542
  @app.cell
543
- def _(Callable, Functor, Generic, a, b, dataclass):
544
  @dataclass
545
- class EvilFunctor(Functor, Generic[a]):
546
- value: list[a]
547
 
548
- def fmap(self, func: Callable[[a], b]) -> "EvilFunctor[b]":
 
 
 
549
  return (
550
- EvilFunctor([self.value[0]] * 2 + list(map(func, self.value[1:])))
551
- if self.value
552
  else []
553
  )
554
 
555
- def __repr__(self):
556
- return repr(self.value)
557
- return (EvilFunctor,)
558
-
559
 
560
- @app.cell
561
- def _(EvilFunctor):
562
- EvilFunctor([1, 2, 3, 4]).check_functor_law()
563
- return
564
 
565
 
566
  @app.cell(hide_code=True)
@@ -572,24 +595,18 @@ def _(mo):
572
  We can now draft the final definition of `Functor` with some utility functions.
573
 
574
  ```Python
575
- @dataclass
576
- class Functor(ABC, Generic[a]):
577
  @abstractmethod
578
- def fmap(self, func: Callable[[a], b]) -> "Functor[b]":
579
  return NotImplementedError
580
 
581
- def check_functor_law(self) -> bool:
582
- return repr(self.fmap(id)) == repr(self)
583
-
584
- def const_fmap(self, b) -> "Functor[b]":
585
- return self.fmap(lambda _: b)
586
 
587
- def void(self) -> "Functor[None]":
588
- return self.const_fmap(None)
589
-
590
- @abstractmethod
591
- def __repr__(self):
592
- return NotImplementedError
593
  ```
594
  """
595
  )
@@ -597,9 +614,9 @@ def _(mo):
597
 
598
 
599
  @app.cell(hide_code=True)
600
- def _(ABC, Callable, Generic, a, abstractmethod, b, dataclass, id, mo):
601
  @dataclass
602
- class Functor(ABC, Generic[a]):
603
  """
604
  ### Doc: Functor
605
 
@@ -607,45 +624,28 @@ def _(ABC, Callable, Generic, a, abstractmethod, b, dataclass, id, mo):
607
 
608
  **Methods:**
609
 
610
- - `fmap(func: Callable[[a], b]) -> Functor[b]`
611
- Abstract method to apply a function `func` to transform the values inside the Functor.
612
-
613
- - `check_functor_law() -> bool`
614
- Verifies the identity law of functors: `fmap(id) == id`.
615
- This ensures that applying `fmap` with the identity function does not alter the structure.
616
 
617
- - `const_fmap(b) -> Functor[b]`
618
- Replaces all values inside the Functor with a constant `b`, preserving the original structure.
619
 
620
- - `void() -> Functor[None]`
621
- Equivalent to `const_fmap(None)`, transforming all values into `None`.
622
-
623
- - `__repr__()`
624
- Abstract method to define a string representation of the Functor.
625
-
626
- **Functor Laws:**
627
- A valid Functor implementation must satisfy:
628
-
629
- 1. **Identity Law:** `F.fmap(id) == F`
630
- 2. **Composition Law:** `F.fmap(f).fmap(g) == F.fmap(lambda x: g(f(x)))`
631
  """
632
 
 
633
  @abstractmethod
634
- def fmap(self, func: Callable[[a], b]) -> "Functor[b]":
635
  return NotImplementedError
636
 
637
- def check_functor_law(self) -> bool:
638
- return repr(self.fmap(id)) == repr(self)
 
639
 
640
- def const_fmap(self, b) -> "Functor[b]":
641
- return self.fmap(lambda _: b)
642
-
643
- def void(self) -> "Functor[None]":
644
- return self.const_fmap(None)
645
-
646
- @abstractmethod
647
- def __repr__(self):
648
- return NotImplementedError
649
 
650
 
651
  mo.md(Functor.__doc__)
@@ -658,13 +658,12 @@ def _(mo):
658
  return
659
 
660
 
661
- @app.cell(hide_code=True)
662
- def _(ftree, list_wrapper, mo):
663
- with mo.redirect_stdout():
664
- print(ftree.const_fmap("λ"))
665
- print(ftree.void())
666
- print(list_wrapper.const_fmap("λ"))
667
- print(list_wrapper.void())
668
  return
669
 
670
 
@@ -674,55 +673,55 @@ def _(mo):
674
  """
675
  ## Functors for Non-Iterable Types
676
 
677
- In the previous examples, we implemented functors for **iterables**, like `ListWrapper` and `RoseTree`, which are inherently **iterable types**. This is a natural fit for functors, as iterables can be mapped over.
678
 
679
  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.
680
 
681
  ### The Maybe Functor
682
 
683
- One example is the **`Maybe`** type from Haskell, which is used to represent computations that can either result in a value (`Just a`) or no value (`Nothing`).
684
 
685
  We can define the `Maybe` functor as below:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  """
687
  )
688
  return
689
 
690
 
691
  @app.cell
692
- def _(Callable, Functor, Generic, a, b, dataclass):
693
- @dataclass
694
- class Just(Generic[a]):
695
- value: a
696
-
697
- def __init__(self, value: a):
698
- # If the value is already a `Just`, we extract the value, else we wrap it
699
- self.value = value.value if isinstance(value, Just) else value
700
-
701
- def __repr__(self):
702
- return f"Just {self.value}"
703
-
704
-
705
  @dataclass
706
- class Maybe(Functor, Generic[a]):
707
- value: None | Just[a]
708
 
709
- def fmap(self, func: Callable[[a], b]) -> "Maybe[b]":
710
- # Apply the function to the value inside `Just`, or return `Nothing` if value is None
711
- return (
712
- Maybe(Just(func(self.value.value))) if self.value else Maybe(None)
713
- )
714
 
715
  def __repr__(self):
716
- return repr(self.value) if self.value else "Nothing"
717
- return Just, Maybe
718
 
719
 
720
  @app.cell(hide_code=True)
721
  def _(mo):
722
  mo.md(
723
  """
724
- - **`Just`** is a wrapper that holds a value. We use it to represent the presence of a value.
725
- - **`Maybe`** is a functor that can either hold a `Just` value or be `Nothing` (equivalent to `None` in Python). The `fmap` method applies a function to the value inside the `Just` wrapper, if it exists. If the value is `None` (representing `Nothing`), `fmap` simply returns `Nothing`.
726
 
727
  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.
728
 
@@ -733,23 +732,13 @@ def _(mo):
733
 
734
 
735
  @app.cell
736
- def _(Just, Maybe, ftree):
737
- mftree = Maybe(Just(ftree))
738
- mint = Maybe(Just(1))
739
  mnone = Maybe(None)
740
- return mftree, mint, mnone
741
-
742
 
743
- @app.cell(hide_code=True)
744
- def _(inc, mftree, mint, mnone, mo):
745
- with mo.redirect_stdout():
746
- print(mftree.check_functor_law())
747
- print(mint.check_functor_law())
748
- print(mnone.check_functor_law())
749
- print(mftree.fmap(inc))
750
- print(mint.fmap(lambda x: x + 1))
751
- print(mnone.fmap(lambda x: x + 1))
752
- return
753
 
754
 
755
  @app.cell(hide_code=True)
@@ -826,10 +815,10 @@ def _(mo):
826
  Remember that we defined the `id` and `compose` function above as:
827
 
828
  ```Python
829
- def id(x: Generic[a]) -> Generic[a]:
830
  return x
831
 
832
- def compose(f: Callable[[b], c], g: Callable[[a], b]) -> Callable[[a], c]:
833
  return lambda x: f(g(x))
834
  ```
835
 
@@ -880,7 +869,11 @@ def _(mo):
880
  - Maps any object $A$ in $C$ to $F ( A )$, in $D$.
881
  - Maps morphisms $f : A → B$ in $C$ to $F ( f ) : F ( A ) → F ( B )$ in $D$.
882
 
883
- > Endofunctors are functors from a category to itself.
 
 
 
 
884
  """
885
  )
886
  return
@@ -894,23 +887,23 @@ def _(mo):
894
 
895
  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.
896
 
897
- 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 `RoseTree[T] -> RoseTree[U]` for types `T`, `U`.
898
 
899
  Recall the definition of `Functor`:
900
 
901
  ```Python
902
  @dataclass
903
- class Functor(ABC, Generic[a])
904
  ```
905
 
906
  And RoseTree:
907
 
908
  ```Python
909
  @dataclass
910
- class RoseTree(Functor, Generic[a])
911
  ```
912
 
913
- **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 `a -> b` to a function `RoseTree[a] -> RoseTree[b]`.
914
 
915
  But that's it. We've defined two parts, something that takes objects in `Py` to objects in another category (that of `RoseTree` types and functions defined on `RoseTree` types), and something that takes morphisms in `Py` to morphisms in this category. So `RoseTree` is a functor.
916
 
@@ -946,50 +939,50 @@ def _(mo):
946
  def _(mo):
947
  mo.md(
948
  """
949
- Remember that we defined the `fmap` (not the `fmap` in `Functor` class) and `id` as
950
  ```python
951
- # fmap :: Callable[[a], b] -> Callable[[Functor[a]], Functor[b]]
952
- fmap = lambda func: lambda f: f.fmap(func)
953
  id = lambda x: x
954
  compose = lambda f, g: lambda x: f(g(x))
955
  ```
956
 
957
  Let's prove that `fmap` is a functor.
958
 
959
- 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. `ListWrapper`, `RoseTree`, `Maybe` and more):
960
 
961
  **Notice that** in this case, we can actually view `fmap` as:
962
  ```python
963
- # fmap :: Callable[[a], b] -> Callable[[Wrapper[a]], Wrapper[b]]
964
- fmap = lambda func: lambda wrapper: wrapper.fmap(func)
965
  ```
966
 
967
  We define `WrapperCategory` as:
968
 
969
  ```python
970
  @dataclass
971
- class WrapperCategory():
972
  @staticmethod
973
- def id() -> Callable[[Wrapper[a]], Wrapper[a]]:
974
- return lambda wrapper: Wrapper(wrapper.value)
975
 
976
  @staticmethod
977
  def compose(
978
- f: Callable[[Wrapper[b]], Wrapper[c]],
979
- g: Callable[[Wrapper[a]], Wrapper[b]],
980
- ) -> Callable[[Wrapper[a]], Wrapper[c]]:
981
- return lambda wrapper: f(g(Wrapper(wrapper.value)))
 
982
  ```
983
 
984
  And `Wrapper` is:
985
 
986
  ```Python
987
  @dataclass
988
- class Wrapper(Generic[a]):
989
- value: a
990
 
991
- def fmap(self, func: Callable[[a], b]) -> "Wrapper[b]":
992
- return Wrapper(func(self.value))
 
993
  ```
994
  """
995
  )
@@ -1000,36 +993,29 @@ def _(mo):
1000
  def _(mo):
1001
  mo.md(
1002
  """
1003
- notice that
1004
 
1005
  ```python
1006
- fmap(f)(wrapper) = wrapper.fmap(f)
 
 
 
 
1007
  ```
1008
-
1009
- We can get:
1010
-
1011
- ```python
1012
- fmap(id)
1013
- = lambda wrapper: wrapper.fmap(id)
1014
- = lambda wrapper: Wrapper(id(wrapper.value))
1015
- = lambda wrapper: Wrapper(wrapper.value)
1016
- = WrapperCategory.id()
1017
- ```
1018
- And:
1019
  ```python
1020
- fmap(compose(f, g))
1021
- = lambda wrapper: wrapper.fmap(compose(f, g))
1022
- = lambda wrapper: Wrapper(compose(f, g)(wrapper.value))
1023
- = lambda wrapper: Wrapper(f(g(wrapper.value)))
1024
-
1025
- WrapperCategory.compose(fmap(f), fmap(g))
1026
- = lambda wrapper: fmap(f)(fmap(g)(wrapper))
1027
- = lambda wrapper: fmap(f)(wrapper.fmap(g))
1028
- = lambda wrapper: fmap(f)(Wrapper(g(wrapper.value)))
1029
- = lambda wrapper: Wrapper(g(wrapper.value)).fmap(f)
1030
- = lambda wrapper: Wrapper(f(Wrapper(g(wrapper.value)).value))
1031
- = lambda wrapper: Wrapper(f(g(wrapper.value)))
1032
- = fmap(compose(f, g))
1033
  ```
1034
 
1035
  So our `Wrapper` is a valid `Functor`.
@@ -1040,33 +1026,27 @@ def _(mo):
1040
  return
1041
 
1042
 
1043
- @app.cell(hide_code=True)
1044
- def _(Callable, Wrapper, a, b, c, dataclass):
1045
  @dataclass
1046
  class WrapperCategory:
1047
  @staticmethod
1048
- def id() -> Callable[[Wrapper[a]], Wrapper[a]]:
1049
- return lambda wrapper: Wrapper(wrapper.value)
1050
 
1051
  @staticmethod
1052
  def compose(
1053
- f: Callable[[Wrapper[b]], Wrapper[c]],
1054
- g: Callable[[Wrapper[a]], Wrapper[b]],
1055
- ) -> Callable[[Wrapper[a]], Wrapper[c]]:
1056
- return lambda wrapper: f(g(Wrapper(wrapper.value)))
 
1057
  return (WrapperCategory,)
1058
 
1059
 
1060
- @app.cell(hide_code=True)
1061
- def _(WrapperCategory, compose, fmap, id, mo, wrapper):
1062
- with mo.redirect_stdout():
1063
- print(fmap(id)(wrapper) == id(wrapper))
1064
- print(
1065
- fmap(compose(lambda x: x + 1, lambda x: x * 2))(wrapper)
1066
- == WrapperCategory.compose(
1067
- fmap(lambda x: x + 1), fmap(lambda x: x * 2)
1068
- )(wrapper)
1069
- )
1070
  return
1071
 
1072
 
@@ -1083,24 +1063,40 @@ def _(mo):
1083
  ### Category of List Concatenation
1084
 
1085
  First, let’s define the category of list concatenation:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  """
1087
  )
1088
  return
1089
 
1090
 
1091
  @app.cell
1092
- def _(Generic, a, dataclass):
1093
  @dataclass
1094
- class ListConcatenation(Generic[a]):
1095
- value: list[a]
1096
 
1097
  @staticmethod
1098
- def id() -> "ListConcatenation[a]":
1099
  return ListConcatenation([])
1100
 
1101
  @staticmethod
1102
  def compose(
1103
- this: "ListConcatenation[a]", other: "ListConcatenation[a]"
1104
  ) -> "ListConcatenation[a]":
1105
  return ListConcatenation(this.value + other.value)
1106
  return (ListConcatenation,)
@@ -1124,6 +1120,20 @@ def _(mo):
1124
  ### Category of Integer Addition
1125
 
1126
  Now, let's define the category of integer addition:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
  """
1128
  )
1129
  return
@@ -1163,12 +1173,16 @@ def _(mo):
1163
  ### Defining the Length Functor
1164
 
1165
  We now define the `length` function as a functor, mapping from the category of list concatenation to the category of integer addition:
 
 
 
 
1166
  """
1167
  )
1168
  return
1169
 
1170
 
1171
- @app.cell
1172
  def _(IntAddition):
1173
  length = lambda l: IntAddition(len(l.value))
1174
  return (length,)
@@ -1190,17 +1204,16 @@ def _(mo):
1190
 
1191
  #### 1. **Identity Law**:
1192
  The identity law states that applying the functor to the identity element of one category should give the identity element of the other category.
 
 
 
 
 
1193
  """
1194
  )
1195
  return
1196
 
1197
 
1198
- @app.cell
1199
- def _(IntAddition, ListConcatenation, length):
1200
- length(ListConcatenation.id()) == IntAddition.id()
1201
- return
1202
-
1203
-
1204
  @app.cell(hide_code=True)
1205
  def _(mo):
1206
  mo.md("""This ensures that the length of an empty list (identity in the `ListConcatenation` category) is `0` (identity in the `IntAddition` category).""")
@@ -1213,22 +1226,16 @@ def _(mo):
1213
  """
1214
  #### 2. **Composition Law**:
1215
  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.
1216
- """
1217
- )
1218
- return
1219
-
1220
-
1221
- @app.cell
1222
- def _(ListConcatenation):
1223
- lista = ListConcatenation([1, 2])
1224
- listb = ListConcatenation([3, 4])
1225
- return lista, listb
1226
-
1227
 
1228
- @app.cell
1229
- def _(IntAddition, ListConcatenation, length, lista, listb):
1230
- length(ListConcatenation.compose(lista, listb)) == IntAddition.compose(
1231
- length(lista), length(listb)
 
 
 
 
 
1232
  )
1233
  return
1234
 
@@ -1239,6 +1246,18 @@ def _(mo):
1239
  return
1240
 
1241
 
 
 
 
 
 
 
 
 
 
 
 
 
1242
  @app.cell(hide_code=True)
1243
  def _(mo):
1244
  mo.md(
@@ -1278,15 +1297,16 @@ def _():
1278
  def _():
1279
  from dataclasses import dataclass
1280
  from typing import Callable, Generic, TypeVar
1281
- return Callable, Generic, TypeVar, dataclass
 
1282
 
1283
 
1284
  @app.cell(hide_code=True)
1285
  def _(TypeVar):
1286
- a = TypeVar("a")
1287
- b = TypeVar("b")
1288
- c = TypeVar("c")
1289
- return a, b, c
1290
 
1291
 
1292
  if __name__ == "__main__":
 
8
  import marimo
9
 
10
  __generated_with = "0.11.17"
11
+ app = marimo.App(app_title="Category Theory and Functors")
12
 
13
 
14
  @app.cell(hide_code=True)
 
37
  /// details | Notebook metadata
38
  type: info
39
 
40
+ version: 0.1.1 | last modified: 2025-03-16 | author: [métaboulie](https://github.com/metaboulie)<br/>
41
  reviewer: [Haleshot](https://github.com/Haleshot)
42
 
43
  ///
 
79
  from dataclasses import dataclass
80
  from typing import Callable, Generic, TypeVar
81
 
82
+ A = TypeVar("A")
83
+ B = TypeVar("B")
84
 
85
  @dataclass
86
+ class Wrapper(Generic[A]):
87
+ value: A
88
  ```
89
 
90
  Now, we can create an instance of wrapped data:
 
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
 
 
124
 
125
 
126
  @app.cell
127
+ def _(A, B, Callable, Functor, Generic, dataclass, pp):
128
  @dataclass
129
+ class Wrapper(Functor, Generic[A]):
130
+ value: A
131
 
132
+ @classmethod
133
+ def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
134
+ return Wrapper(f(a.value))
 
 
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 Wrapper, wrapper
142
 
143
 
 
147
  """
148
  We can analyze the type signature of `fmap` for `Wrapper`:
149
 
150
+ * `f` is of type `Callable[[A], B]`
151
+ * `a` is of type `Wrapper[A]`
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], a: Wrapper[A]) -> Wrapper[B]:
158
  ```
159
 
160
  Essentially, `fmap`:
161
 
162
+ 1. Takes a function `Callable[[A], B]` and a `Wrapper[A]` instance as input.
163
  2. Applies the function to the value inside the wrapper.
164
+ 3. Returns a new `Wrapper[B]` instance with the transformed value, leaving the original wrapper and its internal data unmodified.
165
 
166
  Now, let's examine `list` as a similar kind of wrapper.
167
  """
 
175
  """
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 _(A, B, Callable, Functor, Generic, dataclass, pp):
206
  @dataclass
207
+ class List(Functor, Generic[A]):
208
+ value: list[A]
 
 
 
 
 
 
209
 
210
+ @classmethod
211
+ def fmap(cls, f: Callable[[A], B], a: "List[A]") -> "List[B]":
212
+ return List([f(x) for x in a.value])
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 List, flist
 
 
 
219
 
220
 
221
  @app.cell(hide_code=True)
 
224
  """
225
  ### Extracting the Type of `fmap`
226
 
227
+ The type signature of `fmap` for `List` is:
228
 
229
  ```python
230
+ fmap(f: Callable[[A], B], a: List[A]) -> List[B]
231
  ```
232
 
233
  Similarly, for `Wrapper`:
234
 
235
  ```python
236
+ fmap(f: Callable[[A], B], a: Wrapper[A]) -> Wrapper[B]
237
  ```
238
 
239
  Both follow the same pattern, which we can generalize as:
240
 
241
  ```python
242
+ fmap(f: Callable[[A], B], a: Functor[A]) -> Functor[B]
243
  ```
244
 
245
+ where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
246
 
247
  ### Functors in Haskell (optional)
248
 
 
281
  from typing import Callable, Generic, TypeVar
282
  from abc import ABC, abstractmethod
283
 
284
+ A = TypeVar("A")
285
+ B = TypeVar("B")
286
 
287
  @dataclass
288
+ class Functor(ABC, Generic[A]):
289
+ @classmethod
290
  @abstractmethod
291
+ def fmap(f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
292
  raise NotImplementedError
293
  ```
294
 
 
323
  from dataclasses import dataclass
324
  from typing import Callable, Generic, TypeVar
325
 
326
+ A = TypeVar("A")
327
+ B = TypeVar("B")
328
 
329
  @dataclass
330
  class RoseTree(Functor, Generic[a]):
331
+
332
+ value: A
333
+ children: list["RoseTree[A]"]
334
 
335
+ @classmethod
336
+ def fmap(cls, f: Callable[[A], B], a: "RoseTree[A]") -> "RoseTree[B]":
337
  return RoseTree(
338
+ f(a.value), [cls.fmap(f, child) for child in a.children]
339
  )
340
 
341
  def __repr__(self) -> str:
342
+ return f"Node: {self.value}, Children: {self.children}"
343
  ```
344
 
345
  - The function is applied **recursively** to each node's value.
 
353
 
354
 
355
  @app.cell(hide_code=True)
356
+ def _(A, B, Callable, Functor, Generic, dataclass, mo):
357
  @dataclass
358
+ class RoseTree(Functor, Generic[A]):
359
  """
360
  ### Doc: RoseTree
361
 
 
363
 
364
  **Attributes**
365
 
366
+ - `value (A)`: The value stored in the node.
367
+ - `children (list[RoseTree[A]])`: A list of child nodes forming the tree structure.
368
 
369
  **Methods:**
370
 
371
+ - `fmap(f: Callable[[A], B], a: "RoseTree[A]") -> "RoseTree[B]"`
372
+
373
+ Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
 
 
374
 
375
  **Implementation logic:**
376
 
377
+ - The function `f` is applied to the root node's `value`.
378
  - Each child in `children` recursively calls `fmap`, ensuring all values in the tree are mapped.
379
  - The overall tree structure remains unchanged.
 
 
380
  """
381
 
382
+ value: A
383
+ children: list["RoseTree[A]"]
384
 
385
+ @classmethod
386
+ def fmap(cls, f: Callable[[A], B], a: "RoseTree[A]") -> "RoseTree[B]":
387
  return RoseTree(
388
+ f(a.value), [cls.fmap(f, child) for child in a.children]
389
  )
390
 
391
  def __repr__(self) -> str:
392
+ return f"Node: {self.value}, Children: {self.children}"
393
 
394
 
395
  mo.md(RoseTree.__doc__)
396
  return (RoseTree,)
397
 
398
 
399
+ @app.cell
400
+ def _(RoseTree, pp):
401
+ rosetree = RoseTree(1, [RoseTree(2, []), RoseTree(3, [RoseTree(4, [])])])
402
 
403
+ pp(rosetree)
404
+ pp(RoseTree.fmap(lambda x: [x], rosetree))
405
+ pp(RoseTree.fmap(lambda x: RoseTree(x, []), rosetree))
406
+ return (rosetree,)
 
407
 
408
 
409
  @app.cell(hide_code=True)
 
423
  Translating to Python, we get:
424
 
425
  ```python
426
+ def fmap(func: Callable[[A], B]) -> Callable[[Functor[A]], Functor[B]]
427
  ```
428
 
429
  This means that `fmap`:
430
 
431
+ - Takes an **ordinary function** `Callable[[A], B]` as input.
432
  - Outputs a function that:
433
+ - Takes a **functor** of type `Functor[A]` as input.
434
+ - Outputs a **functor** of type `Functor[B]`.
435
 
436
  We can implement a similar idea in Python:
437
 
438
  ```python
439
+ fmap = lambda f, functor: functor.__class__.fmap(f, functor)
440
+ inc = lambda functor: fmap(lambda x: x + 1, functor)
 
 
 
441
  ```
442
 
443
+ - **`fmap`**: Lifts an ordinary function (`f`) to the functor world, allowing the function to operate on the wrapped value inside the functor.
444
  - **`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.
445
 
446
  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.
447
 
448
  ### Applying the `inc` Function to Various Functors
449
 
450
+ You can now apply `inc` to any functor like `Wrapper`, `List`, or `RoseTree`:
451
 
452
  ```python
453
  # Applying `inc` to a Wrapper
454
  wrapper = Wrapper(5)
455
  inc(wrapper) # Wrapper(value=6)
456
 
457
+ # Applying `inc` to a List
458
+ list_wrapper = List([1, 2, 3])
459
+ inc(list_wrapper) # List(value=[2, 3, 4])
460
 
461
  # Applying `inc` to a RoseTree
462
  tree = RoseTree(1, [RoseTree(2, []), RoseTree(3, [])])
 
469
  return
470
 
471
 
472
+ @app.cell
473
+ def _(flist, pp, rosetree, wrapper):
474
+ fmap = lambda f, functor: functor.__class__.fmap(f, functor)
475
+ inc = lambda functor: fmap(lambda x: x + 1, functor)
476
+
477
+ pp(inc(wrapper))
478
+ pp(inc(flist))
479
+ pp(inc(rosetree))
480
  return fmap, inc
481
 
482
 
 
502
 
503
  ### Functor Law Verification
504
 
505
+ We can define `id` and `compose` in `Python` as below:
506
 
507
+ ```python
508
  id = lambda x: x
509
+ compose = lambda f, g: lambda x: f(g(x))
510
+ ```
511
 
512
+ We can add a helper function `check_functor_law` to verify that an instance satisfies the functor laws.
 
 
 
 
 
 
 
513
 
514
+ ```Python
515
+ check_functor_law = lambda functor: repr(fmap(id, functor)) == repr(functor)
 
516
  ```
517
 
518
  We can verify the functor we've defined.
 
529
 
530
 
531
  @app.cell
532
+ def _(fmap, id):
533
+ check_functor_law = lambda functor: repr(fmap(id, functor)) == repr(functor)
534
+ return (check_functor_law,)
535
+
536
+
537
+ @app.cell
538
+ def _(check_functor_law, flist, pp, rosetree, wrapper):
539
+ for functor in (wrapper, flist, rosetree):
540
+ pp(check_functor_law(functor))
541
+ return (functor,)
542
 
543
 
544
  @app.cell(hide_code=True)
545
  def _(mo):
546
+ mo.md(
547
+ """
548
+ And here is an `EvilFunctor`. We can verify it's not a valid `Functor`.
549
+
550
+ ```python
551
+ @dataclass
552
+ class EvilFunctor(Functor, Generic[A]):
553
+ value: list[A]
554
+
555
+ @classmethod
556
+ def fmap(cls, f: Callable[[A], B], a: "EvilFunctor[A]") -> "EvilFunctor[B]":
557
+ return (
558
+ cls([a.value[0]] * 2 + list(map(f, a.value[1:])))
559
+ if a.value
560
+ else []
561
+ )
562
+ ```
563
+ """
564
+ )
565
  return
566
 
567
 
568
  @app.cell
569
+ def _(A, B, Callable, Functor, Generic, check_functor_law, dataclass, pp):
570
  @dataclass
571
+ class EvilFunctor(Functor, Generic[A]):
572
+ value: list[A]
573
 
574
+ @classmethod
575
+ def fmap(
576
+ cls, f: Callable[[A], B], a: "EvilFunctor[A]"
577
+ ) -> "EvilFunctor[B]":
578
  return (
579
+ cls([a.value[0]] * 2 + [f(x) for x in a.value[1:]])
580
+ if a.value
581
  else []
582
  )
583
 
 
 
 
 
584
 
585
+ pp(check_functor_law(EvilFunctor([1, 2, 3, 4])))
586
+ return (EvilFunctor,)
 
 
587
 
588
 
589
  @app.cell(hide_code=True)
 
595
  We can now draft the final definition of `Functor` with some utility functions.
596
 
597
  ```Python
598
+ @classmethod
 
599
  @abstractmethod
600
+ def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
601
  return NotImplementedError
602
 
603
+ @classmethod
604
+ def const_fmap(cls, a: "Functor[A]", b: B) -> "Functor[B]":
605
+ return cls.fmap(lambda _: b, a)
 
 
606
 
607
+ @classmethod
608
+ def void(cls, a: "Functor[A]") -> "Functor[None]":
609
+ return cls.const_fmap(a, None)
 
 
 
610
  ```
611
  """
612
  )
 
614
 
615
 
616
  @app.cell(hide_code=True)
617
+ def _(A, ABC, B, Callable, Generic, abstractmethod, dataclass, mo):
618
  @dataclass
619
+ class Functor(ABC, Generic[A]):
620
  """
621
  ### Doc: Functor
622
 
 
624
 
625
  **Methods:**
626
 
627
+ - `fmap(f: Callable[[A], B], a: Functor[A]) -> Functor[B]`
628
+ Abstract method to apply a function to all values inside a functor.
 
 
 
 
629
 
630
+ - `const_fmap(a: "Functor[A]", b: B) -> Functor[B]`
631
+ Replaces all values inside a functor with a constant `b`, preserving the original structure.
632
 
633
+ - `void(a: "Functor[A]") -> Functor[None]`
634
+ Equivalent to `const_fmap(a, None)`, transforming all values in a functor into `None`.
 
 
 
 
 
 
 
 
 
635
  """
636
 
637
+ @classmethod
638
  @abstractmethod
639
+ def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
640
  return NotImplementedError
641
 
642
+ @classmethod
643
+ def const_fmap(cls, a: "Functor[A]", b: B) -> "Functor[B]":
644
+ return cls.fmap(lambda _: b, a)
645
 
646
+ @classmethod
647
+ def void(cls, a: "Functor[A]") -> "Functor[None]":
648
+ return cls.const_fmap(a, None)
 
 
 
 
 
 
649
 
650
 
651
  mo.md(Functor.__doc__)
 
658
  return
659
 
660
 
661
+ @app.cell
662
+ def _(List, RoseTree, flist, pp, rosetree):
663
+ pp(RoseTree.const_fmap(rosetree, "λ"))
664
+ pp(RoseTree.void(rosetree))
665
+ pp(List.const_fmap(flist, "λ"))
666
+ pp(List.void(flist))
 
667
  return
668
 
669
 
 
673
  """
674
  ## Functors for Non-Iterable Types
675
 
676
+ 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.
677
 
678
  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.
679
 
680
  ### The Maybe Functor
681
 
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 _(A, B, Callable, Functor, Generic, dataclass):
 
 
 
 
 
 
 
 
 
 
 
 
707
  @dataclass
708
+ class Maybe(Functor, Generic[A]):
709
+ value: None | A
710
 
711
+ @classmethod
712
+ def fmap(cls, f: Callable[[A], B], a: "Maybe[A]") -> "Maybe[B]":
713
+ return cls(None) if a.value is None else cls(f(a.value))
 
 
714
 
715
  def __repr__(self):
716
+ return "Nothing" if self.value is None else repr(self.value)
717
+ return (Maybe,)
718
 
719
 
720
  @app.cell(hide_code=True)
721
  def _(mo):
722
  mo.md(
723
  """
724
+ **`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`.
 
725
 
726
  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.
727
 
 
732
 
733
 
734
  @app.cell
735
+ def _(Maybe, pp):
736
+ mint = Maybe(1)
 
737
  mnone = Maybe(None)
 
 
738
 
739
+ pp(Maybe.fmap(lambda x: x + 1, mint))
740
+ pp(Maybe.fmap(lambda x: x + 1, mnone))
741
+ return mint, mnone
 
 
 
 
 
 
 
742
 
743
 
744
  @app.cell(hide_code=True)
 
815
  Remember that we defined the `id` and `compose` function above as:
816
 
817
  ```Python
818
+ def id(x: Generic[A]) -> Generic[A]:
819
  return x
820
 
821
+ def compose(f: Callable[[B], C], g: Callable[[A], B]) -> Callable[[A], C]:
822
  return lambda x: f(g(x))
823
  ```
824
 
 
869
  - Maps any object $A$ in $C$ to $F ( A )$, in $D$.
870
  - Maps morphisms $f : A → B$ in $C$ to $F ( f ) : F ( A ) → F ( B )$ in $D$.
871
 
872
+ /// admonition |
873
+
874
+ Endofunctors are functors from a category to itself.
875
+
876
+ ///
877
  """
878
  )
879
  return
 
887
 
888
  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.
889
 
890
+ 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`.
891
 
892
  Recall the definition of `Functor`:
893
 
894
  ```Python
895
  @dataclass
896
+ class Functor(ABC, Generic[A])
897
  ```
898
 
899
  And RoseTree:
900
 
901
  ```Python
902
  @dataclass
903
+ class RoseTree(Functor, Generic[A])
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]]`.
907
 
908
  But that's it. We've defined two parts, something that takes objects in `Py` to objects in another category (that of `RoseTree` types and functions defined on `RoseTree` types), and something that takes morphisms in `Py` to morphisms in this category. So `RoseTree` is a functor.
909
 
 
939
  def _(mo):
940
  mo.md(
941
  """
942
+ Remember that we defined the `fmap`, `id` and `compose` as
943
  ```python
944
+ fmap = lambda f, functor: functor.__class__.fmap(f, functor)
 
945
  id = lambda x: x
946
  compose = lambda f, g: lambda x: f(g(x))
947
  ```
948
 
949
  Let's prove that `fmap` is a functor.
950
 
951
+ 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):
952
 
953
  **Notice that** in this case, we can actually view `fmap` as:
954
  ```python
955
+ fmap = lambda f, functor: functor.fmap(f, functor)
 
956
  ```
957
 
958
  We define `WrapperCategory` as:
959
 
960
  ```python
961
  @dataclass
962
+ class WrapperCategory:
963
  @staticmethod
964
+ def id(wrapper: Wrapper[A]) -> Wrapper[A]:
965
+ return Wrapper(wrapper.value)
966
 
967
  @staticmethod
968
  def compose(
969
+ f: Callable[[Wrapper[B]], Wrapper[C]],
970
+ g: Callable[[Wrapper[A]], Wrapper[B]],
971
+ wrapper: Wrapper[A]
972
+ ) -> Callable[[Wrapper[A]], Wrapper[C]]:
973
+ return f(g(Wrapper(wrapper.value)))
974
  ```
975
 
976
  And `Wrapper` is:
977
 
978
  ```Python
979
  @dataclass
980
+ class Wrapper(Functor, Generic[A]):
981
+ value: A
982
 
983
+ @classmethod
984
+ def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
985
+ return Wrapper(f(a.value))
986
  ```
987
  """
988
  )
 
993
  def _(mo):
994
  mo.md(
995
  """
996
+ We can prove that:
997
 
998
  ```python
999
+ fmap(id, wrapper)
1000
+ = Wrapper.fmap(id, wrapper)
1001
+ = Wrapper(id(wrapper.value))
1002
+ = Wrapper(wrapper.value)
1003
+ = WrapperCategory.id(wrapper)
1004
  ```
1005
+ and:
 
 
 
 
 
 
 
 
 
 
1006
  ```python
1007
+ fmap(compose(f, g), wrapper)
1008
+ = Wrapper.fmap(compose(f, g), wrapper)
1009
+ = Wrapper(compose(f, g)(wrapper.value))
1010
+ = Wrapper(f(g(wrapper.value)))
1011
+
1012
+ WrapperCategory.compose(fmap(f, wrapper), fmap(g, wrapper), wrapper)
1013
+ = fmap(f, wrapper)(fmap(g, wrapper)(wrapper))
1014
+ = fmap(f, wrapper)(Wrapper.fmap(g, wrapper))
1015
+ = fmap(f, wrapper)(Wrapper(g(wrapper.value)))
1016
+ = Wrapper.fmap(f, Wrapper(g(wrapper.value)))
1017
+ = Wrapper(f(Wrapper(g(wrapper.value)).value))
1018
+ = Wrapper(f(g(wrapper.value))) # Wrapper(g(wrapper.value)).value = g(wrapper.value)
 
1019
  ```
1020
 
1021
  So our `Wrapper` is a valid `Functor`.
 
1026
  return
1027
 
1028
 
1029
+ @app.cell
1030
+ def _(A, B, C, Callable, Wrapper, dataclass):
1031
  @dataclass
1032
  class WrapperCategory:
1033
  @staticmethod
1034
+ def id(wrapper: Wrapper[A]) -> Wrapper[A]:
1035
+ return Wrapper(wrapper.value)
1036
 
1037
  @staticmethod
1038
  def compose(
1039
+ f: Callable[[Wrapper[B]], Wrapper[C]],
1040
+ g: Callable[[Wrapper[A]], Wrapper[B]],
1041
+ wrapper: Wrapper[A],
1042
+ ) -> Callable[[Wrapper[A]], Wrapper[C]]:
1043
+ return f(g(Wrapper(wrapper.value)))
1044
  return (WrapperCategory,)
1045
 
1046
 
1047
+ @app.cell
1048
+ def _(WrapperCategory, fmap, id, pp, wrapper):
1049
+ pp(fmap(id, wrapper) == WrapperCategory.id(wrapper))
 
 
 
 
 
 
 
1050
  return
1051
 
1052
 
 
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, Generic, dataclass):
1089
  @dataclass
1090
+ class ListConcatenation(Generic[A]):
1091
+ value: list[A]
1092
 
1093
  @staticmethod
1094
+ def id() -> "ListConcatenation[A]":
1095
  return ListConcatenation([])
1096
 
1097
  @staticmethod
1098
  def compose(
1099
+ this: "ListConcatenation[A]", other: "ListConcatenation[A]"
1100
  ) -> "ListConcatenation[a]":
1101
  return ListConcatenation(this.value + other.value)
1102
  return (ListConcatenation,)
 
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
 
1173
  ### Defining the Length Functor
1174
 
1175
  We now define the `length` function as a functor, mapping from the category of list concatenation to the category of integer addition:
1176
+
1177
+ ```python
1178
+ length = lambda l: IntAddition(len(l.value))
1179
+ ```
1180
  """
1181
  )
1182
  return
1183
 
1184
 
1185
+ @app.cell(hide_code=True)
1186
  def _(IntAddition):
1187
  length = lambda l: IntAddition(len(l.value))
1188
  return (length,)
 
1204
 
1205
  #### 1. **Identity Law**:
1206
  The identity law states that applying the functor to the identity element of one category should give the identity element of the other category.
1207
+
1208
+ ```python
1209
+ > length(ListConcatenation.id()) == IntAddition.id()
1210
+ True
1211
+ ```
1212
  """
1213
  )
1214
  return
1215
 
1216
 
 
 
 
 
 
 
1217
  @app.cell(hide_code=True)
1218
  def _(mo):
1219
  mo.md("""This ensures that the length of an empty list (identity in the `ListConcatenation` category) is `0` (identity in the `IntAddition` category).""")
 
1226
  """
1227
  #### 2. **Composition Law**:
1228
  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.
 
 
 
 
 
 
 
 
 
 
 
1229
 
1230
+ ```python
1231
+ > lista = ListConcatenation([1, 2])
1232
+ > listb = ListConcatenation([3, 4])
1233
+ > length(ListConcatenation.compose(lista, listb)) == IntAddition.compose(
1234
+ > length(lista), length(listb)
1235
+ > )
1236
+ True
1237
+ ```
1238
+ """
1239
  )
1240
  return
1241
 
 
1246
  return
1247
 
1248
 
1249
+ @app.cell
1250
+ def _(IntAddition, ListConcatenation, length, pp):
1251
+ pp(length(ListConcatenation.id()) == IntAddition.id())
1252
+ lista = ListConcatenation([1, 2])
1253
+ listb = ListConcatenation([3, 4])
1254
+ pp(
1255
+ length(ListConcatenation.compose(lista, listb))
1256
+ == IntAddition.compose(length(lista), length(listb))
1257
+ )
1258
+ return lista, listb
1259
+
1260
+
1261
  @app.cell(hide_code=True)
1262
  def _(mo):
1263
  mo.md(
 
1297
  def _():
1298
  from dataclasses import dataclass
1299
  from typing import Callable, Generic, TypeVar
1300
+ from pprint import pp
1301
+ return Callable, Generic, TypeVar, dataclass, pp
1302
 
1303
 
1304
  @app.cell(hide_code=True)
1305
  def _(TypeVar):
1306
+ A = TypeVar("A")
1307
+ B = TypeVar("B")
1308
+ C = TypeVar("C")
1309
+ return A, B, C
1310
 
1311
 
1312
  if __name__ == "__main__":
functional_programming/CHANGELOG.md CHANGED
@@ -9,3 +9,28 @@
9
  * `0.1.0` version of notebook `05_functors`
10
 
11
  Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  * `0.1.0` version of notebook `05_functors`
10
 
11
  Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
12
+
13
+ ## 2025-03-16
14
+
15
+ + Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`
16
+ + Refactor the `Functor` class, changing `fmap` and utility methods to `classmethod`
17
+
18
+ For example:
19
+
20
+ ```python
21
+ @dataclass
22
+ class Wrapper(Functor, Generic[A]):
23
+ value: A
24
+
25
+ @classmethod
26
+ def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
27
+ return Wrapper(f(a.value))
28
+
29
+ >>> Wrapper.fmap(lambda x: x + 1, wrapper)
30
+ Wrapper(value=2)
31
+ ```
32
+
33
+ + Move the `check_functor_law` method from `Functor` class to a standard function
34
+ - Rename `ListWrapper` to `List` for simplicity
35
+ - Remove the `Just` class
36
+ + Rewrite proofs