métaboulie commited on
Commit
3c962e6
·
1 Parent(s): 87e47b3

refactor(applicatives): `v0.1.3` of applicatives.py

Browse files
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,25 +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
  reviewer: [Haleshot](https://github.com/Haleshot)
39
 
40
  ///
41
  """
42
  )
43
- return
44
 
45
 
46
  @app.cell(hide_code=True)
47
- def _(mo):
48
  mo.md(
49
  r"""
50
  # The intuition: [Multifunctor](https://arxiv.org/pdf/2401.14286)
@@ -68,17 +68,16 @@ def _(mo):
68
  And we have to declare a special version of the functor class for each case.
69
  """
70
  )
71
- return
72
 
73
 
74
  @app.cell(hide_code=True)
75
- def _(mo):
76
  mo.md(
77
  r"""
78
  ## Defining Multifunctor
79
 
80
  /// admonition
81
- we use prefix `f` rather than `ap` to indicate *Applicative Functor*
82
  ///
83
 
84
  As a result, we may want to define a single `Multifunctor` such that:
@@ -112,11 +111,10 @@ def _(mo):
112
  ```
113
  """
114
  )
115
- return
116
 
117
 
118
  @app.cell(hide_code=True)
119
- def _(mo):
120
  mo.md(
121
  r"""
122
  ## Pure, apply and lift
@@ -135,7 +133,7 @@ def _(mo):
135
  # or if we have a regular function `g`
136
  g: Callable[[A], B]
137
  # then we can have `fg` as
138
- fg: Applicative[Callable[[A], B]] = pure(g)
139
  ```
140
 
141
  2. `apply`: applies a function inside an applicative functor to a value inside an applicative functor
@@ -155,11 +153,10 @@ def _(mo):
155
  ```
156
  """
157
  )
158
- return
159
 
160
 
161
  @app.cell(hide_code=True)
162
- def _(mo):
163
  mo.md(
164
  r"""
165
  /// admonition | How to use *Applicative* in the manner of *Multifunctor*
@@ -175,7 +172,7 @@ def _(mo):
175
 
176
  ///
177
 
178
- /// attention | You can suppress the chaining application of `apply` and `pure` as:
179
 
180
  ```python
181
  apply(pure(g), fa) -> lift(g, fa)
@@ -186,11 +183,10 @@ def _(mo):
186
  ///
187
  """
188
  )
189
- return
190
 
191
 
192
  @app.cell(hide_code=True)
193
- def _(mo):
194
  mo.md(
195
  r"""
196
  ## Abstracting applicatives
@@ -203,14 +199,14 @@ def _(mo):
203
  @classmethod
204
  @abstractmethod
205
  def pure(cls, a: A) -> "Applicative[A]":
206
- return NotImplementedError
207
 
208
  @classmethod
209
  @abstractmethod
210
  def apply(
211
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
212
  ) -> "Applicative[B]":
213
- return NotImplementedError
214
 
215
  @classmethod
216
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
@@ -229,17 +225,15 @@ def _(mo):
229
  ///
230
  """
231
  )
232
- return
233
 
234
 
235
  @app.cell(hide_code=True)
236
- def _(mo):
237
  mo.md(r"""# Instances, laws and utility functions""")
238
- return
239
 
240
 
241
  @app.cell(hide_code=True)
242
- def _(mo):
243
  mo.md(
244
  r"""
245
  ## Applicative instances
@@ -250,16 +244,15 @@ def _(mo):
250
  - apply a function inside the computation context to a value inside the computational context
251
  """
252
  )
253
- return
254
 
255
 
256
  @app.cell(hide_code=True)
257
- def _(mo):
258
  mo.md(
259
  r"""
260
- ### Wrapper
261
 
262
- - `pure` should simply *wrap* an object, in the sense that:
263
 
264
  ```haskell
265
  Wrapper.pure(1) => Wrapper(value=1)
@@ -270,7 +263,6 @@ def _(mo):
270
  The implementation is:
271
  """
272
  )
273
- return
274
 
275
 
276
  @app.cell
@@ -292,27 +284,25 @@ def _(Applicative, dataclass):
292
 
293
 
294
  @app.cell(hide_code=True)
295
- def _(mo):
296
  mo.md(r"""> try with Wrapper below""")
297
- return
298
 
299
 
300
  @app.cell
301
- def _(Wrapper):
302
  Wrapper.lift(
303
  lambda a: lambda b: lambda c: a + b * c,
304
  Wrapper(1),
305
  Wrapper(2),
306
  Wrapper(3),
307
  )
308
- return
309
 
310
 
311
  @app.cell(hide_code=True)
312
- def _(mo):
313
  mo.md(
314
  r"""
315
- ### List
316
 
317
  - `pure` should wrap the object in a list, in the sense that:
318
 
@@ -326,7 +316,6 @@ def _(mo):
326
  The implementation is:
327
  """
328
  )
329
- return
330
 
331
 
332
  @app.cell
@@ -346,31 +335,28 @@ def _(Applicative, dataclass, product):
346
 
347
 
348
  @app.cell(hide_code=True)
349
- def _(mo):
350
  mo.md(r"""> try with List below""")
351
- return
352
 
353
 
354
  @app.cell
355
- def _(List):
356
  List.apply(
357
  List([lambda a: a + 1, lambda a: a * 2]),
358
  List([1, 2]),
359
  )
360
- return
361
 
362
 
363
  @app.cell
364
- def _(List):
365
  List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
366
- return
367
 
368
 
369
  @app.cell(hide_code=True)
370
- def _(mo):
371
  mo.md(
372
  r"""
373
- ### Maybe
374
 
375
  - `pure` should wrap the object in a Maybe, in the sense that:
376
 
@@ -386,7 +372,6 @@ def _(mo):
386
  The implementation is:
387
  """
388
  )
389
- return
390
 
391
 
392
  @app.cell
@@ -414,33 +399,116 @@ def _(Applicative, dataclass):
414
 
415
 
416
  @app.cell(hide_code=True)
417
- def _(mo):
418
  mo.md(r"""> try with Maybe below""")
419
- return
420
 
421
 
422
  @app.cell
423
- def _(Maybe):
424
  Maybe.lift(
425
  lambda a: lambda b: a + b,
426
  Maybe(1),
427
  Maybe(2),
428
  )
429
- return
430
 
431
 
432
  @app.cell
433
- def _(Maybe):
434
  Maybe.lift(
435
  lambda a: lambda b: None,
436
  Maybe(1),
437
  Maybe(2),
438
  )
439
- return
440
 
441
 
442
  @app.cell(hide_code=True)
443
- def _(mo):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  mo.md(
445
  r"""
446
  ## Collect the list of response with sequenceL
@@ -466,17 +534,15 @@ def _(mo):
466
  Let's try `sequenceL` with the instances.
467
  """
468
  )
469
- return
470
 
471
 
472
  @app.cell
473
- def _(Wrapper):
474
  Wrapper.sequenceL([Wrapper(1), Wrapper(2), Wrapper(3)])
475
- return
476
 
477
 
478
  @app.cell(hide_code=True)
479
- def _(mo):
480
  mo.md(
481
  r"""
482
  /// attention
@@ -484,29 +550,25 @@ def _(mo):
484
  ///
485
  """
486
  )
487
- return
488
 
489
 
490
  @app.cell
491
- def _(Maybe):
492
  Maybe.sequenceL([Maybe(1), Maybe(2), Maybe(None), Maybe(3)])
493
- return
494
 
495
 
496
  @app.cell(hide_code=True)
497
- def _(mo):
498
  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.""")
499
- return
500
 
501
 
502
  @app.cell
503
- def _(List):
504
  List.sequenceL([List([1, 2]), List([3]), List([5, 6, 7])])
505
- return
506
 
507
 
508
  @app.cell(hide_code=True)
509
- def _(mo):
510
  mo.md(
511
  r"""
512
  ## Applicative laws
@@ -550,7 +612,7 @@ def _(mo):
550
  ```
551
  This one is the trickiest law to gain intuition for. In some sense it is expressing a sort of associativity property of `apply`.
552
 
553
- We can add 4 helper functions to `Applicative` to check whether an instance respects the laws or not:
554
 
555
  ```python
556
  @dataclass
@@ -589,7 +651,6 @@ def _(mo):
589
  > Try to validate applicative laws below
590
  """
591
  )
592
- return
593
 
594
 
595
  @app.cell
@@ -601,7 +662,7 @@ def _():
601
 
602
 
603
  @app.cell
604
- def _(List, Wrapper):
605
  print("Checking Wrapper")
606
  print(Wrapper.check_identity(Wrapper.pure(1)))
607
  print(Wrapper.check_homomorphism(1, lambda x: x + 1))
@@ -623,11 +684,10 @@ def _(List, Wrapper):
623
  List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
624
  )
625
  )
626
- return
627
 
628
 
629
  @app.cell(hide_code=True)
630
- def _(mo):
631
  mo.md(
632
  r"""
633
  ## Utility functions
@@ -664,22 +724,21 @@ def _(mo):
664
  cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
665
  ) -> "Applicative[B]":
666
  '''
667
- The first computation produces values which are provided
668
- as input to the function(s) produced by the second computation.
669
  '''
670
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
671
  ```
672
 
673
  - `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`.
674
  - Likewise, `keep` sequences the effects of two computations, but **keeps only the result of the first**.
675
- - `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.
676
  """
677
  )
678
- return
679
 
680
 
681
  @app.cell(hide_code=True)
682
- def _(mo):
683
  mo.md(
684
  r"""
685
  /// admonition | exercise
@@ -687,11 +746,10 @@ def _(mo):
687
  ///
688
  """
689
  )
690
- return
691
 
692
 
693
  @app.cell(hide_code=True)
694
- def _(mo):
695
  mo.md(
696
  r"""
697
  # Formal implementation of Applicative
@@ -699,7 +757,6 @@ def _(mo):
699
  Now, we can give the formal implementation of `Applicative`
700
  """
701
  )
702
- return
703
 
704
 
705
  @app.cell
@@ -720,7 +777,8 @@ def _(
720
  @abstractmethod
721
  def pure(cls, a: A) -> "Applicative[A]":
722
  """Lift a value into the Structure."""
723
- return NotImplementedError
 
724
 
725
  @classmethod
726
  @abstractmethod
@@ -728,7 +786,8 @@ def _(
728
  cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
729
  ) -> "Applicative[B]":
730
  """Sequential application."""
731
- return NotImplementedError
 
732
 
733
  @classmethod
734
  def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
@@ -758,7 +817,7 @@ def _(
758
  return cls.pure([])
759
 
760
  return cls.apply(
761
- cls.fmap(lambda v: lambda vs: [v] + vs, fas[0]),
762
  cls.sequenceL(fas[1:]),
763
  )
764
 
@@ -793,21 +852,24 @@ def _(
793
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
794
 
795
  @classmethod
796
- def check_identity(cls, fa: "Applicative[A]"):
797
  if cls.lift(id, fa) != fa:
798
- raise ValueError("Instance violates identity law")
 
799
  return True
800
 
801
  @classmethod
802
- def check_homomorphism(cls, a: A, f: Callable[[A], B]):
803
  if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
804
- raise ValueError("Instance violates homomorphism law")
 
805
  return True
806
 
807
  @classmethod
808
- def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
809
  if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
810
- raise ValueError("Instance violates interchange law")
 
811
  return True
812
 
813
  @classmethod
@@ -816,15 +878,16 @@ def _(
816
  fg: "Applicative[Callable[[B], C]]",
817
  fh: "Applicative[Callable[[A], B]]",
818
  fa: "Applicative[A]",
819
- ):
820
  if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
821
- raise ValueError("Instance violates composition law")
 
822
  return True
823
  return (Applicative,)
824
 
825
 
826
  @app.cell(hide_code=True)
827
- def _(mo):
828
  mo.md(
829
  r"""
830
  # Effectful programming
@@ -834,11 +897,10 @@ def _(mo):
834
  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.
835
  """
836
  )
837
- return
838
 
839
 
840
  @app.cell(hide_code=True)
841
- def _(mo):
842
  mo.md(
843
  r"""
844
  ## The IO Applicative
@@ -847,7 +909,7 @@ def _(mo):
847
 
848
  As before, we first abstract how `pure` and `apply` should function.
849
 
850
- - `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:
851
 
852
  ```haskell
853
  IO.pure(1) => IO(effect=lambda: 1)
@@ -859,7 +921,6 @@ def _(mo):
859
  The implementation is:
860
  """
861
  )
862
- return
863
 
864
 
865
  @app.cell
@@ -882,34 +943,32 @@ def _(Applicative, Callable, dataclass):
882
 
883
 
884
  @app.cell(hide_code=True)
885
- def _(mo):
886
  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:""")
887
- return
888
 
889
 
890
  @app.cell
891
  def _(IO):
892
  def get_chars(n: int = 3):
893
- return IO.sequenceL(
894
- [IO.pure(input(f"input the {i}th str")) for i in range(1, n + 1)]
895
- )
896
  return (get_chars,)
897
 
898
 
899
  @app.cell
900
- def _():
901
  # get_chars()()
902
  return
903
 
904
 
905
  @app.cell(hide_code=True)
906
- def _(mo):
907
  mo.md(r"""# From the perspective of category theory""")
908
- return
909
 
910
 
911
  @app.cell(hide_code=True)
912
- def _(mo):
913
  mo.md(
914
  r"""
915
  ## Lax Monoidal Functor
@@ -917,7 +976,6 @@ def _(mo):
917
  An alternative, equivalent formulation of `Applicative` is given by
918
  """
919
  )
920
- return
921
 
922
 
923
  @app.cell
@@ -939,10 +997,10 @@ def _(ABC, Functor, abstractmethod, dataclass):
939
 
940
 
941
  @app.cell(hide_code=True)
942
- def _(mo):
943
  mo.md(
944
  r"""
945
- Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
946
 
947
  - `unit` provides the identity element
948
  - `tensor` combines two contexts into a product context
@@ -950,14 +1008,13 @@ def _(mo):
950
  More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
951
  """
952
  )
953
- return
954
 
955
 
956
  @app.cell(hide_code=True)
957
- def _(mo):
958
  mo.md(
959
  r"""
960
- 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:
961
 
962
  - Left identity
963
 
@@ -972,11 +1029,10 @@ def _(mo):
972
  `tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
973
  """
974
  )
975
- return
976
 
977
 
978
  @app.cell(hide_code=True)
979
- def _(mo):
980
  mo.md(
981
  r"""
982
  /// admonition | ≅ indicates isomorphism
@@ -988,11 +1044,10 @@ def _(mo):
988
  ///
989
  """
990
  )
991
- return
992
 
993
 
994
  @app.cell(hide_code=True)
995
- def _(mo):
996
  mo.md(
997
  r"""
998
  ## Mutual definability of Monoidal and Applicative
@@ -1010,11 +1065,10 @@ def _(mo):
1010
  ```
1011
  """
1012
  )
1013
- return
1014
 
1015
 
1016
  @app.cell(hide_code=True)
1017
- def _(mo):
1018
  mo.md(
1019
  r"""
1020
  ## Instance: ListMonoidal
@@ -1030,7 +1084,6 @@ def _(mo):
1030
  The implementation is:
1031
  """
1032
  )
1033
- return
1034
 
1035
 
1036
  @app.cell
@@ -1058,9 +1111,8 @@ def _(B, Callable, Monoidal, dataclass, product):
1058
 
1059
 
1060
  @app.cell(hide_code=True)
1061
- def _(mo):
1062
  mo.md(r"""> try with `ListMonoidal` below""")
1063
- return
1064
 
1065
 
1066
  @app.cell
@@ -1072,15 +1124,13 @@ def _(ListMonoidal):
1072
 
1073
 
1074
  @app.cell(hide_code=True)
1075
- def _(mo):
1076
  mo.md(r"""and we can prove that `tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)`:""")
1077
- return
1078
 
1079
 
1080
  @app.cell
1081
- def _(List, xs, ys):
1082
  List.lift(lambda fa: lambda fb: (fa, fb), List(xs.items), List(ys.items))
1083
- return
1084
 
1085
 
1086
  @app.cell(hide_code=True)
@@ -1090,7 +1140,8 @@ def _(ABC, B, Callable, abstractmethod, dataclass):
1090
  @classmethod
1091
  @abstractmethod
1092
  def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
1093
- return NotImplementedError
 
1094
 
1095
  @classmethod
1096
  def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
@@ -1110,10 +1161,10 @@ def _():
1110
 
1111
  @app.cell(hide_code=True)
1112
  def _():
1113
- from dataclasses import dataclass
1114
  from abc import ABC, abstractmethod
1115
- from typing import TypeVar, Union
1116
  from collections.abc import Callable
 
 
1117
  return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
1118
 
1119
 
@@ -1132,7 +1183,309 @@ def _(TypeVar):
1132
 
1133
 
1134
  @app.cell(hide_code=True)
1135
- def _(mo):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
  mo.md(
1137
  r"""
1138
  # Further reading
@@ -1155,7 +1508,6 @@ def _(mo):
1155
  - [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
1156
  """
1157
  )
1158
- return
1159
 
1160
 
1161
  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
 
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,47 +1,65 @@
1
  # Changelog of the functional-programming course
2
 
 
 
 
 
 
 
 
 
 
3
  ## 2025-04-11
4
 
5
  **functors.py**
6
 
7
- + add `Bifunctor` section
8
- * replace `return NotImplementedError` with `raise NotImplementedError`
 
9
 
10
  ## 2025-04-08
11
 
12
  **functors.py**
13
 
14
- * restructure the notebook
15
- * replace `f` in the function signatures with `g` to indicate regular functions and distinguish from functors
16
- * move `Maybe` funtor to section `More Functor instances`
17
- + add `Either` functor
18
- + add `unzip` utility function for functors
19
 
 
 
 
20
 
21
  ## 2025-04-07
22
 
23
  **applicatives.py**
24
 
25
- * the `apply` method of `Maybe` *Applicative* should return `None` when `fg` or `fa` is `None`
26
- + add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`, `Maybe`, `List`
27
- + add description for utility functions of `Applicative`
28
- * refine the implementation of `IO` *Applicative*
29
- * reimplement `get_chars` with `IO.sequenceL`
30
- + add an example to show that `ListMonoidal` is equivalent to `List` *Applicative*
 
 
 
 
 
31
 
32
  ## 2025-04-06
33
 
34
  **applicatives.py**
35
 
36
- - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be generically implemented
 
37
 
38
  ## 2025-04-02
39
 
40
  **functors.py**
41
 
42
- + Migrate to `python3.13`
43
 
44
- + Replace all occurrences of
45
 
46
  ```python
47
  class Functor(Generic[A])
@@ -55,18 +73,18 @@
55
 
56
  for conciseness
57
 
58
- + Use `fa` in function signatures instead of `a` when `fa` is a *Functor*
59
 
60
  **applicatives.py**
61
 
62
- * `0.1.0` version of notebook `06_applicatives.py`
63
 
64
  ## 2025-03-16
65
 
66
  **functors.py**
67
 
68
- + Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`
69
- + Refactor the `Functor` class, changing `fmap` and utility methods to `classmethod`
70
 
71
  For example:
72
 
@@ -83,21 +101,24 @@
83
  Wrapper(value=2)
84
  ```
85
 
86
- + Move the `check_functor_law` method from `Functor` class to a standard function
 
87
  - Rename `ListWrapper` to `List` for simplicity
88
  - Remove the `Just` class
89
- + Rewrite proofs
 
90
 
91
  ## 2025-03-13
92
 
93
  **functors.py**
94
 
95
- * `0.1.0` version of notebook `05_functors`
96
 
97
- Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
 
98
 
99
  ## 2025-03-11
100
 
101
  **functors.py**
102
 
103
- * 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])
 
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`