Srihari Thyagarajan commited on
Commit
8f7960d
·
unverified ·
2 Parent(s): 2ddafd0 8792d7c

Merge pull request #92 from metaboulie/fp/functors

Browse files

refactor(functors): v0.1.4 of notebook functors for fp course

functional_programming/05_functors.py CHANGED
@@ -1,5 +1,5 @@
1
  # /// script
2
- # requires-python = ">=3.9"
3
  # dependencies = [
4
  # "marimo",
5
  # ]
@@ -7,7 +7,7 @@
7
 
8
  import marimo
9
 
10
- __generated_with = "0.11.17"
11
  app = marimo.App(app_title="Category Theory and Functors")
12
 
13
 
@@ -37,7 +37,7 @@ def _(mo):
37
  /// details | Notebook metadata
38
  type: info
39
 
40
- version: 0.1.1 | last modified: 2025-03-16 | author: [métaboulie](https://github.com/metaboulie)<br/>
41
  reviewer: [Haleshot](https://github.com/Haleshot)
42
 
43
  ///
@@ -77,13 +77,13 @@ def _(mo):
77
 
78
  ```python
79
  from dataclasses import dataclass
80
- from typing import Callable, Generic, TypeVar
81
 
82
  A = TypeVar("A")
83
  B = TypeVar("B")
84
 
85
  @dataclass
86
- class Wrapper(Generic[A]):
87
  value: A
88
  ```
89
 
@@ -96,49 +96,55 @@ 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
- ```python
101
- @dataclass
102
- class Wrapper(Functor, Generic[A]):
103
- value: A
104
-
105
- @classmethod
106
- def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]":
107
- return Wrapper(f(a.value))
108
- ```
109
-
110
- Now, we can apply transformations without unwrapping:
111
-
112
- ```python
113
- >>> Wrapper.fmap(lambda x: x + 1, wrapper)
114
- Wrapper(value=2)
115
-
116
- >>> Wrapper.fmap(lambda x: [x], wrapper)
117
- Wrapper(value=[1])
118
- ```
119
-
120
- > Try using the `Wrapper` in the cell below.
121
  """
122
  )
123
  return
124
 
125
 
126
  @app.cell
127
- def _(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
 
144
  @app.cell(hide_code=True)
@@ -147,14 +153,14 @@ def _(mo):
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`:
@@ -173,49 +179,38 @@ def _(mo):
173
  def _(mo):
174
  mo.md(
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)
@@ -227,19 +222,19 @@ def _(mo):
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.
@@ -277,24 +272,29 @@ def _(mo):
277
  To define `Functor` in Python, we use an abstract base class:
278
 
279
  ```python
280
- from dataclasses import dataclass
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
 
295
  We can now extend custom wrappers, containers, or computation contexts with this `Functor` base class, implement the `fmap` method, and apply any function.
 
 
 
 
296
 
297
- Next, let's implement a more complex data structure: [RoseTree](https://en.wikipedia.org/wiki/Rose_tree).
 
 
 
 
 
 
 
 
298
  """
299
  )
300
  return
@@ -303,96 +303,147 @@ def _(mo):
303
  @app.cell(hide_code=True)
304
  def _(mo):
305
  mo.md(
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  """
307
- ## Case Study: RoseTree
 
308
 
309
- A **RoseTree** is a tree where:
310
 
311
- - Each node holds a **value**.
312
- - Each node has a **list of child nodes** (which are also RoseTrees).
 
 
 
313
 
314
- This structure is useful for representing hierarchical data, such as:
 
 
315
 
316
- - Abstract Syntax Trees (ASTs)
317
- - File system directories
318
- - Recursive computations
319
 
320
- We can implement `RoseTree` by extending the `Functor` class:
321
 
322
- ```python
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.
346
- - The tree structure **remains unchanged**.
347
- - Only the values inside the tree are modified.
348
 
349
- > Try using `RoseTree` in the cell below.
350
  """
351
  )
352
  return
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
 
362
- A Functor implementation of `RoseTree`, allowing transformation of values while preserving the tree structure.
 
 
 
 
 
 
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
 
@@ -423,7 +474,7 @@ def _(mo):
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`:
@@ -433,51 +484,42 @@ def _(mo):
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, [])])
463
- inc(tree) # RoseTree(value=2, children=[RoseTree(value=3, children=[]), RoseTree(value=4, children=[])])
464
- ```
465
 
466
- > Try using `fmap` in the cell below.
 
 
 
 
 
 
467
  """
468
  )
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
 
483
  @app.cell(hide_code=True)
@@ -497,25 +539,20 @@ def _(mo):
497
  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`.
498
 
499
  /// admonition |
500
- - 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.
501
  ///
 
 
 
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.
519
  """
520
  )
521
  return
@@ -528,12 +565,26 @@ def _():
528
  return compose, id
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):
@@ -543,202 +594,136 @@ def _(check_functor_law, flist, pp, rosetree, wrapper):
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)
590
  def _(mo):
591
  mo.md(
592
- """
593
- ## Final definition of Functor
594
-
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
  )
613
  return
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
-
623
- A generic interface for types that support mapping over their values.
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__)
652
- return (Functor,)
 
 
 
653
 
654
 
655
  @app.cell(hide_code=True)
656
  def _(mo):
657
- mo.md("""> Try with utility functions in the cell below""")
 
 
 
 
 
 
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
 
670
  @app.cell(hide_code=True)
671
  def _(mo):
672
- mo.md(
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
-
728
- > Try using `Maybe` in the cell below.
729
- """
730
- )
731
- return
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,7 +800,7 @@ def _(mo):
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]:
@@ -887,20 +872,20 @@ def _(mo):
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]]`.
@@ -928,8 +913,13 @@ def _(mo):
928
 
929
  Once again there are a few axioms that functors have to obey.
930
 
931
- 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)}}$
932
- 2. Functors must distribute over morphism composition, i.e. ${\displaystyle F(f\circ g)=F(f)\circ F(g)}$
 
 
 
 
 
933
  """
934
  )
935
  return
@@ -939,22 +929,22 @@ def _(mo):
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
@@ -977,12 +967,12 @@ def _(mo):
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
  )
@@ -1045,8 +1035,8 @@ def _(A, B, C, Callable, Wrapper, dataclass):
1045
 
1046
 
1047
  @app.cell
1048
- def _(WrapperCategory, fmap, id, pp, wrapper):
1049
- pp(fmap(id, wrapper) == WrapperCategory.id(wrapper))
1050
  return
1051
 
1052
 
@@ -1056,38 +1046,22 @@ def _(mo):
1056
  """
1057
  ## Length as a Functor
1058
 
1059
- 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.
1060
 
1061
  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**.
1062
 
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
@@ -1120,20 +1094,6 @@ def _(mo):
1120
  ### Category of Integer Addition
1121
 
1122
  Now, let's define the category of integer addition:
1123
-
1124
- ```python
1125
- @dataclass
1126
- class IntAddition:
1127
- value: int
1128
-
1129
- @staticmethod
1130
- def id() -> "IntAddition":
1131
- return IntAddition(0)
1132
-
1133
- @staticmethod
1134
- def compose(this: "IntAddition", other: "IntAddition") -> "IntAddition":
1135
- return IntAddition(this.value + other.value)
1136
- ```
1137
  """
1138
  )
1139
  return
@@ -1202,18 +1162,20 @@ def _(mo):
1202
 
1203
  Now, let’s verify that `length` satisfies the two functor laws.
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).""")
@@ -1224,31 +1186,16 @@ def _(mo):
1224
  def _(mo):
1225
  mo.md(
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
 
1242
 
1243
- @app.cell(hide_code=True)
1244
- def _(mo):
1245
- 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.""")
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(
@@ -1258,6 +1205,193 @@ def _(IntAddition, ListConcatenation, length, pp):
1258
  return lista, listb
1259
 
1260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1261
  @app.cell(hide_code=True)
1262
  def _(mo):
1263
  mo.md(
@@ -1265,17 +1399,17 @@ def _(mo):
1265
  # Further reading
1266
 
1267
  - [The Trivial Monad](http://blog.sigfpe.com/2007/04/trivial-monad.html)
1268
- - [Haskellwiki. Category Theory](https://en.wikibooks.org/wiki/Haskell/Category_theory)
1269
- - [Haskellforall. The Category Design Pattern](https://www.haskellforall.com/2012/08/the-category-design-pattern.html)
1270
- - [Haskellforall. The Functor Design Pattern](https://www.haskellforall.com/2012/09/the-functor-design-pattern.html)
1271
 
1272
  /// attention | ATTENTION
1273
  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.
1274
  ///
1275
 
1276
- - [Haskellwiki. Functor](https://wiki.haskell.org/index.php?title=Functor)
1277
- - [Haskellwiki. Typeclassopedia#Functor](https://wiki.haskell.org/index.php?title=Typeclassopedia#Functor)
1278
- - [Haskellwiki. Typeclassopedia#Category](https://wiki.haskell.org/index.php?title=Typeclassopedia#Category)
 
1279
  """
1280
  )
1281
  return
@@ -1296,9 +1430,9 @@ def _():
1296
  @app.cell(hide_code=True)
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)
@@ -1306,7 +1440,8 @@ 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__":
 
1
  # /// script
2
+ # requires-python = ">=3.13"
3
  # dependencies = [
4
  # "marimo",
5
  # ]
 
7
 
8
  import marimo
9
 
10
+ __generated_with = "0.12.8"
11
  app = marimo.App(app_title="Category Theory and Functors")
12
 
13
 
 
37
  /// details | Notebook metadata
38
  type: info
39
 
40
+ version: 0.1.5 | last modified: 2025-04-11 | author: [métaboulie](https://github.com/metaboulie)<br/>
41
  reviewer: [Haleshot](https://github.com/Haleshot)
42
 
43
  ///
 
77
 
78
  ```python
79
  from dataclasses import dataclass
80
+ from typing import TypeVar
81
 
82
  A = TypeVar("A")
83
  B = TypeVar("B")
84
 
85
  @dataclass
86
+ class Wrapper[A]:
87
  value: A
88
  ```
89
 
 
96
  ### Mapping Functions Over Wrapped Data
97
 
98
  To modify wrapped data while keeping it wrapped, we define an `fmap` method:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  """
100
  )
101
  return
102
 
103
 
104
  @app.cell
105
+ def _(B, Callable, Functor, dataclass):
106
  @dataclass
107
+ class Wrapper[A](Functor):
108
  value: A
109
 
110
  @classmethod
111
+ def fmap(cls, g: Callable[[A], B], fa: "Wrapper[A]") -> "Wrapper[B]":
112
+ return Wrapper(g(fa.value))
113
+ return (Wrapper,)
114
+
115
+
116
+ @app.cell(hide_code=True)
117
+ def _(mo):
118
+ mo.md(
119
+ r"""
120
+ /// attention
121
+
122
+ To distinguish between regular types and functors, we use the prefix `f` to indicate `Functor`.
123
+
124
+ For instance,
125
+
126
+ - `a: A` is a regular variable of type `A`
127
+ - `g: Callable[[A], B]` is a regular function from type `A` to `B`
128
+ - `fa: Functor[A]` is a *Functor* wrapping a value of type `A`
129
+ - `fg: Functor[Callable[[A], B]]` is a *Functor* wrapping a function from type `A` to `B`
130
+
131
+ and we will avoid using `f` to represent a function
132
+
133
+ ///
134
+
135
+ > Try with Wrapper below
136
+ """
137
+ )
138
+ return
139
 
140
 
141
+ @app.cell
142
+ def _(Wrapper, pp):
143
  wrapper = Wrapper(1)
144
 
145
  pp(Wrapper.fmap(lambda x: x + 1, wrapper))
146
  pp(Wrapper.fmap(lambda x: [x], wrapper))
147
+ return (wrapper,)
148
 
149
 
150
  @app.cell(hide_code=True)
 
153
  """
154
  We can analyze the type signature of `fmap` for `Wrapper`:
155
 
156
+ * `g` is of type `Callable[[A], B]`
157
+ * `fa` is of type `Wrapper[A]`
158
  * The return value is of type `Wrapper[B]`
159
 
160
  Thus, in Python's type system, we can express the type signature of `fmap` as:
161
 
162
  ```python
163
+ fmap(g: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]:
164
  ```
165
 
166
  Essentially, `fmap`:
 
179
  def _(mo):
180
  mo.md(
181
  """
182
+ ## The List Functor
183
 
184
  We can define a `List` class to represent a wrapped list that supports `fmap`:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  """
186
  )
187
  return
188
 
189
 
190
  @app.cell
191
+ def _(B, Callable, Functor, dataclass):
192
  @dataclass
193
+ class List[A](Functor):
194
  value: list[A]
195
 
196
  @classmethod
197
+ def fmap(cls, g: Callable[[A], B], fa: "List[A]") -> "List[B]":
198
+ return List([g(x) for x in fa.value])
199
+ return (List,)
200
 
201
 
202
+ @app.cell(hide_code=True)
203
+ def _(mo):
204
+ mo.md(r"""> Try with List below""")
205
+ return
206
+
207
+
208
+ @app.cell
209
+ def _(List, pp):
210
  flist = List([1, 2, 3, 4])
211
  pp(List.fmap(lambda x: x + 1, flist))
212
  pp(List.fmap(lambda x: [x], flist))
213
+ return (flist,)
214
 
215
 
216
  @app.cell(hide_code=True)
 
222
  The type signature of `fmap` for `List` is:
223
 
224
  ```python
225
+ fmap(g: Callable[[A], B], fa: List[A]) -> List[B]
226
  ```
227
 
228
  Similarly, for `Wrapper`:
229
 
230
  ```python
231
+ fmap(g: Callable[[A], B], fa: Wrapper[A]) -> Wrapper[B]
232
  ```
233
 
234
  Both follow the same pattern, which we can generalize as:
235
 
236
  ```python
237
+ fmap(g: Callable[[A], B], fa: Functor[A]) -> Functor[B]
238
  ```
239
 
240
  where `Functor` can be `Wrapper`, `List`, or any other wrapper type that follows the same structure.
 
272
  To define `Functor` in Python, we use an abstract base class:
273
 
274
  ```python
 
 
 
 
 
 
 
275
  @dataclass
276
+ class Functor[A](ABC):
277
  @classmethod
278
  @abstractmethod
279
+ def fmap(g: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
280
  raise NotImplementedError
281
  ```
282
 
283
  We can now extend custom wrappers, containers, or computation contexts with this `Functor` base class, implement the `fmap` method, and apply any function.
284
+ """
285
+ )
286
+ return
287
+
288
 
289
+ @app.cell(hide_code=True)
290
+ def _(mo):
291
+ mo.md(
292
+ r"""
293
+ # More Functor instances (optional)
294
+
295
+ In this section, we will explore more *Functor* instances to help you build up a better comprehension.
296
+
297
+ The main reference is [Data.Functor](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Functor.html)
298
  """
299
  )
300
  return
 
303
  @app.cell(hide_code=True)
304
  def _(mo):
305
  mo.md(
306
+ r"""
307
+ ## The [Maybe](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Maybe.html#t:Maybe) Functor
308
+
309
+ **`Maybe`** is a functor that can either hold a value (`Just(value)`) or be `Nothing` (equivalent to `None` in Python).
310
+
311
+ - It the value exists, `fmap` applies the function to this value inside the functor.
312
+ - If the value is `None`, `fmap` simply returns `None`.
313
+
314
+ /// admonition
315
+ 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.
316
+ ///
317
+
318
+ We can implement the `Maybe` functor as:
319
  """
320
+ )
321
+ return
322
 
 
323
 
324
+ @app.cell
325
+ def _(B, Callable, Functor, dataclass):
326
+ @dataclass
327
+ class Maybe[A](Functor):
328
+ value: None | A
329
 
330
+ @classmethod
331
+ def fmap(cls, g: Callable[[A], B], fa: "Maybe[A]") -> "Maybe[B]":
332
+ return cls(None) if fa.value is None else cls(g(fa.value))
333
 
334
+ def __repr__(self):
335
+ return "Nothing" if self.value is None else f"Just({self.value!r})"
336
+ return (Maybe,)
337
 
 
338
 
339
+ @app.cell
340
+ def _(Maybe, pp):
341
+ pp(Maybe.fmap(lambda x: x + 1, Maybe(1)))
342
+ pp(Maybe.fmap(lambda x: x + 1, Maybe(None)))
343
+ return
344
 
 
 
345
 
346
+ @app.cell(hide_code=True)
347
+ def _(mo):
348
+ mo.md(
349
+ r"""
350
+ ## The [Either](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Either.html#t:Either) Functor
351
 
352
+ The `Either` type represents values with two possibilities: a value of type `Either a b` is either `Left a` or `Right b`.
 
 
 
 
353
 
354
+ The `Either` type is sometimes used to represent a value which is **either correct or an error**; by convention, the `left` attribute is used to hold an error value and the `right` attribute is used to hold a correct value.
 
 
355
 
356
+ `fmap` for `Either` will ignore Left values, but will apply the supplied function to values contained in the Right.
 
 
357
 
358
+ The implementation is:
359
  """
360
  )
361
  return
362
 
363
 
364
+ @app.cell
365
+ def _(B, Callable, Functor, Union, dataclass):
366
  @dataclass
367
+ class Either[A](Functor):
368
+ left: A = None
369
+ right: A = None
370
+
371
+ def __post_init__(self):
372
+ if (self.left is not None and self.right is not None) or (
373
+ self.left is None and self.right is None
374
+ ):
375
+ raise TypeError(
376
+ "Provide either the value of the left or the value of the right."
377
+ )
378
 
379
+ @classmethod
380
+ def fmap(
381
+ cls, g: Callable[[A], B], fa: "Either[A]"
382
+ ) -> Union["Either[A]", "Either[B]"]:
383
+ if fa.left is not None:
384
+ return cls(left=fa.left)
385
+ return cls(right=g(fa.right))
386
 
387
+ def __repr__(self):
388
+ if self.left is not None:
389
+ return f"Left({self.left!r})"
390
+ return f"Right({self.right!r})"
391
+ return (Either,)
392
 
 
 
393
 
394
+ @app.cell
395
+ def _(Either):
396
+ print(Either.fmap(lambda x: x + 1, Either(left=TypeError("Parse Error"))))
397
+ print(Either.fmap(lambda x: x + 1, Either(right=1)))
398
+ return
399
 
 
400
 
401
+ @app.cell(hide_code=True)
402
+ def _(mo):
403
+ mo.md(
404
+ """
405
+ ## The [RoseTree](https://en.wikipedia.org/wiki/Rose_tree) Functor
406
+
407
+ A **RoseTree** is a tree where:
408
+
409
+ - Each node holds a **value**.
410
+ - Each node has a **list of child nodes** (which are also RoseTrees).
411
 
412
+ This structure is useful for representing hierarchical data, such as:
413
 
414
+ - Abstract Syntax Trees (ASTs)
415
+ - File system directories
416
+ - Recursive computations
417
+
418
+ The implementation is:
419
  """
420
+ )
421
+ return
422
 
423
+
424
+ @app.cell
425
+ def _(B, Callable, Functor, dataclass):
426
+ @dataclass
427
+ class RoseTree[A](Functor):
428
+ value: A # The value stored in the node.
429
+ children: list[
430
+ "RoseTree[A]"
431
+ ] # A list of child nodes forming the tree structure.
432
 
433
  @classmethod
434
+ def fmap(cls, g: Callable[[A], B], fa: "RoseTree[A]") -> "RoseTree[B]":
435
+ """
436
+ Applies a function to each value in the tree, producing a new `RoseTree[b]` with transformed values.
437
+
438
+ 1. `g` is applied to the root node's `value`.
439
+ 2. Each child in `children` recursively calls `fmap`.
440
+ """
441
  return RoseTree(
442
+ g(fa.value), [cls.fmap(g, child) for child in fa.children]
443
  )
444
 
445
  def __repr__(self) -> str:
446
  return f"Node: {self.value}, Children: {self.children}"
 
 
 
447
  return (RoseTree,)
448
 
449
 
 
474
  Translating to Python, we get:
475
 
476
  ```python
477
+ def fmap(g: Callable[[A], B]) -> Callable[[Functor[A]], Functor[B]]
478
  ```
479
 
480
  This means that `fmap`:
 
484
  - Takes a **functor** of type `Functor[A]` as input.
485
  - Outputs a **functor** of type `Functor[B]`.
486
 
487
+ 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.
488
+ """
489
+ )
490
+ return
 
 
 
 
 
 
 
491
 
 
492
 
493
+ @app.cell
494
+ def _():
495
+ inc = lambda functor: functor.fmap(lambda x: x + 1, functor)
496
+ return (inc,)
497
 
 
 
 
 
498
 
499
+ @app.cell
500
+ def _(flist, inc, pp, rosetree, wrapper):
501
+ pp(inc(wrapper))
502
+ pp(inc(flist))
503
+ pp(inc(rosetree))
504
+ return
505
 
 
 
 
 
506
 
507
+ @app.cell(hide_code=True)
508
+ def _(mo):
509
+ mo.md(
510
+ r"""
511
+ /// admonition | exercise
512
+ Implement other generic functions and apply them to different *Functor* instances.
513
+ ///
514
  """
515
  )
516
  return
517
 
518
 
519
+ @app.cell(hide_code=True)
520
+ def _(mo):
521
+ mo.md(r"""# Functor laws and utility functions""")
522
+ return
 
 
 
 
 
523
 
524
 
525
  @app.cell(hide_code=True)
 
539
  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`.
540
 
541
  /// admonition |
542
+ - 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.
543
  ///
544
+ """
545
+ )
546
+ return
547
 
 
 
 
 
 
 
 
 
 
 
548
 
549
+ @app.cell(hide_code=True)
550
+ def _(mo):
551
+ mo.md(
552
+ r"""
553
+ ### Functor laws verification
554
 
555
+ We can define `id` and `compose` in `Python` as:
556
  """
557
  )
558
  return
 
565
  return compose, id
566
 
567
 
568
+ @app.cell(hide_code=True)
569
+ def _(mo):
570
+ mo.md(r"""We can add a helper function `check_functor_law` to verify that an instance satisfies the functor laws:""")
571
+ return
572
+
573
+
574
  @app.cell
575
+ def _(id):
576
+ check_functor_law = lambda functor: repr(functor.fmap(id, functor)) == repr(
577
+ functor
578
+ )
579
  return (check_functor_law,)
580
 
581
 
582
+ @app.cell(hide_code=True)
583
+ def _(mo):
584
+ mo.md(r"""We can verify the functor we've defined:""")
585
+ return
586
+
587
+
588
  @app.cell
589
  def _(check_functor_law, flist, pp, rosetree, wrapper):
590
  for functor in (wrapper, flist, rosetree):
 
594
 
595
  @app.cell(hide_code=True)
596
  def _(mo):
597
+ mo.md("""And here is an `EvilFunctor`. We can verify it's not a valid `Functor`.""")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  return
599
 
600
 
601
  @app.cell
602
+ def _(B, Callable, Functor, dataclass):
603
  @dataclass
604
+ class EvilFunctor[A](Functor):
605
  value: list[A]
606
 
607
  @classmethod
608
  def fmap(
609
+ cls, g: Callable[[A], B], fa: "EvilFunctor[A]"
610
  ) -> "EvilFunctor[B]":
611
  return (
612
+ cls([fa.value[0]] * 2 + [g(x) for x in fa.value[1:]])
613
+ if fa.value
614
  else []
615
  )
616
+ return (EvilFunctor,)
617
 
618
 
619
+ @app.cell
620
+ def _(EvilFunctor, check_functor_law, pp):
621
  pp(check_functor_law(EvilFunctor([1, 2, 3, 4])))
622
+ return
623
 
624
 
625
  @app.cell(hide_code=True)
626
  def _(mo):
627
  mo.md(
628
+ r"""
629
+ ## Utility functions
 
 
630
 
631
+ ```python
632
+ @classmethod
633
+ def const(cls, fa: "Functor[A]", b: B) -> "Functor[B]":
634
+ return cls.fmap(lambda _: b, fa)
 
635
 
636
+ @classmethod
637
+ def void(cls, fa: "Functor[A]") -> "Functor[None]":
638
+ return cls.const(fa, None)
639
 
640
+ @classmethod
641
+ def unzip(
642
+ cls, fab: "Functor[tuple[A, B]]"
643
+ ) -> tuple["Functor[A]", "Functor[B]"]:
644
+ return cls.fmap(lambda p: p[0], fab), cls.fmap(lambda p: p[1], fab)
645
  ```
646
+
647
+ - `const` replaces all values inside a functor with a constant `b`
648
+ - `void` is equivalent to `const(fa, None)`, transforming all values in a functor into `None`
649
+ - `unzip` is a generalization of the regular *unzip* on a list of pairs
650
  """
651
  )
652
  return
653
 
654
 
655
+ @app.cell
656
+ def _(List, Maybe):
657
+ print(Maybe.const(Maybe(0), 1))
658
+ print(Maybe.const(Maybe(None), 1))
659
+ print(List.const(List([1, 2, 3, 4]), 1))
660
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
 
 
 
 
662
 
663
+ @app.cell
664
+ def _(List, Maybe):
665
+ print(Maybe.void(Maybe(1)))
666
+ print(List.void(List([1, 2, 3])))
667
+ return
668
 
669
 
670
+ @app.cell
671
+ def _(List, Maybe):
672
+ print(Maybe.unzip(Maybe(("Hello", "World"))))
673
+ print(List.unzip(List([("I", "love"), ("really", "λ")])))
674
+ return
675
 
676
 
677
  @app.cell(hide_code=True)
678
  def _(mo):
679
+ mo.md(
680
+ r"""
681
+ /// admonition
682
+ You can always override these utility functions with a more efficient implementation for specific instances.
683
+ ///
684
+ """
685
+ )
686
  return
687
 
688
 
689
  @app.cell
690
  def _(List, RoseTree, flist, pp, rosetree):
691
+ pp(RoseTree.const(rosetree, "λ"))
692
  pp(RoseTree.void(rosetree))
693
+ pp(List.const(flist, "λ"))
694
  pp(List.void(flist))
695
  return
696
 
697
 
698
  @app.cell(hide_code=True)
699
  def _(mo):
700
+ mo.md("""# Formal implementation of Functor""")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  return
702
 
703
 
704
  @app.cell
705
+ def _(ABC, B, Callable, abstractmethod, dataclass):
706
  @dataclass
707
+ class Functor[A](ABC):
 
 
708
  @classmethod
709
+ @abstractmethod
710
+ def fmap(cls, g: Callable[[A], B], fa: "Functor[A]") -> "Functor[B]":
711
+ raise NotImplementedError("Subclasses must implement fmap")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ @classmethod
714
+ def const(cls, fa: "Functor[A]", b: B) -> "Functor[B]":
715
+ return cls.fmap(lambda _: b, fa)
716
 
717
+ @classmethod
718
+ def void(cls, fa: "Functor[A]") -> "Functor[None]":
719
+ return cls.const(fa, None)
 
720
 
721
+ @classmethod
722
+ def unzip(
723
+ cls, fab: "Functor[tuple[A, B]]"
724
+ ) -> tuple["Functor[A]", "Functor[B]"]:
725
+ return cls.fmap(lambda p: p[0], fab), cls.fmap(lambda p: p[1], fab)
726
+ return (Functor,)
727
 
728
 
729
  @app.cell(hide_code=True)
 
800
  Remember that we defined the `id` and `compose` function above as:
801
 
802
  ```Python
803
+ def id(x: A) -> A:
804
  return x
805
 
806
  def compose(f: Callable[[B], C], g: Callable[[A], B]) -> Callable[[A], C]:
 
872
 
873
  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.
874
 
875
+ 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`.
876
 
877
  Recall the definition of `Functor`:
878
 
879
  ```Python
880
  @dataclass
881
+ class Functor[A](ABC)
882
  ```
883
 
884
  And RoseTree:
885
 
886
  ```Python
887
  @dataclass
888
+ class RoseTree[A](Functor)
889
  ```
890
 
891
  **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]]`.
 
913
 
914
  Once again there are a few axioms that functors have to obey.
915
 
916
+ 1. Given an identity morphism $id_A$ on an object $A$, $F ( id_A )$ must be the identity morphism on $F ( A )$.:
917
+
918
+ $$F({id} _{A})={id} _{F(A)}$$
919
+
920
+ 3. Functors must distribute over morphism composition.
921
+
922
+ $$F(f\circ g)=F(f)\circ F(g)$$
923
  """
924
  )
925
  return
 
929
  def _(mo):
930
  mo.md(
931
  """
932
+ Remember that we defined the `id` and `compose` as
933
  ```python
 
934
  id = lambda x: x
935
  compose = lambda f, g: lambda x: f(g(x))
936
  ```
937
 
938
+ We can define `fmap` as:
939
 
 
 
 
940
  ```python
941
+ fmap = lambda g, functor: functor.fmap(g, functor)
942
  ```
943
 
944
+ Let's prove that `fmap` is a functor.
945
+
946
+ 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):
947
+
948
  We define `WrapperCategory` as:
949
 
950
  ```python
 
967
 
968
  ```Python
969
  @dataclass
970
+ class Wrapper[A](Functor):
971
  value: A
972
 
973
  @classmethod
974
+ def fmap(cls, g: Callable[[A], B], fa: "Wrapper[A]") -> "Wrapper[B]":
975
+ return Wrapper(g(fa.value))
976
  ```
977
  """
978
  )
 
1035
 
1036
 
1037
  @app.cell
1038
+ def _(WrapperCategory, id, pp, wrapper):
1039
+ pp(wrapper.fmap(id, wrapper) == WrapperCategory.id(wrapper))
1040
  return
1041
 
1042
 
 
1046
  """
1047
  ## Length as a Functor
1048
 
1049
+ 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.
1050
 
1051
  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**.
1052
 
1053
  ### Category of List Concatenation
1054
 
1055
  First, let’s define the category of list concatenation:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  """
1057
  )
1058
  return
1059
 
1060
 
1061
  @app.cell
1062
+ def _(A, dataclass):
1063
  @dataclass
1064
+ class ListConcatenation[A]:
1065
  value: list[A]
1066
 
1067
  @staticmethod
 
1094
  ### Category of Integer Addition
1095
 
1096
  Now, let's define the category of integer addition:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1097
  """
1098
  )
1099
  return
 
1162
 
1163
  Now, let’s verify that `length` satisfies the two functor laws.
1164
 
1165
+ **Identity Law**
 
1166
 
1167
+ The identity law states that applying the functor to the identity element of one category should give the identity element of the other category.
 
 
 
1168
  """
1169
  )
1170
  return
1171
 
1172
 
1173
+ @app.cell
1174
+ def _(IntAddition, ListConcatenation, length, pp):
1175
+ pp(length(ListConcatenation.id()) == IntAddition.id())
1176
+ return
1177
+
1178
+
1179
  @app.cell(hide_code=True)
1180
  def _(mo):
1181
  mo.md("""This ensures that the length of an empty list (identity in the `ListConcatenation` category) is `0` (identity in the `IntAddition` category).""")
 
1186
  def _(mo):
1187
  mo.md(
1188
  """
1189
+ **Composition Law**
 
1190
 
1191
+ 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.
 
 
 
 
 
 
 
1192
  """
1193
  )
1194
  return
1195
 
1196
 
 
 
 
 
 
 
1197
  @app.cell
1198
  def _(IntAddition, ListConcatenation, length, pp):
 
1199
  lista = ListConcatenation([1, 2])
1200
  listb = ListConcatenation([3, 4])
1201
  pp(
 
1205
  return lista, listb
1206
 
1207
 
1208
+ @app.cell(hide_code=True)
1209
+ def _(mo):
1210
+ 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.""")
1211
+ return
1212
+
1213
+
1214
+ @app.cell(hide_code=True)
1215
+ def _(mo):
1216
+ mo.md(
1217
+ r"""
1218
+ # Bifunctor
1219
+
1220
+ A `Bifunctor` is a type constructor that takes two type arguments and **is a functor in both arguments.**
1221
+
1222
+ For example, think about `Either`'s usual `Functor` instance. It only allows you to fmap over the second type parameter: `right` values get mapped, `left` values stay as they are.
1223
+
1224
+ However, its `Bifunctor` instance allows you to map both halves of the sum.
1225
+
1226
+ There are three core methods for `Bifunctor`:
1227
+
1228
+ - `bimap` allows mapping over both type arguments at once.
1229
+ - `first` and `second` are also provided for mapping over only one type argument at a time.
1230
+
1231
+
1232
+ The abstraction of `Bifunctor` is:
1233
+ """
1234
+ )
1235
+ return
1236
+
1237
+
1238
+ @app.cell
1239
+ def _(ABC, B, Callable, D, dataclass, f, id):
1240
+ @dataclass
1241
+ class Bifunctor[A, C](ABC):
1242
+ @classmethod
1243
+ def bimap(
1244
+ cls, g: Callable[[A], B], h: Callable[[C], D], fa: "Bifunctor[A, C]"
1245
+ ) -> "Bifunctor[B, D]":
1246
+ return cls.first(f, cls.second(g, fa))
1247
+
1248
+ @classmethod
1249
+ def first(
1250
+ cls, g: Callable[[A], B], fa: "Bifunctor[A, C]"
1251
+ ) -> "Bifunctor[B, C]":
1252
+ return cls.bimap(g, id, fa)
1253
+
1254
+ @classmethod
1255
+ def second(
1256
+ cls, g: Callable[[B], C], fa: "Bifunctor[A, B]"
1257
+ ) -> "Bifunctor[A, C]":
1258
+ return cls.bimap(id, g, fa)
1259
+ return (Bifunctor,)
1260
+
1261
+
1262
+ @app.cell(hide_code=True)
1263
+ def _(mo):
1264
+ mo.md(
1265
+ r"""
1266
+ /// admonition | minimal implementation requirement
1267
+ - `bimap` or both `first` and `second`
1268
+ ///
1269
+ """
1270
+ )
1271
+ return
1272
+
1273
+
1274
+ @app.cell(hide_code=True)
1275
+ def _(mo):
1276
+ mo.md(r"""## Instances of Bifunctor""")
1277
+ return
1278
+
1279
+
1280
+ @app.cell(hide_code=True)
1281
+ def _(mo):
1282
+ mo.md(
1283
+ r"""
1284
+ ### The Either Bifunctor
1285
+
1286
+ For the `Either Bifunctor`, we allow it to map a function over the `left` value as well.
1287
+
1288
+ Notice that, the `Either Bifunctor` still only contains the `left` value or the `right` value.
1289
+ """
1290
+ )
1291
+ return
1292
+
1293
+
1294
+ @app.cell
1295
+ def _(B, Bifunctor, Callable, D, dataclass):
1296
+ @dataclass
1297
+ class BiEither[A, C](Bifunctor):
1298
+ left: A = None
1299
+ right: C = None
1300
+
1301
+ def __post_init__(self):
1302
+ if (self.left is not None and self.right is not None) or (
1303
+ self.left is None and self.right is None
1304
+ ):
1305
+ raise TypeError(
1306
+ "Provide either the value of the left or the value of the right."
1307
+ )
1308
+
1309
+ @classmethod
1310
+ def bimap(
1311
+ cls, g: Callable[[A], B], h: Callable[[C], D], fa: "BiEither[A, C]"
1312
+ ) -> "BiEither[B, D]":
1313
+ if fa.left is not None:
1314
+ return cls(left=g(fa.left))
1315
+ return cls(right=h(fa.right))
1316
+
1317
+ def __repr__(self):
1318
+ if self.left is not None:
1319
+ return f"Left({self.left!r})"
1320
+ return f"Right({self.right!r})"
1321
+ return (BiEither,)
1322
+
1323
+
1324
+ @app.cell
1325
+ def _(BiEither):
1326
+ print(BiEither.bimap(lambda x: x + 1, lambda x: x * 2, BiEither(left=1)))
1327
+ print(BiEither.bimap(lambda x: x + 1, lambda x: x * 2, BiEither(right=2)))
1328
+ print(BiEither.first(lambda x: x + 1, BiEither(left=1)))
1329
+ print(BiEither.first(lambda x: x + 1, BiEither(right=2)))
1330
+ print(BiEither.second(lambda x: x + 1, BiEither(left=1)))
1331
+ print(BiEither.second(lambda x: x + 1, BiEither(right=2)))
1332
+ return
1333
+
1334
+
1335
+ @app.cell(hide_code=True)
1336
+ def _(mo):
1337
+ mo.md(
1338
+ r"""
1339
+ ### The 2d Tuple Bifunctor
1340
+
1341
+ For 2d tuples, we simply expect `bimap` to map 2 functions to the 2 elements in the tuple respectively.
1342
+ """
1343
+ )
1344
+ return
1345
+
1346
+
1347
+ @app.cell
1348
+ def _(B, Bifunctor, Callable, D, dataclass):
1349
+ @dataclass
1350
+ class BiTuple[A, C](Bifunctor):
1351
+ value: tuple[A, C]
1352
+
1353
+ @classmethod
1354
+ def bimap(
1355
+ cls, g: Callable[[A], B], h: Callable[[C], D], fa: "BiTuple[A, C]"
1356
+ ) -> "BiTuple[B, D]":
1357
+ return cls((g(fa.value[0]), h(fa.value[1])))
1358
+ return (BiTuple,)
1359
+
1360
+
1361
+ @app.cell
1362
+ def _(BiTuple):
1363
+ print(BiTuple.bimap(lambda x: x + 1, lambda x: x * 2, BiTuple((1, 2))))
1364
+ print(BiTuple.first(lambda x: x + 1, BiTuple((1, 2))))
1365
+ print(BiTuple.second(lambda x: x + 1, BiTuple((1, 2))))
1366
+ return
1367
+
1368
+
1369
+ @app.cell(hide_code=True)
1370
+ def _(mo):
1371
+ mo.md(
1372
+ r"""
1373
+ ## Bifunctor laws
1374
+
1375
+ The only law we need to follow is
1376
+
1377
+ ```python
1378
+ bimap(id, id, fa) == id(fa)
1379
+ ```
1380
+
1381
+ and then other laws are followed automatically.
1382
+ """
1383
+ )
1384
+ return
1385
+
1386
+
1387
+ @app.cell
1388
+ def _(BiEither, BiTuple, id):
1389
+ print(BiEither.bimap(id, id, BiEither(left=1)) == id(BiEither(left=1)))
1390
+ print(BiEither.bimap(id, id, BiEither(right=1)) == id(BiEither(right=1)))
1391
+ print(BiTuple.bimap(id, id, BiTuple((1, 2))) == id(BiTuple((1, 2))))
1392
+ return
1393
+
1394
+
1395
  @app.cell(hide_code=True)
1396
  def _(mo):
1397
  mo.md(
 
1399
  # Further reading
1400
 
1401
  - [The Trivial Monad](http://blog.sigfpe.com/2007/04/trivial-monad.html)
1402
+ - [Haskellforall: The Category Design Pattern](https://www.haskellforall.com/2012/08/the-category-design-pattern.html)
1403
+ - [Haskellforall: The Functor Design Pattern](https://www.haskellforall.com/2012/09/the-functor-design-pattern.html)
 
1404
 
1405
  /// attention | ATTENTION
1406
  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.
1407
  ///
1408
 
1409
+ - [Haskellwiki: Functor](https://wiki.haskell.org/index.php?title=Functor)
1410
+ - [Haskellwiki: Typeclassopedia#Functor](https://wiki.haskell.org/index.php?title=Typeclassopedia#Functor)
1411
+ - [Haskellwiki: Typeclassopedia#Category](https://wiki.haskell.org/index.php?title=Typeclassopedia#Category)
1412
+ - [Haskellwiki: Category Theory](https://en.wikibooks.org/wiki/Haskell/Category_theory)
1413
  """
1414
  )
1415
  return
 
1430
  @app.cell(hide_code=True)
1431
  def _():
1432
  from dataclasses import dataclass
1433
+ from typing import Callable, TypeVar, Union
1434
  from pprint import pp
1435
+ return Callable, TypeVar, Union, dataclass, pp
1436
 
1437
 
1438
  @app.cell(hide_code=True)
 
1440
  A = TypeVar("A")
1441
  B = TypeVar("B")
1442
  C = TypeVar("C")
1443
+ D = TypeVar("D")
1444
+ return A, B, C, D
1445
 
1446
 
1447
  if __name__ == "__main__":
functional_programming/06_applicatives.py CHANGED
@@ -7,12 +7,12 @@
7
 
8
  import marimo
9
 
10
- __generated_with = "0.12.4"
11
  app = marimo.App(app_title="Applicative programming with effects")
12
 
13
 
14
  @app.cell(hide_code=True)
15
- def _(mo):
16
  mo.md(
17
  r"""
18
  # Applicative programming with effects
@@ -26,24 +26,25 @@ def _(mo):
26
 
27
  In this notebook, you will learn:
28
 
29
- 1. How to view `applicative` as multi-functor.
30
  2. How to use `lift` to simplify chaining application.
31
  3. How to bring *effects* to the functional pure world.
32
- 4. How to view `applicative` as lax monoidal functor.
 
33
 
34
  /// details | Notebook metadata
35
  type: info
36
 
37
- version: 0.1.2 | last modified: 2025-04-07 | author: [métaboulie](https://github.com/metaboulie)<br/>
 
38
 
39
  ///
40
  """
41
  )
42
- return
43
 
44
 
45
  @app.cell(hide_code=True)
46
- def _(mo):
47
  mo.md(
48
  r"""
49
  # The intuition: [Multifunctor](https://arxiv.org/pdf/2401.14286)
@@ -67,17 +68,16 @@ def _(mo):
67
  And we have to declare a special version of the functor class for each case.
68
  """
69
  )
70
- return
71
 
72
 
73
  @app.cell(hide_code=True)
74
- def _(mo):
75
  mo.md(
76
  r"""
77
  ## Defining Multifunctor
78
 
79
  /// admonition
80
- we use prefix `f` rather than `ap` to indicate *Applicative Functor*
81
  ///
82
 
83
  As a result, we may want to define a single `Multifunctor` such that:
@@ -111,11 +111,10 @@ def _(mo):
111
  ```
112
  """
113
  )
114
- return
115
 
116
 
117
  @app.cell(hide_code=True)
118
- def _(mo):
119
  mo.md(
120
  r"""
121
  ## Pure, apply and lift
@@ -134,7 +133,7 @@ def _(mo):
134
  # or if we have a regular function `g`
135
  g: Callable[[A], B]
136
  # then we can have `fg` as
137
- fg: Applicative[Callable[[A], B]] = pure(g)
138
  ```
139
 
140
  2. `apply`: applies a function inside an applicative functor to a value inside an applicative functor
@@ -154,11 +153,10 @@ def _(mo):
154
  ```
155
  """
156
  )
157
- return
158
 
159
 
160
  @app.cell(hide_code=True)
161
- def _(mo):
162
  mo.md(
163
  r"""
164
  /// admonition | How to use *Applicative* in the manner of *Multifunctor*
@@ -174,7 +172,7 @@ def _(mo):
174
 
175
  ///
176
 
177
- /// attention | You can suppress the chaining application of `apply` and `pure` as:
178
 
179
  ```python
180
  apply(pure(g), fa) -> lift(g, fa)
@@ -185,11 +183,10 @@ def _(mo):
185
  ///
186
  """
187
  )
188
- return
189
 
190
 
191
  @app.cell(hide_code=True)
192
- def _(mo):
193
  mo.md(
194
  r"""
195
  ## Abstracting applicatives
@@ -202,14 +199,14 @@ def _(mo):
202
  @classmethod
203
  @abstractmethod
204
  def pure(cls, a: A) -> "Applicative[A]":
205
- return NotImplementedError
206
 
207
  @classmethod
208
  @abstractmethod
209
  def apply(
210
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
211
  ) -> "Applicative[B]":
212
- return NotImplementedError
213
 
214
  @classmethod
215
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
@@ -228,17 +225,15 @@ def _(mo):
228
  ///
229
  """
230
  )
231
- return
232
 
233
 
234
  @app.cell(hide_code=True)
235
- def _(mo):
236
  mo.md(r"""# Instances, laws and utility functions""")
237
- return
238
 
239
 
240
  @app.cell(hide_code=True)
241
- def _(mo):
242
  mo.md(
243
  r"""
244
  ## Applicative instances
@@ -249,16 +244,15 @@ def _(mo):
249
  - apply a function inside the computation context to a value inside the computational context
250
  """
251
  )
252
- return
253
 
254
 
255
  @app.cell(hide_code=True)
256
- def _(mo):
257
  mo.md(
258
  r"""
259
- ### Wrapper
260
 
261
- - `pure` should simply *wrap* an object, in the sense that:
262
 
263
  ```haskell
264
  Wrapper.pure(1) => Wrapper(value=1)
@@ -269,7 +263,6 @@ def _(mo):
269
  The implementation is:
270
  """
271
  )
272
- return
273
 
274
 
275
  @app.cell
@@ -291,27 +284,25 @@ def _(Applicative, dataclass):
291
 
292
 
293
  @app.cell(hide_code=True)
294
- def _(mo):
295
  mo.md(r"""> try with Wrapper below""")
296
- return
297
 
298
 
299
  @app.cell
300
- def _(Wrapper):
301
  Wrapper.lift(
302
  lambda a: lambda b: lambda c: a + b * c,
303
  Wrapper(1),
304
  Wrapper(2),
305
  Wrapper(3),
306
  )
307
- return
308
 
309
 
310
  @app.cell(hide_code=True)
311
- def _(mo):
312
  mo.md(
313
  r"""
314
- ### List
315
 
316
  - `pure` should wrap the object in a list, in the sense that:
317
 
@@ -325,7 +316,6 @@ def _(mo):
325
  The implementation is:
326
  """
327
  )
328
- return
329
 
330
 
331
  @app.cell
@@ -345,31 +335,28 @@ def _(Applicative, dataclass, product):
345
 
346
 
347
  @app.cell(hide_code=True)
348
- def _(mo):
349
  mo.md(r"""> try with List below""")
350
- return
351
 
352
 
353
  @app.cell
354
- def _(List):
355
  List.apply(
356
  List([lambda a: a + 1, lambda a: a * 2]),
357
  List([1, 2]),
358
  )
359
- return
360
 
361
 
362
  @app.cell
363
- def _(List):
364
  List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
365
- return
366
 
367
 
368
  @app.cell(hide_code=True)
369
- def _(mo):
370
  mo.md(
371
  r"""
372
- ### Maybe
373
 
374
  - `pure` should wrap the object in a Maybe, in the sense that:
375
 
@@ -385,7 +372,6 @@ def _(mo):
385
  The implementation is:
386
  """
387
  )
388
- return
389
 
390
 
391
  @app.cell
@@ -413,33 +399,116 @@ def _(Applicative, dataclass):
413
 
414
 
415
  @app.cell(hide_code=True)
416
- def _(mo):
417
  mo.md(r"""> try with Maybe below""")
418
- return
419
 
420
 
421
  @app.cell
422
- def _(Maybe):
423
  Maybe.lift(
424
  lambda a: lambda b: a + b,
425
  Maybe(1),
426
  Maybe(2),
427
  )
428
- return
429
 
430
 
431
  @app.cell
432
- def _(Maybe):
433
  Maybe.lift(
434
  lambda a: lambda b: None,
435
  Maybe(1),
436
  Maybe(2),
437
  )
438
- return
439
 
440
 
441
  @app.cell(hide_code=True)
442
- def _(mo):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  mo.md(
444
  r"""
445
  ## Collect the list of response with sequenceL
@@ -465,17 +534,15 @@ def _(mo):
465
  Let's try `sequenceL` with the instances.
466
  """
467
  )
468
- return
469
 
470
 
471
  @app.cell
472
- def _(Wrapper):
473
  Wrapper.sequenceL([Wrapper(1), Wrapper(2), Wrapper(3)])
474
- return
475
 
476
 
477
  @app.cell(hide_code=True)
478
- def _(mo):
479
  mo.md(
480
  r"""
481
  /// attention
@@ -483,29 +550,25 @@ def _(mo):
483
  ///
484
  """
485
  )
486
- return
487
 
488
 
489
  @app.cell
490
- def _(Maybe):
491
  Maybe.sequenceL([Maybe(1), Maybe(2), Maybe(None), Maybe(3)])
492
- return
493
 
494
 
495
  @app.cell(hide_code=True)
496
- def _(mo):
497
  mo.md(r"""The result of `sequenceL` for `List Applicative` is the Cartesian product of the input lists, yielding all possible ordered combinations of elements from each list.""")
498
- return
499
 
500
 
501
  @app.cell
502
- def _(List):
503
  List.sequenceL([List([1, 2]), List([3]), List([5, 6, 7])])
504
- return
505
 
506
 
507
  @app.cell(hide_code=True)
508
- def _(mo):
509
  mo.md(
510
  r"""
511
  ## Applicative laws
@@ -549,7 +612,7 @@ def _(mo):
549
  ```
550
  This one is the trickiest law to gain intuition for. In some sense it is expressing a sort of associativity property of `apply`.
551
 
552
- We can add 4 helper functions to `Applicative` to check whether an instance respects the laws or not:
553
 
554
  ```python
555
  @dataclass
@@ -588,7 +651,6 @@ def _(mo):
588
  > Try to validate applicative laws below
589
  """
590
  )
591
- return
592
 
593
 
594
  @app.cell
@@ -600,7 +662,7 @@ def _():
600
 
601
 
602
  @app.cell
603
- def _(List, Wrapper):
604
  print("Checking Wrapper")
605
  print(Wrapper.check_identity(Wrapper.pure(1)))
606
  print(Wrapper.check_homomorphism(1, lambda x: x + 1))
@@ -622,11 +684,10 @@ def _(List, Wrapper):
622
  List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
623
  )
624
  )
625
- return
626
 
627
 
628
  @app.cell(hide_code=True)
629
- def _(mo):
630
  mo.md(
631
  r"""
632
  ## Utility functions
@@ -663,34 +724,32 @@ def _(mo):
663
  cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
664
  ) -> "Applicative[B]":
665
  '''
666
- The first computation produces values which are provided
667
- as input to the function(s) produced by the second computation.
668
  '''
669
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
670
  ```
671
 
672
  - `skip` sequences the effects of two Applicative computations, but **discards the result of the first**. For example, if `m1` and `m2` are instances of type `Maybe[Int]`, then `Maybe.skip(m1, m2)` is `Nothing` whenever either `m1` or `m2` is `Nothing`; but if not, it will have the same value as `m2`.
673
  - Likewise, `keep` sequences the effects of two computations, but **keeps only the result of the first**.
674
- - `revapp` is similar to `apply`, but where the first computation produces value(s) which are provided as input to the function(s) produced by the second computation.
675
  """
676
  )
677
- return
678
 
679
 
680
  @app.cell(hide_code=True)
681
- def _(mo):
682
  mo.md(
683
  r"""
684
- /// admonition | exercise
685
  Try to use utility functions with different instances
686
  ///
687
  """
688
  )
689
- return
690
 
691
 
692
  @app.cell(hide_code=True)
693
- def _(mo):
694
  mo.md(
695
  r"""
696
  # Formal implementation of Applicative
@@ -698,7 +757,6 @@ def _(mo):
698
  Now, we can give the formal implementation of `Applicative`
699
  """
700
  )
701
- return
702
 
703
 
704
  @app.cell
@@ -719,7 +777,8 @@ def _(
719
  @abstractmethod
720
  def pure(cls, a: A) -> "Applicative[A]":
721
  """Lift a value into the Structure."""
722
- return NotImplementedError
 
723
 
724
  @classmethod
725
  @abstractmethod
@@ -727,7 +786,8 @@ def _(
727
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
728
  ) -> "Applicative[B]":
729
  """Sequential application."""
730
- return NotImplementedError
 
731
 
732
  @classmethod
733
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
@@ -757,7 +817,7 @@ def _(
757
  return cls.pure([])
758
 
759
  return cls.apply(
760
- cls.fmap(lambda v: lambda vs: [v] + vs, fas[0]),
761
  cls.sequenceL(fas[1:]),
762
  )
763
 
@@ -792,21 +852,24 @@ def _(
792
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
793
 
794
  @classmethod
795
- def check_identity(cls, fa: "Applicative[A]"):
796
  if cls.lift(id, fa) != fa:
797
- raise ValueError("Instance violates identity law")
 
798
  return True
799
 
800
  @classmethod
801
- def check_homomorphism(cls, a: A, f: Callable[[A], B]):
802
  if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
803
- raise ValueError("Instance violates homomorphism law")
 
804
  return True
805
 
806
  @classmethod
807
- def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
808
  if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
809
- raise ValueError("Instance violates interchange law")
 
810
  return True
811
 
812
  @classmethod
@@ -815,15 +878,16 @@ def _(
815
  fg: "Applicative[Callable[[B], C]]",
816
  fh: "Applicative[Callable[[A], B]]",
817
  fa: "Applicative[A]",
818
- ):
819
  if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
820
- raise ValueError("Instance violates composition law")
 
821
  return True
822
  return (Applicative,)
823
 
824
 
825
  @app.cell(hide_code=True)
826
- def _(mo):
827
  mo.md(
828
  r"""
829
  # Effectful programming
@@ -833,11 +897,10 @@ def _(mo):
833
  The arguments are no longer just plain values but may also have effects, such as the possibility of failure, having many ways to succeed, or performing input/output actions. In this manner, applicative functors can also be viewed as abstracting the idea of **applying pure functions to effectful arguments**, with the precise form of effects that are permitted depending on the nature of the underlying functor.
834
  """
835
  )
836
- return
837
 
838
 
839
  @app.cell(hide_code=True)
840
- def _(mo):
841
  mo.md(
842
  r"""
843
  ## The IO Applicative
@@ -846,7 +909,7 @@ def _(mo):
846
 
847
  As before, we first abstract how `pure` and `apply` should function.
848
 
849
- - `pure` should wrap the object in an IO action, and make the object *callable* if it's not because we want to perform the action later:
850
 
851
  ```haskell
852
  IO.pure(1) => IO(effect=lambda: 1)
@@ -858,7 +921,6 @@ def _(mo):
858
  The implementation is:
859
  """
860
  )
861
- return
862
 
863
 
864
  @app.cell
@@ -881,34 +943,32 @@ def _(Applicative, Callable, dataclass):
881
 
882
 
883
  @app.cell(hide_code=True)
884
- def _(mo):
885
  mo.md(r"""For example, a function that reads a given number of lines from the keyboard can be defined in applicative style as follows:""")
886
- return
887
 
888
 
889
  @app.cell
890
  def _(IO):
891
  def get_chars(n: int = 3):
892
- return IO.sequenceL(
893
- [IO.pure(input(f"input the {i}th str")) for i in range(1, n + 1)]
894
- )
895
  return (get_chars,)
896
 
897
 
898
  @app.cell
899
- def _():
900
  # get_chars()()
901
  return
902
 
903
 
904
  @app.cell(hide_code=True)
905
- def _(mo):
906
  mo.md(r"""# From the perspective of category theory""")
907
- return
908
 
909
 
910
  @app.cell(hide_code=True)
911
- def _(mo):
912
  mo.md(
913
  r"""
914
  ## Lax Monoidal Functor
@@ -916,7 +976,6 @@ def _(mo):
916
  An alternative, equivalent formulation of `Applicative` is given by
917
  """
918
  )
919
- return
920
 
921
 
922
  @app.cell
@@ -938,10 +997,10 @@ def _(ABC, Functor, abstractmethod, dataclass):
938
 
939
 
940
  @app.cell(hide_code=True)
941
- def _(mo):
942
  mo.md(
943
  r"""
944
- Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
945
 
946
  - `unit` provides the identity element
947
  - `tensor` combines two contexts into a product context
@@ -949,14 +1008,13 @@ def _(mo):
949
  More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
950
  """
951
  )
952
- return
953
 
954
 
955
  @app.cell(hide_code=True)
956
- def _(mo):
957
  mo.md(
958
  r"""
959
- Furthermore, to deserve the name "monoidal", instances of Monoidal ought to satisfy the following laws, which seem much more straightforward than the traditional Applicative laws:
960
 
961
  - Left identity
962
 
@@ -971,11 +1029,10 @@ def _(mo):
971
  `tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
972
  """
973
  )
974
- return
975
 
976
 
977
  @app.cell(hide_code=True)
978
- def _(mo):
979
  mo.md(
980
  r"""
981
  /// admonition | ≅ indicates isomorphism
@@ -987,11 +1044,10 @@ def _(mo):
987
  ///
988
  """
989
  )
990
- return
991
 
992
 
993
  @app.cell(hide_code=True)
994
- def _(mo):
995
  mo.md(
996
  r"""
997
  ## Mutual definability of Monoidal and Applicative
@@ -1009,11 +1065,10 @@ def _(mo):
1009
  ```
1010
  """
1011
  )
1012
- return
1013
 
1014
 
1015
  @app.cell(hide_code=True)
1016
- def _(mo):
1017
  mo.md(
1018
  r"""
1019
  ## Instance: ListMonoidal
@@ -1029,7 +1084,6 @@ def _(mo):
1029
  The implementation is:
1030
  """
1031
  )
1032
- return
1033
 
1034
 
1035
  @app.cell
@@ -1057,9 +1111,8 @@ def _(B, Callable, Monoidal, dataclass, product):
1057
 
1058
 
1059
  @app.cell(hide_code=True)
1060
- def _(mo):
1061
  mo.md(r"""> try with `ListMonoidal` below""")
1062
- return
1063
 
1064
 
1065
  @app.cell
@@ -1071,15 +1124,13 @@ def _(ListMonoidal):
1071
 
1072
 
1073
  @app.cell(hide_code=True)
1074
- def _(mo):
1075
  mo.md(r"""and we can prove that `tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)`:""")
1076
- return
1077
 
1078
 
1079
  @app.cell
1080
- def _(List, xs, ys):
1081
  List.lift(lambda fa: lambda fb: (fa, fb), List(xs.items), List(ys.items))
1082
- return
1083
 
1084
 
1085
  @app.cell(hide_code=True)
@@ -1089,7 +1140,8 @@ def _(ABC, B, Callable, abstractmethod, dataclass):
1089
  @classmethod
1090
  @abstractmethod
1091
  def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
1092
- return NotImplementedError
 
1093
 
1094
  @classmethod
1095
  def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
@@ -1109,10 +1161,10 @@ def _():
1109
 
1110
  @app.cell(hide_code=True)
1111
  def _():
1112
- from dataclasses import dataclass
1113
  from abc import ABC, abstractmethod
1114
- from typing import TypeVar, Union
1115
  from collections.abc import Callable
 
 
1116
  return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
1117
 
1118
 
@@ -1131,7 +1183,309 @@ def _(TypeVar):
1131
 
1132
 
1133
  @app.cell(hide_code=True)
1134
- def _(mo):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
  mo.md(
1136
  r"""
1137
  # Further reading
@@ -1154,7 +1508,6 @@ def _(mo):
1154
  - [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
1155
  """
1156
  )
1157
- return
1158
 
1159
 
1160
  if __name__ == "__main__":
 
7
 
8
  import marimo
9
 
10
+ __generated_with = "0.12.9"
11
  app = marimo.App(app_title="Applicative programming with effects")
12
 
13
 
14
  @app.cell(hide_code=True)
15
+ def _(mo) -> None:
16
  mo.md(
17
  r"""
18
  # Applicative programming with effects
 
26
 
27
  In this notebook, you will learn:
28
 
29
+ 1. How to view `Applicative` as multi-functor intuitively.
30
  2. How to use `lift` to simplify chaining application.
31
  3. How to bring *effects* to the functional pure world.
32
+ 4. How to view `Applicative` as a lax monoidal functor.
33
+ 5. How to use `Alternative` to amalgamate multiple computations into a single computation.
34
 
35
  /// details | Notebook metadata
36
  type: info
37
 
38
+ version: 0.1.3 | last modified: 2025-04-16 | author: [métaboulie](https://github.com/metaboulie)<br/>
39
+ reviewer: [Haleshot](https://github.com/Haleshot)
40
 
41
  ///
42
  """
43
  )
 
44
 
45
 
46
  @app.cell(hide_code=True)
47
+ def _(mo) -> None:
48
  mo.md(
49
  r"""
50
  # The intuition: [Multifunctor](https://arxiv.org/pdf/2401.14286)
 
68
  And we have to declare a special version of the functor class for each case.
69
  """
70
  )
 
71
 
72
 
73
  @app.cell(hide_code=True)
74
+ def _(mo) -> None:
75
  mo.md(
76
  r"""
77
  ## Defining Multifunctor
78
 
79
  /// admonition
80
+ we use prefix `f` rather than `ap` to indicate *Applicative Functor*
81
  ///
82
 
83
  As a result, we may want to define a single `Multifunctor` such that:
 
111
  ```
112
  """
113
  )
 
114
 
115
 
116
  @app.cell(hide_code=True)
117
+ def _(mo) -> None:
118
  mo.md(
119
  r"""
120
  ## Pure, apply and lift
 
133
  # or if we have a regular function `g`
134
  g: Callable[[A], B]
135
  # then we can have `fg` as
136
+ fg: Applicative[Callable[[A], B]] = pure(g)
137
  ```
138
 
139
  2. `apply`: applies a function inside an applicative functor to a value inside an applicative functor
 
153
  ```
154
  """
155
  )
 
156
 
157
 
158
  @app.cell(hide_code=True)
159
+ def _(mo) -> None:
160
  mo.md(
161
  r"""
162
  /// admonition | How to use *Applicative* in the manner of *Multifunctor*
 
172
 
173
  ///
174
 
175
+ /// attention | You can suppress the chaining application of `apply` and `pure` as:
176
 
177
  ```python
178
  apply(pure(g), fa) -> lift(g, fa)
 
183
  ///
184
  """
185
  )
 
186
 
187
 
188
  @app.cell(hide_code=True)
189
+ def _(mo) -> None:
190
  mo.md(
191
  r"""
192
  ## Abstracting applicatives
 
199
  @classmethod
200
  @abstractmethod
201
  def pure(cls, a: A) -> "Applicative[A]":
202
+ raise NotImplementedError("Subclasses must implement pure")
203
 
204
  @classmethod
205
  @abstractmethod
206
  def apply(
207
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
208
  ) -> "Applicative[B]":
209
+ raise NotImplementedError("Subclasses must implement apply")
210
 
211
  @classmethod
212
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
 
225
  ///
226
  """
227
  )
 
228
 
229
 
230
  @app.cell(hide_code=True)
231
+ def _(mo) -> None:
232
  mo.md(r"""# Instances, laws and utility functions""")
 
233
 
234
 
235
  @app.cell(hide_code=True)
236
+ def _(mo) -> None:
237
  mo.md(
238
  r"""
239
  ## Applicative instances
 
244
  - apply a function inside the computation context to a value inside the computational context
245
  """
246
  )
 
247
 
248
 
249
  @app.cell(hide_code=True)
250
+ def _(mo) -> None:
251
  mo.md(
252
  r"""
253
+ ### The Wrapper Applicative
254
 
255
+ - `pure` should simply *wrap* an object, in the sense that:
256
 
257
  ```haskell
258
  Wrapper.pure(1) => Wrapper(value=1)
 
263
  The implementation is:
264
  """
265
  )
 
266
 
267
 
268
  @app.cell
 
284
 
285
 
286
  @app.cell(hide_code=True)
287
+ def _(mo) -> None:
288
  mo.md(r"""> try with Wrapper below""")
 
289
 
290
 
291
  @app.cell
292
+ def _(Wrapper) -> None:
293
  Wrapper.lift(
294
  lambda a: lambda b: lambda c: a + b * c,
295
  Wrapper(1),
296
  Wrapper(2),
297
  Wrapper(3),
298
  )
 
299
 
300
 
301
  @app.cell(hide_code=True)
302
+ def _(mo) -> None:
303
  mo.md(
304
  r"""
305
+ ### The List Applicative
306
 
307
  - `pure` should wrap the object in a list, in the sense that:
308
 
 
316
  The implementation is:
317
  """
318
  )
 
319
 
320
 
321
  @app.cell
 
335
 
336
 
337
  @app.cell(hide_code=True)
338
+ def _(mo) -> None:
339
  mo.md(r"""> try with List below""")
 
340
 
341
 
342
  @app.cell
343
+ def _(List) -> None:
344
  List.apply(
345
  List([lambda a: a + 1, lambda a: a * 2]),
346
  List([1, 2]),
347
  )
 
348
 
349
 
350
  @app.cell
351
+ def _(List) -> None:
352
  List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
 
353
 
354
 
355
  @app.cell(hide_code=True)
356
+ def _(mo) -> None:
357
  mo.md(
358
  r"""
359
+ ### The Maybe Applicative
360
 
361
  - `pure` should wrap the object in a Maybe, in the sense that:
362
 
 
372
  The implementation is:
373
  """
374
  )
 
375
 
376
 
377
  @app.cell
 
399
 
400
 
401
  @app.cell(hide_code=True)
402
+ def _(mo) -> None:
403
  mo.md(r"""> try with Maybe below""")
 
404
 
405
 
406
  @app.cell
407
+ def _(Maybe) -> None:
408
  Maybe.lift(
409
  lambda a: lambda b: a + b,
410
  Maybe(1),
411
  Maybe(2),
412
  )
 
413
 
414
 
415
  @app.cell
416
+ def _(Maybe) -> None:
417
  Maybe.lift(
418
  lambda a: lambda b: None,
419
  Maybe(1),
420
  Maybe(2),
421
  )
 
422
 
423
 
424
  @app.cell(hide_code=True)
425
+ def _(mo) -> None:
426
+ mo.md(
427
+ r"""
428
+ ### The Either Applicative
429
+
430
+ - `pure` should wrap the object in `Right`, in the sense that:
431
+
432
+ ```haskell
433
+ Either.pure(1) => Right(1)
434
+ ```
435
+
436
+ - `apply` should apply a function that is either on Left or Right to a value that is either on Left or Right
437
+ - if the function is `Left`, simply returns the `Left` of the function
438
+ - else `fmap` the `Right` of the function to the value
439
+
440
+ The implementation is:
441
+ """
442
+ )
443
+
444
+
445
+ @app.cell
446
+ def _(Applicative, B, Callable, Union, dataclass):
447
+ @dataclass
448
+ class Either[A](Applicative):
449
+ left: A = None
450
+ right: A = None
451
+
452
+ def __post_init__(self):
453
+ if (self.left is not None and self.right is not None) or (
454
+ self.left is None and self.right is None
455
+ ):
456
+ msg = "Provide either the value of the left or the value of the right."
457
+ raise TypeError(
458
+ msg
459
+ )
460
+
461
+ @classmethod
462
+ def pure(cls, a: A) -> "Either[A]":
463
+ return cls(right=a)
464
+
465
+ @classmethod
466
+ def apply(
467
+ cls, fg: "Either[Callable[[A], B]]", fa: "Either[A]"
468
+ ) -> "Either[B]":
469
+ if fg.left is not None:
470
+ return cls(left=fg.left)
471
+ return cls.fmap(fg.right, fa)
472
+
473
+ @classmethod
474
+ def fmap(
475
+ cls, g: Callable[[A], B], fa: "Either[A]"
476
+ ) -> Union["Either[A]", "Either[B]"]:
477
+ if fa.left is not None:
478
+ return cls(left=fa.left)
479
+ return cls(right=g(fa.right))
480
+
481
+ def __repr__(self):
482
+ if self.left is not None:
483
+ return f"Left({self.left!r})"
484
+ return f"Right({self.right!r})"
485
+ return (Either,)
486
+
487
+
488
+ @app.cell(hide_code=True)
489
+ def _(mo) -> None:
490
+ mo.md(r"""> try with `Either` below""")
491
+
492
+
493
+ @app.cell
494
+ def _(Either) -> None:
495
+ Either.apply(Either(left=TypeError("Parse Error")), Either(right=2))
496
+
497
+
498
+ @app.cell
499
+ def _(Either) -> None:
500
+ Either.apply(
501
+ Either(right=lambda x: x + 1), Either(left=TypeError("Parse Error"))
502
+ )
503
+
504
+
505
+ @app.cell
506
+ def _(Either) -> None:
507
+ Either.apply(Either(right=lambda x: x + 1), Either(right=1))
508
+
509
+
510
+ @app.cell(hide_code=True)
511
+ def _(mo) -> None:
512
  mo.md(
513
  r"""
514
  ## Collect the list of response with sequenceL
 
534
  Let's try `sequenceL` with the instances.
535
  """
536
  )
 
537
 
538
 
539
  @app.cell
540
+ def _(Wrapper) -> None:
541
  Wrapper.sequenceL([Wrapper(1), Wrapper(2), Wrapper(3)])
 
542
 
543
 
544
  @app.cell(hide_code=True)
545
+ def _(mo) -> None:
546
  mo.md(
547
  r"""
548
  /// attention
 
550
  ///
551
  """
552
  )
 
553
 
554
 
555
  @app.cell
556
+ def _(Maybe) -> None:
557
  Maybe.sequenceL([Maybe(1), Maybe(2), Maybe(None), Maybe(3)])
 
558
 
559
 
560
  @app.cell(hide_code=True)
561
+ def _(mo) -> None:
562
  mo.md(r"""The result of `sequenceL` for `List Applicative` is the Cartesian product of the input lists, yielding all possible ordered combinations of elements from each list.""")
 
563
 
564
 
565
  @app.cell
566
+ def _(List) -> None:
567
  List.sequenceL([List([1, 2]), List([3]), List([5, 6, 7])])
 
568
 
569
 
570
  @app.cell(hide_code=True)
571
+ def _(mo) -> None:
572
  mo.md(
573
  r"""
574
  ## Applicative laws
 
612
  ```
613
  This one is the trickiest law to gain intuition for. In some sense it is expressing a sort of associativity property of `apply`.
614
 
615
+ We can add 4 helper functions to `Applicative` to check whether an instance respects the laws or not:
616
 
617
  ```python
618
  @dataclass
 
651
  > Try to validate applicative laws below
652
  """
653
  )
 
654
 
655
 
656
  @app.cell
 
662
 
663
 
664
  @app.cell
665
+ def _(List, Wrapper) -> None:
666
  print("Checking Wrapper")
667
  print(Wrapper.check_identity(Wrapper.pure(1)))
668
  print(Wrapper.check_homomorphism(1, lambda x: x + 1))
 
684
  List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
685
  )
686
  )
 
687
 
688
 
689
  @app.cell(hide_code=True)
690
+ def _(mo) -> None:
691
  mo.md(
692
  r"""
693
  ## Utility functions
 
724
  cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
725
  ) -> "Applicative[B]":
726
  '''
727
+ The first computation produces values which are provided
728
+ as input to the function(s) produced by the second computation.
729
  '''
730
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
731
  ```
732
 
733
  - `skip` sequences the effects of two Applicative computations, but **discards the result of the first**. For example, if `m1` and `m2` are instances of type `Maybe[Int]`, then `Maybe.skip(m1, m2)` is `Nothing` whenever either `m1` or `m2` is `Nothing`; but if not, it will have the same value as `m2`.
734
  - Likewise, `keep` sequences the effects of two computations, but **keeps only the result of the first**.
735
+ - `revapp` is similar to `apply`, but where the first computation produces value(s) which are provided as input to the function(s) produced by the second computation.
736
  """
737
  )
 
738
 
739
 
740
  @app.cell(hide_code=True)
741
+ def _(mo) -> None:
742
  mo.md(
743
  r"""
744
+ /// admonition | Exercise
745
  Try to use utility functions with different instances
746
  ///
747
  """
748
  )
 
749
 
750
 
751
  @app.cell(hide_code=True)
752
+ def _(mo) -> None:
753
  mo.md(
754
  r"""
755
  # Formal implementation of Applicative
 
757
  Now, we can give the formal implementation of `Applicative`
758
  """
759
  )
 
760
 
761
 
762
  @app.cell
 
777
  @abstractmethod
778
  def pure(cls, a: A) -> "Applicative[A]":
779
  """Lift a value into the Structure."""
780
+ msg = "Subclasses must implement pure"
781
+ raise NotImplementedError(msg)
782
 
783
  @classmethod
784
  @abstractmethod
 
786
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
787
  ) -> "Applicative[B]":
788
  """Sequential application."""
789
+ msg = "Subclasses must implement apply"
790
+ raise NotImplementedError(msg)
791
 
792
  @classmethod
793
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
 
817
  return cls.pure([])
818
 
819
  return cls.apply(
820
+ cls.fmap(lambda v: lambda vs: [v, *vs], fas[0]),
821
  cls.sequenceL(fas[1:]),
822
  )
823
 
 
852
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
853
 
854
  @classmethod
855
+ def check_identity(cls, fa: "Applicative[A]") -> bool:
856
  if cls.lift(id, fa) != fa:
857
+ msg = "Instance violates identity law"
858
+ raise ValueError(msg)
859
  return True
860
 
861
  @classmethod
862
+ def check_homomorphism(cls, a: A, f: Callable[[A], B]) -> bool:
863
  if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
864
+ msg = "Instance violates homomorphism law"
865
+ raise ValueError(msg)
866
  return True
867
 
868
  @classmethod
869
+ def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]") -> bool:
870
  if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
871
+ msg = "Instance violates interchange law"
872
+ raise ValueError(msg)
873
  return True
874
 
875
  @classmethod
 
878
  fg: "Applicative[Callable[[B], C]]",
879
  fh: "Applicative[Callable[[A], B]]",
880
  fa: "Applicative[A]",
881
+ ) -> bool:
882
  if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
883
+ msg = "Instance violates composition law"
884
+ raise ValueError(msg)
885
  return True
886
  return (Applicative,)
887
 
888
 
889
  @app.cell(hide_code=True)
890
+ def _(mo) -> None:
891
  mo.md(
892
  r"""
893
  # Effectful programming
 
897
  The arguments are no longer just plain values but may also have effects, such as the possibility of failure, having many ways to succeed, or performing input/output actions. In this manner, applicative functors can also be viewed as abstracting the idea of **applying pure functions to effectful arguments**, with the precise form of effects that are permitted depending on the nature of the underlying functor.
898
  """
899
  )
 
900
 
901
 
902
  @app.cell(hide_code=True)
903
+ def _(mo) -> None:
904
  mo.md(
905
  r"""
906
  ## The IO Applicative
 
909
 
910
  As before, we first abstract how `pure` and `apply` should function.
911
 
912
+ - `pure` should wrap the object in an IO action, and make the object *callable* if it's not because we want to perform the action later:
913
 
914
  ```haskell
915
  IO.pure(1) => IO(effect=lambda: 1)
 
921
  The implementation is:
922
  """
923
  )
 
924
 
925
 
926
  @app.cell
 
943
 
944
 
945
  @app.cell(hide_code=True)
946
+ def _(mo) -> None:
947
  mo.md(r"""For example, a function that reads a given number of lines from the keyboard can be defined in applicative style as follows:""")
 
948
 
949
 
950
  @app.cell
951
  def _(IO):
952
  def get_chars(n: int = 3):
953
+ return IO.sequenceL([
954
+ IO.pure(input(f"input the {i}th str")) for i in range(1, n + 1)
955
+ ])
956
  return (get_chars,)
957
 
958
 
959
  @app.cell
960
+ def _() -> None:
961
  # get_chars()()
962
  return
963
 
964
 
965
  @app.cell(hide_code=True)
966
+ def _(mo) -> None:
967
  mo.md(r"""# From the perspective of category theory""")
 
968
 
969
 
970
  @app.cell(hide_code=True)
971
+ def _(mo) -> None:
972
  mo.md(
973
  r"""
974
  ## Lax Monoidal Functor
 
976
  An alternative, equivalent formulation of `Applicative` is given by
977
  """
978
  )
 
979
 
980
 
981
  @app.cell
 
997
 
998
 
999
  @app.cell(hide_code=True)
1000
+ def _(mo) -> None:
1001
  mo.md(
1002
  r"""
1003
+ Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
1004
 
1005
  - `unit` provides the identity element
1006
  - `tensor` combines two contexts into a product context
 
1008
  More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
1009
  """
1010
  )
 
1011
 
1012
 
1013
  @app.cell(hide_code=True)
1014
+ def _(mo) -> None:
1015
  mo.md(
1016
  r"""
1017
+ Furthermore, to deserve the name "monoidal", instances of Monoidal ought to satisfy the following laws, which seem much more straightforward than the traditional Applicative laws:
1018
 
1019
  - Left identity
1020
 
 
1029
  `tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
1030
  """
1031
  )
 
1032
 
1033
 
1034
  @app.cell(hide_code=True)
1035
+ def _(mo) -> None:
1036
  mo.md(
1037
  r"""
1038
  /// admonition | ≅ indicates isomorphism
 
1044
  ///
1045
  """
1046
  )
 
1047
 
1048
 
1049
  @app.cell(hide_code=True)
1050
+ def _(mo) -> None:
1051
  mo.md(
1052
  r"""
1053
  ## Mutual definability of Monoidal and Applicative
 
1065
  ```
1066
  """
1067
  )
 
1068
 
1069
 
1070
  @app.cell(hide_code=True)
1071
+ def _(mo) -> None:
1072
  mo.md(
1073
  r"""
1074
  ## Instance: ListMonoidal
 
1084
  The implementation is:
1085
  """
1086
  )
 
1087
 
1088
 
1089
  @app.cell
 
1111
 
1112
 
1113
  @app.cell(hide_code=True)
1114
+ def _(mo) -> None:
1115
  mo.md(r"""> try with `ListMonoidal` below""")
 
1116
 
1117
 
1118
  @app.cell
 
1124
 
1125
 
1126
  @app.cell(hide_code=True)
1127
+ def _(mo) -> None:
1128
  mo.md(r"""and we can prove that `tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)`:""")
 
1129
 
1130
 
1131
  @app.cell
1132
+ def _(List, xs, ys) -> None:
1133
  List.lift(lambda fa: lambda fb: (fa, fb), List(xs.items), List(ys.items))
 
1134
 
1135
 
1136
  @app.cell(hide_code=True)
 
1140
  @classmethod
1141
  @abstractmethod
1142
  def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
1143
+ msg = "Subclasses must implement fmap"
1144
+ raise NotImplementedError(msg)
1145
 
1146
  @classmethod
1147
  def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
 
1161
 
1162
  @app.cell(hide_code=True)
1163
  def _():
 
1164
  from abc import ABC, abstractmethod
 
1165
  from collections.abc import Callable
1166
+ from dataclasses import dataclass
1167
+ from typing import TypeVar, Union
1168
  return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
1169
 
1170
 
 
1183
 
1184
 
1185
  @app.cell(hide_code=True)
1186
+ def _(mo) -> None:
1187
+ mo.md(
1188
+ r"""
1189
+ # From Applicative to Alternative
1190
+
1191
+ ## Abstracting Alternative
1192
+
1193
+ In our studies so far, we saw that both `Maybe` and `List` can represent computations with a varying number of results.
1194
+
1195
+ We use `Maybe` to indicate a computation can fail somehow and `List` for computations that can have many possible results. In both of these cases, one useful operation is amalgamating all possible results from multiple computations into a single computation.
1196
+
1197
+ `Alternative` formalizes computations that support:
1198
+
1199
+ - **Failure** (empty result)
1200
+ - **Choice** (combination of results)
1201
+ - **Repetition** (multiple results)
1202
+
1203
+ It extends `Applicative` with monoidal structure, where:
1204
+
1205
+ ```python
1206
+ @dataclass
1207
+ class Alternative[A](Applicative, ABC):
1208
+ @classmethod
1209
+ @abstractmethod
1210
+ def empty(cls) -> "Alternative[A]":
1211
+ '''Identity element for alternative computations'''
1212
+
1213
+ @classmethod
1214
+ @abstractmethod
1215
+ def alt(
1216
+ cls, fa: "Alternative[A]", fb: "Alternative[A]"
1217
+ ) -> "Alternative[A]":
1218
+ '''Binary operation combining computations'''
1219
+ ```
1220
+
1221
+ - `empty` is the identity element (e.g., `Maybe(None)`, `List([])`)
1222
+ - `alt` is a combination operator (e.g., `Maybe` fallback, list concatenation)
1223
+
1224
+ `empty` and `alt` should satisfy the following **laws**:
1225
+
1226
+ ```python
1227
+ # Left identity
1228
+ alt(empty, fa) == fa
1229
+ # Right identity
1230
+ alt(fa, empty) == fa
1231
+ # Associativity
1232
+ alt(fa, alt(fb, fc)) == alt(alt(fa, fb), fc)
1233
+ ```
1234
+
1235
+ /// admonition
1236
+ Actually, `Alternative` is a *monoid* on `Applicative Functors`. We will talk about *monoid* and review these laws in the next notebook about `Monads`.
1237
+ ///
1238
+
1239
+ /// attention | minimal implementation requirement
1240
+ - `empty`
1241
+ - `alt`
1242
+ ///
1243
+ """
1244
+ )
1245
+
1246
+
1247
+ @app.cell(hide_code=True)
1248
+ def _(mo) -> None:
1249
+ mo.md(
1250
+ r"""
1251
+ ## Instances of Alternative
1252
+
1253
+ ### The Maybe Alternative
1254
+
1255
+ - `empty`: the identity element of `Maybe` is `Maybe(None)`
1256
+ - `alt`: return the first element if it's not `None`, else return the second element
1257
+ """
1258
+ )
1259
+
1260
+
1261
+ @app.cell
1262
+ def _(Alternative, Maybe, dataclass):
1263
+ @dataclass
1264
+ class AltMaybe[A](Maybe, Alternative):
1265
+ @classmethod
1266
+ def empty(cls) -> "AltMaybe[A]":
1267
+ return cls(None)
1268
+
1269
+ @classmethod
1270
+ def alt(cls, fa: "AltMaybe[A]", fb: "AltMaybe[A]") -> "AltMaybe[A]":
1271
+ if fa.value is not None:
1272
+ return cls(fa.value)
1273
+ return cls(fb.value)
1274
+
1275
+ def __repr__(self):
1276
+ return "Nothing" if self.value is None else f"Just({self.value!r})"
1277
+ return (AltMaybe,)
1278
+
1279
+
1280
+ @app.cell
1281
+ def _(AltMaybe) -> None:
1282
+ print(AltMaybe.empty())
1283
+ print(AltMaybe.alt(AltMaybe(None), AltMaybe(1)))
1284
+ print(AltMaybe.alt(AltMaybe(None), AltMaybe(None)))
1285
+ print(AltMaybe.alt(AltMaybe(1), AltMaybe(None)))
1286
+ print(AltMaybe.alt(AltMaybe(1), AltMaybe(2)))
1287
+
1288
+
1289
+ @app.cell
1290
+ def _(AltMaybe) -> None:
1291
+ print(AltMaybe.check_left_identity(AltMaybe(1)))
1292
+ print(AltMaybe.check_right_identity(AltMaybe(1)))
1293
+ print(AltMaybe.check_associativity(AltMaybe(1), AltMaybe(2), AltMaybe(None)))
1294
+
1295
+
1296
+ @app.cell(hide_code=True)
1297
+ def _(mo) -> None:
1298
+ mo.md(
1299
+ r"""
1300
+ ### The List Alternative
1301
+
1302
+ - `empty`: the identity element of `List` is `List([])`
1303
+ - `alt`: return the concatenation of 2 input lists
1304
+ """
1305
+ )
1306
+
1307
+
1308
+ @app.cell
1309
+ def _(Alternative, List, dataclass):
1310
+ @dataclass
1311
+ class AltList[A](List, Alternative):
1312
+ @classmethod
1313
+ def empty(cls) -> "AltList[A]":
1314
+ return cls([])
1315
+
1316
+ @classmethod
1317
+ def alt(cls, fa: "AltList[A]", fb: "AltList[A]") -> "AltList[A]":
1318
+ return cls(fa.value + fb.value)
1319
+ return (AltList,)
1320
+
1321
+
1322
+ @app.cell
1323
+ def _(AltList) -> None:
1324
+ print(AltList.empty())
1325
+ print(AltList.alt(AltList([1, 2, 3]), AltList([4, 5])))
1326
+
1327
+
1328
+ @app.cell
1329
+ def _(AltList) -> None:
1330
+ AltList([1])
1331
+
1332
+
1333
+ @app.cell
1334
+ def _(AltList) -> None:
1335
+ AltList([1])
1336
+
1337
+
1338
+ @app.cell
1339
+ def _(AltList) -> None:
1340
+ print(AltList.check_left_identity(AltList([1, 2, 3])))
1341
+ print(AltList.check_right_identity(AltList([1, 2, 3])))
1342
+ print(
1343
+ AltList.check_associativity(
1344
+ AltList([1, 2]), AltList([3, 4, 5]), AltList([6])
1345
+ )
1346
+ )
1347
+
1348
+
1349
+ @app.cell(hide_code=True)
1350
+ def _(mo) -> None:
1351
+ mo.md(
1352
+ r"""
1353
+ ## some and many
1354
+
1355
+
1356
+ /// admonition | This section mainly refers to
1357
+
1358
+ - https://stackoverflow.com/questions/7671009/some-and-many-functions-from-the-alternative-type-class/7681283#7681283
1359
+
1360
+ ///
1361
+
1362
+ First let's have a look at the implementation of `some` and `many`:
1363
+
1364
+ ```python
1365
+ @classmethod
1366
+ def some(cls, fa: "Alternative[A]") -> "Alternative[list[A]]":
1367
+ # Short-circuit if input is empty
1368
+ if fa == cls.empty():
1369
+ return cls.empty()
1370
+
1371
+ return cls.apply(
1372
+ cls.fmap(lambda a: lambda b: [a] + b, fa), cls.many(fa)
1373
+ )
1374
+
1375
+ @classmethod
1376
+ def many(cls, fa: "Alternative[A]") -> "Alternative[list[A]]":
1377
+ # Directly return empty list if input is empty
1378
+ if fa == cls.empty():
1379
+ return cls.pure([])
1380
+
1381
+ return cls.alt(cls.some(fa), cls.pure([]))
1382
+ ```
1383
+
1384
+ So `some f` runs `f` once, then *many* times, and conses the results. `many f` runs f *some* times, or *alternatively* just returns the empty list.
1385
+
1386
+ The idea is that they both run `f` as often as possible until it **fails**, collecting the results in a list. The difference is that `some f` immediately fails if `f` fails, while `many f` will still succeed and *return* the empty list in such a case. But what all this exactly means depends on how `alt` is defined.
1387
+
1388
+ Let's see what it does for the instances `AltMaybe` and `AltList`.
1389
+ """
1390
+ )
1391
+
1392
+
1393
+ @app.cell(hide_code=True)
1394
+ def _(mo) -> None:
1395
+ mo.md(r"""For `AltMaybe`. `None` means failure, so some `None` fails as well and evaluates to `None` while many `None` succeeds and evaluates to `Just []`. Both `some (Just ())` and `many (Just ())` never return, because `Just ()` never fails.""")
1396
+
1397
+
1398
+ @app.cell
1399
+ def _(AltMaybe) -> None:
1400
+ print(AltMaybe.some(AltMaybe.empty()))
1401
+ print(AltMaybe.many(AltMaybe.empty()))
1402
+
1403
+
1404
+ @app.cell(hide_code=True)
1405
+ def _(mo) -> None:
1406
+ mo.md(r"""For `AltList`, `[]` means failure, so `some []` evaluates to `[]` (no answers) while `many []` evaluates to `[[]]` (there's one answer and it is the empty list). Again `some [()]` and `many [()]` don't return.""")
1407
+
1408
+
1409
+ @app.cell
1410
+ def _(AltList) -> None:
1411
+ print(AltList.some(AltList.empty()))
1412
+ print(AltList.many(AltList.empty()))
1413
+
1414
+
1415
+ @app.cell(hide_code=True)
1416
+ def _(mo) -> None:
1417
+ mo.md(r"""## Formal implementation of Alternative""")
1418
+
1419
+
1420
+ @app.cell
1421
+ def _(ABC, Applicative, abstractmethod, dataclass):
1422
+ @dataclass
1423
+ class Alternative[A](Applicative, ABC):
1424
+ """A monoid on applicative functors."""
1425
+
1426
+ @classmethod
1427
+ @abstractmethod
1428
+ def empty(cls) -> "Alternative[A]":
1429
+ msg = "Subclasses must implement empty"
1430
+ raise NotImplementedError(msg)
1431
+
1432
+ @classmethod
1433
+ @abstractmethod
1434
+ def alt(
1435
+ cls, fa: "Alternative[A]", fb: "Alternative[A]"
1436
+ ) -> "Alternative[A]":
1437
+ msg = "Subclasses must implement alt"
1438
+ raise NotImplementedError(msg)
1439
+
1440
+ @classmethod
1441
+ def some(cls, fa: "Alternative[A]") -> "Alternative[list[A]]":
1442
+ # Short-circuit if input is empty
1443
+ if fa == cls.empty():
1444
+ return cls.empty()
1445
+
1446
+ return cls.apply(
1447
+ cls.fmap(lambda a: lambda b: [a, *b], fa), cls.many(fa)
1448
+ )
1449
+
1450
+ @classmethod
1451
+ def many(cls, fa: "Alternative[A]") -> "Alternative[list[A]]":
1452
+ # Directly return empty list if input is empty
1453
+ if fa == cls.empty():
1454
+ return cls.pure([])
1455
+
1456
+ return cls.alt(cls.some(fa), cls.pure([]))
1457
+
1458
+ @classmethod
1459
+ def check_left_identity(cls, fa: "Alternative[A]") -> bool:
1460
+ return cls.alt(cls.empty(), fa) == fa
1461
+
1462
+ @classmethod
1463
+ def check_right_identity(cls, fa: "Alternative[A]") -> bool:
1464
+ return cls.alt(fa, cls.empty()) == fa
1465
+
1466
+ @classmethod
1467
+ def check_associativity(
1468
+ cls, fa: "Alternative[A]", fb: "Alternative[A]", fc: "Alternative[A]"
1469
+ ) -> bool:
1470
+ return cls.alt(fa, cls.alt(fb, fc)) == cls.alt(cls.alt(fa, fb), fc)
1471
+ return (Alternative,)
1472
+
1473
+
1474
+ @app.cell(hide_code=True)
1475
+ def _(mo) -> None:
1476
+ mo.md(
1477
+ r"""
1478
+ /// admonition
1479
+
1480
+ We will explore more about `Alternative` in a future notebooks about [Monadic Parsing](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/monadic-parsing-in-haskell/E557DFCCE00E0D4B6ED02F3FB0466093)
1481
+
1482
+ ///
1483
+ """
1484
+ )
1485
+
1486
+
1487
+ @app.cell(hide_code=True)
1488
+ def _(mo) -> None:
1489
  mo.md(
1490
  r"""
1491
  # Further reading
 
1508
  - [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
1509
  """
1510
  )
 
1511
 
1512
 
1513
  if __name__ == "__main__":
functional_programming/CHANGELOG.md CHANGED
@@ -1,26 +1,90 @@
1
  # Changelog of the functional-programming course
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## 2025-04-07
4
 
5
- * the `apply` method of `Maybe` *Applicative* should return `None` when `fg` or `fa` is `None`
6
- + add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`, `Maybe`, `List`
7
- + add description for utility functions of `Applicative`
8
- * refine the implementation of `IO` *Applicative*
9
- * reimplement `get_chars` with `IO.sequenceL`
10
- + add an example to show that `ListMonoidal` is equivalent to `List` *Applicative*
 
 
 
 
 
 
 
11
 
12
  ## 2025-04-06
13
 
14
- - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be generically implemented
 
 
 
15
 
16
  ## 2025-04-02
17
 
18
- * `0.1.0` version of notebook `06_applicatives.py`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  ## 2025-03-16
21
 
22
- + Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`
23
- + Refactor the `Functor` class, changing `fmap` and utility methods to `classmethod`
 
 
24
 
25
  For example:
26
 
@@ -37,17 +101,24 @@
37
  Wrapper(value=2)
38
  ```
39
 
40
- + Move the `check_functor_law` method from `Functor` class to a standard function
 
41
  - Rename `ListWrapper` to `List` for simplicity
42
  - Remove the `Just` class
43
- + Rewrite proofs
 
44
 
45
  ## 2025-03-13
46
 
47
- * `0.1.0` version of notebook `05_functors`
48
 
49
- Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
 
 
 
50
 
51
  ## 2025-03-11
52
 
53
- * Demo version of notebook `05_functors.py`
 
 
 
1
  # Changelog of the functional-programming course
2
 
3
+ ## 2025-04-16
4
+
5
+ **applicatives.py**
6
+
7
+ - replace `return NotImplementedError` with `raise NotImplementedError`
8
+
9
+ - add `Either` applicative
10
+ - Add `Alternative`
11
+
12
+ ## 2025-04-11
13
+
14
+ **functors.py**
15
+
16
+ - add `Bifunctor` section
17
+
18
+ - replace `return NotImplementedError` with `raise NotImplementedError`
19
+
20
+ ## 2025-04-08
21
+
22
+ **functors.py**
23
+
24
+ - restructure the notebook
25
+ - replace `f` in the function signatures with `g` to indicate regular functions and
26
+ distinguish from functors
27
+ - move `Maybe` funtor to section `More Functor instances`
28
+
29
+ - add `Either` functor
30
+
31
+ - add `unzip` utility function for functors
32
+
33
  ## 2025-04-07
34
 
35
+ **applicatives.py**
36
+
37
+ - the `apply` method of `Maybe` _Applicative_ should return `None` when `fg` or `fa` is
38
+ `None`
39
+
40
+ - add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`,
41
+ `Maybe`, `List`
42
+ - add description for utility functions of `Applicative`
43
+
44
+ - refine the implementation of `IO` _Applicative_
45
+ - reimplement `get_chars` with `IO.sequenceL`
46
+
47
+ - add an example to show that `ListMonoidal` is equivalent to `List` _Applicative_
48
 
49
  ## 2025-04-06
50
 
51
+ **applicatives.py**
52
+
53
+ - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be
54
+ generically implemented
55
 
56
  ## 2025-04-02
57
 
58
+ **functors.py**
59
+
60
+ - Migrate to `python3.13`
61
+
62
+ - Replace all occurrences of
63
+
64
+ ```python
65
+ class Functor(Generic[A])
66
+ ```
67
+
68
+ with
69
+
70
+ ```python
71
+ class Functor[A]
72
+ ```
73
+
74
+ for conciseness
75
+
76
+ - Use `fa` in function signatures instead of `a` when `fa` is a _Functor_
77
+
78
+ **applicatives.py**
79
+
80
+ - `0.1.0` version of notebook `06_applicatives.py`
81
 
82
  ## 2025-03-16
83
 
84
+ **functors.py**
85
+
86
+ - Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`
87
+ - Refactor the `Functor` class, changing `fmap` and utility methods to `classmethod`
88
 
89
  For example:
90
 
 
101
  Wrapper(value=2)
102
  ```
103
 
104
+ - Move the `check_functor_law` method from `Functor` class to a standard function
105
+
106
  - Rename `ListWrapper` to `List` for simplicity
107
  - Remove the `Just` class
108
+
109
+ - Rewrite proofs
110
 
111
  ## 2025-03-13
112
 
113
+ **functors.py**
114
 
115
+ - `0.1.0` version of notebook `05_functors`
116
+
117
+ Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot)
118
+ for reviewing
119
 
120
  ## 2025-03-11
121
 
122
+ **functors.py**
123
+
124
+ - Demo version of notebook `05_functors.py`
functional_programming/README.md CHANGED
@@ -1,61 +1,72 @@
1
  # Learn Functional Programming
2
 
3
- _🚧 This collection is a
4
- [work in progress](https://github.com/marimo-team/learn/issues/51)._
5
 
6
  This series of marimo notebooks introduces the powerful paradigm of functional
7
- programming through Python. Taking inspiration from Haskell and Category Theory,
8
- we'll build a strong foundation in FP concepts that can transform how you
9
- approach software development.
10
 
11
  ## What You'll Learn
12
 
13
- **Using only Python's standard library**, we'll construct functional programming
14
- concepts from first principles.
15
 
16
  Topics include:
17
 
18
- - Recursion and higher-order functions
19
- - Category theory fundamentals
20
- - Functors, applicatives, and monads
21
- - Composable abstractions for robust code
22
 
23
- ## Timeline & Collaboration
24
 
25
- I'm currently studying functional programming and Haskell, estimating about 2
26
- months or even longer to complete this series. The structure may evolve as the
27
- project develops.
28
 
29
- If you're interested in collaborating or have questions, please reach out to me
30
- on Discord (@eugene.hs).
31
 
32
- **Running notebooks.** To run a notebook locally, use
33
-
34
- ```bash
35
- uvx marimo edit <URL>
36
  ```
37
 
38
  For example, run the `Functor` tutorial with
39
 
40
- ```bash
41
  uvx marimo edit https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py
42
  ```
43
 
44
- You can also open notebooks in our online playground by appending `marimo.app/`
45
- to a notebook's URL:
46
- [marimo.app/github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py](https://marimo.app/https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py).
 
 
 
 
 
 
47
 
48
- # Description of notebooks
 
 
 
 
 
49
 
50
  Check [here](https://github.com/marimo-team/learn/issues/51) for current series
51
- structure.
52
 
53
- | Notebook | Description | References |
54
- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
- | [05. Category and Functors](https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py) | Learn why `len` is a _Functor_ from `list concatenation` to `integer addition`, how to _lift_ an ordinary function into a _computation context_, and how to write an _adapter_ between two categories. | - [The Trivial Monad](http://blog.sigfpe.com/2007/04/trivial-monad.html) <br> - [Haskellwiki. Category Theory](https://en.wikibooks.org/wiki/Haskell/Category_theory) <br> - [Haskellforall. The Category Design Pattern](https://www.haskellforall.com/2012/08/the-category-design-pattern.html) <br> - [Haskellforall. The Functor Design Pattern](https://www.haskellforall.com/2012/09/the-functor-design-pattern.html) <br> - [Haskellwiki. Functor](https://wiki.haskell.org/index.php?title=Functor) <br> - [Haskellwiki. Typeclassopedia#Functor](https://wiki.haskell.org/index.php?title=Typeclassopedia#Functor) <br> - [Haskellwiki. Typeclassopedia#Category](https://wiki.haskell.org/index.php?title=Typeclassopedia#Category) |
 
56
 
57
  **Authors.**
58
 
59
  Thanks to all our notebook authors!
60
 
61
- - [métaboulie](https://github.com/metaboulie)
 
 
 
 
 
 
 
1
  # Learn Functional Programming
2
 
3
+ _🚧 This collection is a [work in progress](https://github.com/marimo-team/learn/issues/51)._
 
4
 
5
  This series of marimo notebooks introduces the powerful paradigm of functional
6
+ programming through Python. Taking inspiration from Haskell and Category
7
+ Theory, we'll build a strong foundation in FP concepts that can transform how
8
+ you approach software development.
9
 
10
  ## What You'll Learn
11
 
12
+ **Using only Python's standard library**, we'll construct functional
13
+ programming concepts from first principles.
14
 
15
  Topics include:
16
 
17
+ + Currying and higher-order functions
18
+ + Functors, Applicatives, and Monads
19
+ + Category theory fundamentals
 
20
 
21
+ ## Running Notebooks
22
 
23
+ ### Locally
 
 
24
 
25
+ To run a notebook locally, use
 
26
 
27
+ ```bash
28
+ uvx marimo edit <URL>
 
 
29
  ```
30
 
31
  For example, run the `Functor` tutorial with
32
 
33
+ ```bash
34
  uvx marimo edit https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py
35
  ```
36
 
37
+ ### On Our Online Playground
38
+
39
+ You can also open notebooks in our online playground by appending `marimo.app/` to a notebook's URL like:
40
+
41
+ https://marimo.app/https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py
42
+
43
+ ### On Our Landing Page
44
+
45
+ Open the notebooks in our landing page page [here](https://marimo-team.github.io/learn/functional_programming/05_functors.html)
46
 
47
+ ## Collaboration
48
+
49
+ If you're interested in collaborating or have questions, please reach out to me
50
+ on Discord (@eugene.hs).
51
+
52
+ ## Description of notebooks
53
 
54
  Check [here](https://github.com/marimo-team/learn/issues/51) for current series
55
+ structure.
56
 
57
+ | Notebook | Title | Key Concepts | Prerequisites |
58
+ |----------|-------|--------------|---------------|
59
+ | [05. Functors](https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py) | Category Theory and Functors | Category Theory, Functor, fmap, Bifunctor | Basic Python, Functions |
60
+ | [06. Applicatives](https://github.com/marimo-team/learn/blob/main/functional_programming/06_applicatives.py) | Applicative programming with effects | Applicative Functor, pure, apply, Effectful programming, Alternative | Functors |
61
 
62
  **Authors.**
63
 
64
  Thanks to all our notebook authors!
65
 
66
+ - [métaboulie](https://github.com/metaboulie)
67
+
68
+ **Reviewers.**
69
+
70
+ Thanks to all our notebook reviews!
71
+
72
+ - [Haleshot](https://github.com/Haleshot)