metaboulie commited on
Commit
e485eac
·
1 Parent(s): 48feff6

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

Browse files
functional_programming/06_applicatives.py CHANGED
@@ -29,12 +29,12 @@ def _(mo):
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.1 | last modified: 2025-04-06 | author: [métaboulie](https://github.com/metaboulie)<br/>
38
 
39
  ///
40
  """
@@ -76,12 +76,15 @@ def _(mo):
76
  r"""
77
  ## Defining Multifunctor
78
 
 
 
 
 
79
  As a result, we may want to define a single `Multifunctor` such that:
80
 
81
  1. Lift a regular n-argument function into the context of functors
82
 
83
  ```python
84
- # we use prefix `f` here to indicate `Functor`
85
  # lift a regular 3-argument function `g`
86
  g: Callable[[A, B, C], D]
87
  # into the context of functors
@@ -119,7 +122,7 @@ def _(mo):
119
 
120
  Traditionally, applicative functors are presented through two core operations:
121
 
122
- 1. `pure`: embeds an object (value or function) into the functor
123
 
124
  ```python
125
  # a -> F a
@@ -134,7 +137,7 @@ def _(mo):
134
  fg: Applicative[Callable[[A], B]] = pure(g)
135
  ```
136
 
137
- 2. `apply`: applies a function inside a functor to a value inside a functor
138
 
139
  ```python
140
  # F (a -> b) -> F a -> F b
@@ -174,9 +177,9 @@ def _(mo):
174
  /// attention | You can suppress the chaining application of `apply` and `pure` as:
175
 
176
  ```python
177
- apply(pure(f), fa) -> lift(f, fa)
178
- apply(apply(pure(f), fa), fb) -> lift(f, fa, fb)
179
- apply(apply(apply(pure(f), fa), fb), fc) -> lift(f, fa, fb, fc)
180
  ```
181
 
182
  ///
@@ -240,7 +243,7 @@ def _(mo):
240
  r"""
241
  ## Applicative instances
242
 
243
- When we are actually implementing an *Applicative* instance, we can keep in mind that `pure` and `apply` are fundamentally:
244
 
245
  - embed an object (value or function) to the computational context
246
  - apply a function inside the computation context to a value inside the computational context
@@ -261,7 +264,7 @@ def _(mo):
261
  Wrapper.pure(1) => Wrapper(value=1)
262
  ```
263
 
264
- - `apply` should apply a *wrapped* function to a *wrapper* value
265
 
266
  The implementation is:
267
  """
@@ -376,7 +379,7 @@ def _(mo):
376
  ```
377
 
378
  - `apply` should apply a function maybe exist to a value maybe exist
379
- - if the function is `None`, apply returns `None`
380
  - else apply the function to the value and wrap the result in `Just`
381
 
382
  The implementation is:
@@ -399,12 +402,13 @@ def _(Applicative, dataclass):
399
  def apply(
400
  cls, fg: "Maybe[Callable[[A], B]]", fa: "Maybe[A]"
401
  ) -> "Maybe[B]":
402
- if fg.value is None:
403
  return cls(None)
 
404
  return cls(fg.value(fa.value))
405
 
406
  def __repr__(self):
407
- return "Nothing" if self.value is None else repr(f"Just {self.value}")
408
  return (Maybe,)
409
 
410
 
@@ -434,12 +438,87 @@ def _(Maybe):
434
  return
435
 
436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  @app.cell(hide_code=True)
438
  def _(mo):
439
  mo.md(
440
  r"""
441
  ## Applicative laws
442
 
 
 
 
 
 
 
 
 
 
443
  Traditionally, there are four laws that `Applicative` instances should satisfy. In some sense, they are all concerned with making sure that `pure` deserves its name:
444
 
445
  - The identity law:
@@ -468,16 +547,7 @@ def _(mo):
468
  # fa: Applicative[A]
469
  apply(fg, apply(fh, fa)) = lift(compose, fg, fh, fa)
470
  ```
471
- This one is the trickiest law to gain intuition for. In some sense it is expressing a sort of associativity property of (<*>).
472
-
473
- /// admonition | id and compose
474
-
475
- Remember that
476
-
477
- - id = lambda x: x
478
- - compose = lambda f: lambda g: lambda x: f(g(x))
479
-
480
- ///
481
 
482
  We can add 4 helper functions to `Applicative` to check whether an instance respects the laws or not:
483
 
@@ -561,7 +631,7 @@ def _(mo):
561
  r"""
562
  ## Utility functions
563
 
564
- /// attention
565
  `fmap` is defined automatically using `pure` and `apply`, so you can use `fmap` with any `Applicative`
566
  ///
567
 
@@ -599,24 +669,14 @@ def _(mo):
599
  return cls.lift(lambda a: lambda f: f(a), fa, fg)
600
  ```
601
 
602
- We can have a sense of how `skip` and `keep` work by trying out sequencing the effects of `Maybe`, where one computation is `None`
 
 
603
  """
604
  )
605
  return
606
 
607
 
608
- @app.cell
609
- def _(Maybe):
610
- print("Maybe.skip")
611
- print(Maybe.skip(Maybe(1), Maybe(None)))
612
- print(Maybe.skip(Maybe(None), Maybe(1)))
613
-
614
- print("\nMaybe.keep")
615
- print(Maybe.keep(Maybe(1), Maybe(None)))
616
- print(Maybe.keep(Maybe(None), Maybe(1)))
617
- return
618
-
619
-
620
  @app.cell(hide_code=True)
621
  def _(mo):
622
  mo.md(
@@ -679,6 +739,7 @@ def _(
679
 
680
  for arg in args:
681
  curr = cls.apply(curr, arg)
 
682
  return curr
683
 
684
  @classmethod
@@ -687,6 +748,19 @@ def _(
687
  ) -> "Applicative[B]":
688
  return cls.lift(f, fa)
689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  @classmethod
691
  def skip(
692
  cls, fa: "Applicative[A]", fb: "Applicative[B]"
@@ -754,9 +828,9 @@ def _(mo):
754
  r"""
755
  # Effectful programming
756
 
757
- Our original motivation for applicatives was the desire the generalise the idea of mapping to functions with multiple arguments. This is a valid interpretation of the concept of applicatives, but from the three instances we have seen it becomes clear that there is also another, more abstract view.
758
 
759
- 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.
760
  """
761
  )
762
  return
@@ -779,7 +853,7 @@ def _(mo):
779
  IO.pure(f) => IO(effect=f)
780
  ```
781
 
782
- - `apply` should perform an action that produces a function and perform an action that produces a value, then call the function with the value
783
 
784
  The implementation is:
785
  """
@@ -798,13 +872,11 @@ def _(Applicative, Callable, dataclass):
798
 
799
  @classmethod
800
  def pure(cls, a):
801
- """Lift a value into the IO context"""
802
  return cls(a) if isinstance(a, Callable) else IO(lambda: a)
803
 
804
  @classmethod
805
- def apply(cls, f, a):
806
- """Applicative apply implementation"""
807
- return cls.pure(f()(a()))
808
  return (IO,)
809
 
810
 
@@ -817,19 +889,15 @@ def _(mo):
817
  @app.cell
818
  def _(IO):
819
  def get_chars(n: int = 3):
820
- if n <= 0:
821
- return ""
822
- return IO.lift(
823
- lambda: lambda s1: lambda: lambda s2: s1 + "\n" + s2,
824
- IO.pure(input("input line")),
825
- IO.pure(get_chars(n - 1)),
826
  )
827
  return (get_chars,)
828
 
829
 
830
  @app.cell
831
  def _():
832
- # print(get_chars()())
833
  return
834
 
835
 
@@ -932,12 +1000,12 @@ def _(mo):
932
 
933
  ```python
934
  pure(a) = fmap((lambda _: a), unit)
935
- apply(mf, mx) = fmap((lambda pair: pair[0](pair[1])), tensor(mf, mx))
936
  ```
937
 
938
  ```python
939
  unit() = pure(())
940
- tensor(fa, fb) = lift( ,fa, fb)
941
  ```
942
  """
943
  )
@@ -985,15 +1053,12 @@ def _(B, Callable, Monoidal, dataclass, product):
985
  cls, f: Callable[[A], B], ma: "ListMonoidal[A]"
986
  ) -> "ListMonoidal[B]":
987
  return cls([f(a) for a in ma.items])
988
-
989
- def __repr__(self):
990
- return repr(self.items)
991
  return (ListMonoidal,)
992
 
993
 
994
  @app.cell(hide_code=True)
995
  def _(mo):
996
- mo.md(r"""> try with Maybe below""")
997
  return
998
 
999
 
@@ -1005,6 +1070,18 @@ def _(ListMonoidal):
1005
  return xs, ys
1006
 
1007
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  @app.cell(hide_code=True)
1009
  def _(ABC, B, Callable, abstractmethod, dataclass):
1010
  @dataclass
 
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
  """
 
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:
84
 
85
  1. Lift a regular n-argument function into the context of functors
86
 
87
  ```python
 
88
  # lift a regular 3-argument function `g`
89
  g: Callable[[A, B, C], D]
90
  # into the context of functors
 
122
 
123
  Traditionally, applicative functors are presented through two core operations:
124
 
125
+ 1. `pure`: embeds an object (value or function) into the applicative functor
126
 
127
  ```python
128
  # a -> F a
 
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
141
 
142
  ```python
143
  # F (a -> b) -> F a -> F b
 
177
  /// attention | You can suppress the chaining application of `apply` and `pure` as:
178
 
179
  ```python
180
+ apply(pure(g), fa) -> lift(g, fa)
181
+ apply(apply(pure(g), fa), fb) -> lift(g, fa, fb)
182
+ apply(apply(apply(pure(g), fa), fb), fc) -> lift(g, fa, fb, fc)
183
  ```
184
 
185
  ///
 
243
  r"""
244
  ## Applicative instances
245
 
246
+ When we are actually implementing an *Applicative* instance, we can keep in mind that `pure` and `apply` fundamentally:
247
 
248
  - embed an object (value or function) to the computational context
249
  - apply a function inside the computation context to a value inside the computational context
 
264
  Wrapper.pure(1) => Wrapper(value=1)
265
  ```
266
 
267
+ - `apply` should apply a *wrapped* function to a *wrapped* value
268
 
269
  The implementation is:
270
  """
 
379
  ```
380
 
381
  - `apply` should apply a function maybe exist to a value maybe exist
382
+ - if the function is `None` or the value is `None`, simply returns `None`
383
  - else apply the function to the value and wrap the result in `Just`
384
 
385
  The implementation is:
 
402
  def apply(
403
  cls, fg: "Maybe[Callable[[A], B]]", fa: "Maybe[A]"
404
  ) -> "Maybe[B]":
405
+ if fg.value is None or fa.value is None:
406
  return cls(None)
407
+
408
  return cls(fg.value(fa.value))
409
 
410
  def __repr__(self):
411
+ return "Nothing" if self.value is None else f"Just({self.value!r})"
412
  return (Maybe,)
413
 
414
 
 
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
446
+
447
+ One often wants to execute a list of commands and collect the list of their response, and we can define a function `sequenceL` for this
448
+
449
+ /// admonition
450
+ In a further notebook about `Traversable`, we will have a more generic `sequence` that execute a **sequence** of commands and collect the **sequence** of their response, which is not limited to `list`.
451
+ ///
452
+
453
+ ```python
454
+ @classmethod
455
+ def sequenceL(cls, fas: list["Applicative[A]"]) -> "Applicative[list[A]]":
456
+ if not fas:
457
+ return cls.pure([])
458
+
459
+ return cls.apply(
460
+ cls.fmap(lambda v: lambda vs: [v] + vs, fas[0]),
461
+ cls.sequenceL(fas[1:]),
462
+ )
463
+ ```
464
+
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
482
+ For the `Maybe` Applicative, the presence of any `Nothing` causes the entire computation to return Nothing.
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
512
 
513
+ /// admonition | id and compose
514
+
515
+ Remember that
516
+
517
+ - `id = lambda x: x`
518
+ - `compose = lambda f: lambda g: lambda x: f(g(x))`
519
+
520
+ ///
521
+
522
  Traditionally, there are four laws that `Applicative` instances should satisfy. In some sense, they are all concerned with making sure that `pure` deserves its name:
523
 
524
  - The identity law:
 
547
  # fa: Applicative[A]
548
  apply(fg, apply(fh, fa)) = lift(compose, fg, fh, fa)
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
 
 
631
  r"""
632
  ## Utility functions
633
 
634
+ /// attention | using `fmap`
635
  `fmap` is defined automatically using `pure` and `apply`, so you can use `fmap` with any `Applicative`
636
  ///
637
 
 
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(
 
739
 
740
  for arg in args:
741
  curr = cls.apply(curr, arg)
742
+
743
  return curr
744
 
745
  @classmethod
 
748
  ) -> "Applicative[B]":
749
  return cls.lift(f, fa)
750
 
751
+ @classmethod
752
+ def sequenceL(cls, fas: list["Applicative[A]"]) -> "Applicative[list[A]]":
753
+ """
754
+ Execute a list of commands and collect the list of their response.
755
+ """
756
+ if not fas:
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
+
764
  @classmethod
765
  def skip(
766
  cls, fa: "Applicative[A]", fb: "Applicative[B]"
 
828
  r"""
829
  # Effectful programming
830
 
831
+ Our original motivation for applicatives was the desire to generalise the idea of mapping to functions with multiple arguments. This is a valid interpretation of the concept of applicatives, but from the three instances we have seen it becomes clear that there is also another, more abstract view.
832
 
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
 
853
  IO.pure(f) => IO(effect=f)
854
  ```
855
 
856
+ - `apply` should perform an action that produces a value, then apply the function with the value
857
 
858
  The implementation is:
859
  """
 
872
 
873
  @classmethod
874
  def pure(cls, a):
 
875
  return cls(a) if isinstance(a, Callable) else IO(lambda: a)
876
 
877
  @classmethod
878
+ def apply(cls, fg, fa):
879
+ return cls.pure(fg.effect(fa.effect()))
 
880
  return (IO,)
881
 
882
 
 
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
 
 
1000
 
1001
  ```python
1002
  pure(a) = fmap((lambda _: a), unit)
1003
+ apply(fg, fa) = fmap((lambda pair: pair[0](pair[1])), tensor(fg, fa))
1004
  ```
1005
 
1006
  ```python
1007
  unit() = pure(())
1008
+ tensor(fa, fb) = lift(lambda fa: lambda fb: (fa, fb), fa, fb)
1009
  ```
1010
  """
1011
  )
 
1053
  cls, f: Callable[[A], B], ma: "ListMonoidal[A]"
1054
  ) -> "ListMonoidal[B]":
1055
  return cls([f(a) for a in ma.items])
 
 
 
1056
  return (ListMonoidal,)
1057
 
1058
 
1059
  @app.cell(hide_code=True)
1060
  def _(mo):
1061
+ mo.md(r"""> try with `ListMonoidal` below""")
1062
  return
1063
 
1064
 
 
1070
  return xs, ys
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)
1086
  def _(ABC, B, Callable, abstractmethod, dataclass):
1087
  @dataclass
functional_programming/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
  # Changelog of the functional-programming course
2
 
 
 
 
 
 
 
 
 
 
3
  ## 2025-04-06
4
 
5
  - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be generically implemented
 
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