metaboulie commited on
Commit
e9e13d8
·
1 Parent(s): 48c286d

Add 0.1.0 version of notebook 06_applicatives for fp course

Browse files
functional_programming/06_applicatives.py ADDED
@@ -0,0 +1,1149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.12"
3
+ # dependencies = [
4
+ # "marimo",
5
+ # ]
6
+ # ///
7
+
8
+ import marimo
9
+
10
+ __generated_with = "0.12.0"
11
+ app = marimo.App(app_title="Applicative programming with effects")
12
+
13
+ @app.cell(hide_code=True)
14
+ def _(mo):
15
+ mo.md(
16
+ r"""
17
+ # Applicative programming with effects
18
+
19
+ `Applicative Functor` encapsulates certain sorts of *effectful* computations in a functionally pure way, and encourages an *applicative* programming style.
20
+
21
+ Applicative is a functor with application, providing operations to
22
+
23
+ + embed pure expressions (`pure`), and
24
+ + sequence computations and combine their results (`apply`).
25
+
26
+ In this notebook, you will learn:
27
+
28
+ 1. How to view `applicative` as multi-functor.
29
+ 2. How to use `lift` to simplify chaining application.
30
+ 3. How to bring *effects* to the functional pure world.
31
+ 4. How to view `applicative` as lax monoidal functor
32
+
33
+ /// details | Notebook metadata
34
+ type: info
35
+
36
+ version: 0.1.0 | last modified: 2025-04-02 | author: [métaboulie](https://github.com/metaboulie)<br/>
37
+
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)
50
+
51
+ ## Limitations of functor
52
+
53
+ Recall that functors abstract the idea of mapping a function over each element of a structure.
54
+
55
+ Suppose now that we wish to generalise this idea to allow functions with any number of arguments to be mapped, rather than being restricted to functions with a single argument. More precisely, suppose that we wish to define a hierarchy of `fmap` functions with the following types:
56
+
57
+ ```haskell
58
+ fmap0 :: a -> f a
59
+
60
+ fmap1 :: (a -> b) -> f a -> f b
61
+
62
+ fmap2 :: (a -> b -> c) -> f a -> f b -> f c
63
+
64
+ fmap3 :: (a -> b -> c -> d) -> f a -> f b -> f c -> f d
65
+ ```
66
+
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
+ 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
88
+ fg: Callable[[Functor[A], Functor[B], Functor[C]], Functor[D]]
89
+ ```
90
+
91
+ 3. Apply it to n functor-wrapped values
92
+
93
+ ```python
94
+ # fa: Functor[A], fb: Functor[B], fc: Functor[C]
95
+ fg(fa, fb, fc)
96
+ ```
97
+
98
+ 5. Get a single functor-wrapped result
99
+
100
+ ```python
101
+ fd: Functor[D]
102
+ ```
103
+
104
+ We will define a function `lift` such that
105
+
106
+ ```python
107
+ fd = lift(g, fa, fb, fc)
108
+ ```
109
+ """
110
+ )
111
+ return
112
+
113
+
114
+ @app.cell(hide_code=True)
115
+ def _(mo):
116
+ mo.md(
117
+ r"""
118
+ ## Pure, apply and lift
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
126
+ pure: Callable[[A], Applicative[A]]
127
+ # for example, if `a` is
128
+ a: A
129
+ # then we can have `fa` as
130
+ fa: Applicative[A] = pure(a)
131
+ # or if we have a regular function `g`
132
+ g: Callable[[A], B]
133
+ # then we can have `fg` as
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
141
+ apply: Callable[[Applicative[Callable[[A], B]], Applicative[A]], Applicative[B]]
142
+ # and we can have
143
+ fd = apply(apply(apply(fg, fa), fb), fc)
144
+ ```
145
+
146
+
147
+ As a result,
148
+
149
+ ```python
150
+ lift(g, fa, fb, fc) = apply(apply(apply(pure(g), fa), fb), fc)
151
+ ```
152
+ """
153
+ )
154
+ return
155
+
156
+
157
+ @app.cell(hide_code=True)
158
+ def _(mo):
159
+ mo.md(
160
+ r"""
161
+ /// admonition | How to use *Applicative* in the manner of *Multifunctor*
162
+
163
+ 1. Define `pure` and `apply` for an `Applicative` subclass
164
+
165
+ - We can define them much easier compared with `lift`.
166
+
167
+ 2. Use the `lift` method
168
+
169
+ - We can use it much more convenient compared with the combination of `pure` and `apply`.
170
+
171
+
172
+ ///
173
+
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
+ ///
183
+ """
184
+ )
185
+ return
186
+
187
+
188
+ @app.cell(hide_code=True)
189
+ def _(mo):
190
+ mo.md(
191
+ r"""
192
+ ## Abstracting applicatives
193
+
194
+ We can now provide an initial abstraction definition of applicatives:
195
+
196
+ ```python
197
+ @dataclass
198
+ class Applicative[A](Functor, ABC):
199
+ @classmethod
200
+ @abstractmethod
201
+ def pure(cls, a: A) -> "Applicative[A]":
202
+ return NotImplementedError
203
+
204
+ @classmethod
205
+ @abstractmethod
206
+ def apply(
207
+ cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
208
+ ) -> "Applicative[B]":
209
+ return NotImplementedError
210
+
211
+ @classmethod
212
+ def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
213
+ curr = cls.pure(f)
214
+ if not args:
215
+ return curr
216
+ for arg in args:
217
+ curr = cls.apply(curr, arg)
218
+ return curr
219
+ ```
220
+
221
+ /// attention | minimal implmentation requirement
222
+
223
+ - `pure`
224
+ - `apply`
225
+ ///
226
+ """
227
+ )
228
+ return
229
+
230
+
231
+ @app.cell(hide_code=True)
232
+ def _(mo):
233
+ mo.md(r"""# Instances, laws and utility functions""")
234
+ return
235
+
236
+
237
+ @app.cell(hide_code=True)
238
+ def _(mo):
239
+ mo.md(
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
247
+ """
248
+ )
249
+ return
250
+
251
+
252
+ @app.cell(hide_code=True)
253
+ def _(mo):
254
+ mo.md(
255
+ r"""
256
+ ### Wrapper
257
+
258
+ - `pure` should simply *wrap* an object, in the sense that:
259
+
260
+ ```haskell
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
+ """
268
+ )
269
+ return
270
+
271
+
272
+ @app.cell
273
+ def _(Applicative, dataclass):
274
+ @dataclass
275
+ class Wrapper[A](Applicative):
276
+ value: A
277
+
278
+ @classmethod
279
+ def pure(cls, a: A) -> "Wrapper[A]":
280
+ return cls(a)
281
+
282
+ @classmethod
283
+ def apply(
284
+ cls, fg: "Wrapper[Callable[[A], B]]", fa: "Wrapper[A]"
285
+ ) -> "Wrapper[B]":
286
+ return cls(fg.value(fa.value))
287
+ return (Wrapper,)
288
+
289
+
290
+ @app.cell(hide_code=True)
291
+ def _(mo):
292
+ mo.md(r"""> try with Wrapper below""")
293
+ return
294
+
295
+
296
+ @app.cell
297
+ def _(Wrapper):
298
+ Wrapper.lift(
299
+ lambda a: lambda b: lambda c: a + b * c,
300
+ Wrapper(1),
301
+ Wrapper(2),
302
+ Wrapper(3),
303
+ )
304
+ return
305
+
306
+
307
+ @app.cell(hide_code=True)
308
+ def _(mo):
309
+ mo.md(
310
+ r"""
311
+ ### List
312
+
313
+ - `pure` should wrap the object in a list, in the sense that:
314
+
315
+ ```haskell
316
+ List.pure(1) => List(value=[1])
317
+ ```
318
+
319
+ - `apply` should apply a list of functions to a list of values
320
+ - you can think of this as cartesian product, concating the result of applying every function to every value
321
+
322
+ The implementation is:
323
+ """
324
+ )
325
+ return
326
+
327
+
328
+ @app.cell
329
+ def _(Applicative, dataclass, product):
330
+ @dataclass
331
+ class List[A](Applicative):
332
+ value: list[A]
333
+
334
+ @classmethod
335
+ def pure(cls, a: A) -> "List[A]":
336
+ return cls([a])
337
+
338
+ @classmethod
339
+ def apply(cls, fg: "List[Callable[[A], B]]", fa: "List[A]") -> "List[B]":
340
+ return cls([g(a) for g, a in product(fg.value, fa.value)])
341
+ return (List,)
342
+
343
+
344
+ @app.cell(hide_code=True)
345
+ def _(mo):
346
+ mo.md(r"""> try with List below""")
347
+ return
348
+
349
+
350
+ @app.cell
351
+ def _(List):
352
+ List.apply(
353
+ List([lambda a: a + 1, lambda a: a * 2]),
354
+ List([1, 2]),
355
+ )
356
+ return
357
+
358
+
359
+ @app.cell
360
+ def _(List):
361
+ List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
362
+ return
363
+
364
+
365
+ @app.cell(hide_code=True)
366
+ def _(mo):
367
+ mo.md(
368
+ r"""
369
+ ### Maybe
370
+
371
+ - `pure` should wrap the object in a Maybe, in the sense that:
372
+
373
+ ```haskell
374
+ Maybe.pure(1) => "Just 1"
375
+ Maybe.pure(None) => "Nothing"
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:
383
+ """
384
+ )
385
+ return
386
+
387
+
388
+ @app.cell
389
+ def _(Applicative, dataclass):
390
+ @dataclass
391
+ class Maybe[A](Applicative):
392
+ value: None | A
393
+
394
+ @classmethod
395
+ def pure(cls, a: A) -> "Maybe[A]":
396
+ return cls(a)
397
+
398
+ @classmethod
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
+
411
+ @app.cell(hide_code=True)
412
+ def _(mo):
413
+ mo.md(r"""> try with Maybe below""")
414
+ return
415
+
416
+
417
+ @app.cell
418
+ def _(Maybe):
419
+ Maybe.lift(
420
+ lambda a: lambda b: a + b,
421
+ Maybe(1),
422
+ Maybe(2),
423
+ )
424
+ return
425
+
426
+
427
+ @app.cell
428
+ def _(Maybe):
429
+ Maybe.lift(
430
+ lambda a: lambda b: None,
431
+ Maybe(1),
432
+ Maybe(2),
433
+ )
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:
446
+ ```python
447
+ # fa: Applicative[A]
448
+ apply(pure(id), fa) = fa
449
+ ```
450
+ - Homomorphism:
451
+ ```python
452
+ # a: A
453
+ # g: Callable[[A], B]
454
+ apply(pure(g), pure(a)) = pure(g(a))
455
+ ```
456
+ Intuitively, applying a non-effectful function to a non-effectful argument in an effectful context is the same as just applying the function to the argument and then injecting the result into the context with pure.
457
+ - Interchange:
458
+ ```python
459
+ # a: A
460
+ # fg: Applicative[Callable[[A], B]]
461
+ apply(fg, pure(a)) = apply(pure(lambda g: g(a)), fg)
462
+ ```
463
+ Intuitively, this says that when evaluating the application of an effectful function to a pure argument, the order in which we evaluate the function and its argument doesn't matter.
464
+ - Composition:
465
+ ```python
466
+ # fg: Applicative[Callable[[B], C]]
467
+ # fh: Applicative[Callable[[A], B]]
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
+
484
+ ```python
485
+ @dataclass
486
+ class Applicative[A](Functor, ABC):
487
+
488
+ @classmethod
489
+ def check_identity(cls, fa: "Applicative[A]"):
490
+ if cls.lift(id, fa) != fa:
491
+ raise ValueError("Instance violates identity law")
492
+ return True
493
+
494
+ @classmethod
495
+ def check_homomorphism(cls, a: A, f: Callable[[A], B]):
496
+ if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
497
+ raise ValueError("Instance violates homomorphism law")
498
+ return True
499
+
500
+ @classmethod
501
+ def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
502
+ if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
503
+ raise ValueError("Instance violates interchange law")
504
+ return True
505
+
506
+ @classmethod
507
+ def check_composition(
508
+ cls,
509
+ fg: "Applicative[Callable[[B], C]]",
510
+ fh: "Applicative[Callable[[A], B]]",
511
+ fa: "Applicative[A]",
512
+ ):
513
+ if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
514
+ raise ValueError("Instance violates composition law")
515
+ return True
516
+ ```
517
+
518
+ > Try to validate applicative laws below
519
+ """
520
+ )
521
+ return
522
+
523
+
524
+ @app.cell
525
+ def _():
526
+ id = lambda x: x
527
+ compose = lambda f: lambda g: lambda x: f(g(x))
528
+ const = lambda a: lambda _: a
529
+ return compose, const, id
530
+
531
+
532
+ @app.cell
533
+ def _(List, Wrapper):
534
+ print("Checking Wrapper")
535
+ print(Wrapper.check_identity(Wrapper.pure(1)))
536
+ print(Wrapper.check_homomorphism(1, lambda x: x + 1))
537
+ print(Wrapper.check_interchange(1, Wrapper.pure(lambda x: x + 1)))
538
+ print(
539
+ Wrapper.check_composition(
540
+ Wrapper.pure(lambda x: x * 2),
541
+ Wrapper.pure(lambda x: x + 0.1),
542
+ Wrapper.pure(1),
543
+ )
544
+ )
545
+
546
+ print("\nChecking List")
547
+ print(List.check_identity(List.pure(1)))
548
+ print(List.check_homomorphism(1, lambda x: x + 1))
549
+ print(List.check_interchange(1, List.pure(lambda x: x + 1)))
550
+ print(
551
+ List.check_composition(
552
+ List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
553
+ )
554
+ )
555
+ return
556
+
557
+
558
+ @app.cell(hide_code=True)
559
+ def _(mo):
560
+ mo.md(
561
+ r"""
562
+ ## Utility functions
563
+
564
+ ```python
565
+ @dataclass
566
+ class Applicative[A](Functor, ABC):
567
+ @classmethod
568
+ def skip(
569
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
570
+ ) -> "Applicative[B]":
571
+ '''
572
+ Sequences the effects of two Applicative computations,
573
+ but discards the result of the first.
574
+ '''
575
+ return cls.apply(cls.const(fa, id), fb)
576
+
577
+ @classmethod
578
+ def keep(
579
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
580
+ ) -> "Applicative[B]":
581
+ '''
582
+ Sequences the effects of two Applicative computations,
583
+ but discard the result of the second.
584
+ '''
585
+ return cls.lift(const, fa, fb)
586
+
587
+ @classmethod
588
+ def revapp(
589
+ cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
590
+ ) -> "Applicative[B]":
591
+ '''
592
+ The first computation produces values which are provided
593
+ as input to the function(s) produced by the second computation.
594
+ '''
595
+ return cls.lift(lambda a: lambda f: f(a), fa, fg)
596
+ ```
597
+
598
+ We can have a sense of how `skip` and `keep` work by trying out sequencing the effects of `Maybe`, where one computation is `None`
599
+ """
600
+ )
601
+ return
602
+
603
+
604
+ @app.cell
605
+ def _(Maybe):
606
+ print("Maybe.skip")
607
+ print(Maybe.skip(Maybe(1), Maybe(None)))
608
+ print(Maybe.skip(Maybe(None), Maybe(1)))
609
+
610
+ print("\nMaybe.keep")
611
+ print(Maybe.keep(Maybe(1), Maybe(None)))
612
+ print(Maybe.keep(Maybe(None), Maybe(1)))
613
+ return
614
+
615
+
616
+ @app.cell(hide_code=True)
617
+ def _(mo):
618
+ mo.md(
619
+ r"""
620
+ /// admonition | exercise
621
+ Try to use utility functions with different instances
622
+ ///
623
+ """
624
+ )
625
+ return
626
+
627
+
628
+ @app.cell(hide_code=True)
629
+ def _(mo):
630
+ mo.md(
631
+ r"""
632
+ # Formal implementation of Applicative
633
+
634
+ Now, we can give the formal implementation of `Applicative`
635
+ """
636
+ )
637
+ return
638
+
639
+
640
+ @app.cell
641
+ def _(
642
+ ABC,
643
+ B,
644
+ Callable,
645
+ Functor,
646
+ abstractmethod,
647
+ compose,
648
+ const,
649
+ dataclass,
650
+ id,
651
+ ):
652
+ @dataclass
653
+ class Applicative[A](Functor, ABC):
654
+ @classmethod
655
+ @abstractmethod
656
+ def pure(cls, a: A) -> "Applicative[A]":
657
+ """Lift a value into the Structure."""
658
+ return NotImplementedError
659
+
660
+ @classmethod
661
+ @abstractmethod
662
+ def apply(
663
+ cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
664
+ ) -> "Applicative[B]":
665
+ """Sequential application."""
666
+ return NotImplementedError
667
+
668
+ @classmethod
669
+ def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
670
+ """Lift a function of arbitrary arity to work with values in applicative context."""
671
+ curr = cls.pure(f)
672
+
673
+ if not args:
674
+ return curr
675
+
676
+ for arg in args:
677
+ curr = cls.apply(curr, arg)
678
+ return curr
679
+
680
+ @classmethod
681
+ def fmap(
682
+ cls, f: Callable[[A], B], fa: "Applicative[A]"
683
+ ) -> "Applicative[B]":
684
+ return cls.lift(f, fa)
685
+
686
+ @classmethod
687
+ def skip(
688
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
689
+ ) -> "Applicative[B]":
690
+ """
691
+ Sequences the effects of two Applicative computations,
692
+ but discards the result of the first.
693
+ """
694
+ return cls.apply(cls.const(fa, id), fb)
695
+
696
+ @classmethod
697
+ def keep(
698
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
699
+ ) -> "Applicative[B]":
700
+ """
701
+ Sequences the effects of two Applicative computations,
702
+ but discard the result of the second.
703
+ """
704
+ return cls.lift(const, fa, fb)
705
+
706
+ @classmethod
707
+ def revapp(
708
+ cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
709
+ ) -> "Applicative[B]":
710
+ """
711
+ The first computation produces values which are provided
712
+ as input to the function(s) produced by the second computation.
713
+ """
714
+ return cls.lift(lambda a: lambda f: f(a), fa, fg)
715
+
716
+ @classmethod
717
+ def check_identity(cls, fa: "Applicative[A]"):
718
+ if cls.lift(id, fa) != fa:
719
+ raise ValueError("Instance violates identity law")
720
+ return True
721
+
722
+ @classmethod
723
+ def check_homomorphism(cls, a: A, f: Callable[[A], B]):
724
+ if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
725
+ raise ValueError("Instance violates homomorphism law")
726
+ return True
727
+
728
+ @classmethod
729
+ def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
730
+ if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
731
+ raise ValueError("Instance violates interchange law")
732
+ return True
733
+
734
+ @classmethod
735
+ def check_composition(
736
+ cls,
737
+ fg: "Applicative[Callable[[B], C]]",
738
+ fh: "Applicative[Callable[[A], B]]",
739
+ fa: "Applicative[A]",
740
+ ):
741
+ if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
742
+ raise ValueError("Instance violates composition law")
743
+ return True
744
+ return (Applicative,)
745
+
746
+
747
+ @app.cell(hide_code=True)
748
+ def _(mo):
749
+ mo.md(
750
+ r"""
751
+ # Effectful Programming
752
+
753
+ 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.
754
+
755
+ 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.
756
+ """
757
+ )
758
+ return
759
+
760
+
761
+ @app.cell(hide_code=True)
762
+ def _(mo):
763
+ mo.md(
764
+ r"""
765
+ ## The IO Applicative
766
+
767
+ We will try to define an `IO` applicative here.
768
+
769
+ As before, we first abstract how `pure` and `apply` should function.
770
+
771
+ - `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:
772
+
773
+ ```haskell
774
+ IO.pure(1) => IO(effect=lambda: 1)
775
+ IO.pure(f) => IO(effect=f)
776
+ ```
777
+
778
+ - `apply` should perform an action that produces a function and perform an action that produces a value, then call the function with the value
779
+
780
+ The implementation is:
781
+ """
782
+ )
783
+ return
784
+
785
+
786
+ @app.cell
787
+ def _(Applicative, Callable, dataclass):
788
+ @dataclass
789
+ class IO(Applicative):
790
+ effect: Callable
791
+
792
+ def __call__(self):
793
+ return self.effect()
794
+
795
+ @classmethod
796
+ def pure(cls, a):
797
+ """Lift a value into the IO context"""
798
+ return cls(a) if isinstance(a, Callable) else IO(lambda: a)
799
+
800
+ @classmethod
801
+ def apply(cls, f, a):
802
+ """Applicative apply implementation"""
803
+ return cls.pure(f()(a()))
804
+ return (IO,)
805
+
806
+
807
+ @app.cell(hide_code=True)
808
+ def _(mo):
809
+ 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:""")
810
+ return
811
+
812
+
813
+ @app.cell
814
+ def _(IO):
815
+ def get_chars(n: int = 3):
816
+ if n <= 0:
817
+ return ""
818
+ return IO.lift(
819
+ lambda: lambda s1: lambda: lambda s2: s1 + "\n" + s2,
820
+ IO.pure(input(f"input line")),
821
+ IO.pure(get_chars(n - 1)),
822
+ )
823
+ return (get_chars,)
824
+
825
+
826
+ @app.cell
827
+ def _():
828
+ # print(get_chars()())
829
+ return
830
+
831
+
832
+ @app.cell(hide_code=True)
833
+ def _(mo):
834
+ mo.md(r"""## Collect the sequence of response with sequenceL""")
835
+ return
836
+
837
+
838
+ @app.cell(hide_code=True)
839
+ def _(mo):
840
+ mo.md(
841
+ r"""
842
+ One often wants to execute a sequence of commands and collect the sequence of their response, and we can define a function `sequenceL` for this
843
+
844
+ /// admonition
845
+ sequenceL actually means that
846
+
847
+ > execute a list of commands and collect the list of their response
848
+ ///
849
+ """
850
+ )
851
+ return
852
+
853
+
854
+ @app.cell
855
+ def _(A, Applicative):
856
+ def sequenceL(actions: list[Applicative[A]], ap) -> Applicative[list[A]]:
857
+ if not actions:
858
+ return ap.pure([])
859
+
860
+ return ap.lift(
861
+ lambda: lambda l1: lambda: lambda l2: list(l1) + list(l2),
862
+ actions[0],
863
+ sequenceL(actions[1:], ap),
864
+ )
865
+ return (sequenceL,)
866
+
867
+
868
+ @app.cell(hide_code=True)
869
+ def _(mo):
870
+ mo.md(
871
+ r"""
872
+ This function transforms a list of applicative actions into a single such action that returns a list of result values, and captures a common pattern of applicative programming.
873
+
874
+ And we can rewrite the `get_chars` more concisely:
875
+ """
876
+ )
877
+ return
878
+
879
+
880
+ @app.cell
881
+ def _(IO, sequenceL):
882
+ def get_chars_sequenceL(n: int = 3):
883
+ return sequenceL(
884
+ [IO.pure(input(f"input the {i}nd str") for i in range(1, n + 1))], IO
885
+ )
886
+ return (get_chars_sequenceL,)
887
+
888
+
889
+ @app.cell
890
+ def _():
891
+ # print(get_chars_sequenceL()())
892
+ return
893
+
894
+
895
+ @app.cell(hide_code=True)
896
+ def _(mo):
897
+ mo.md(r"""# From the perspective of category theory""")
898
+ return
899
+
900
+
901
+ @app.cell(hide_code=True)
902
+ def _(mo):
903
+ mo.md(
904
+ r"""
905
+ ## Lax Monoidal Functor
906
+
907
+ An alternative, equivalent formulation of `Applicative` is given by
908
+ """
909
+ )
910
+ return
911
+
912
+
913
+ @app.cell
914
+ def _(ABC, Functor, abstractmethod, dataclass):
915
+ @dataclass
916
+ class Monoidal[A](Functor, ABC):
917
+ @classmethod
918
+ @abstractmethod
919
+ def unit(cls) -> "Monoidal[Tuple[()]]":
920
+ pass
921
+
922
+ @classmethod
923
+ @abstractmethod
924
+ def tensor(
925
+ cls, this: "Monoidal[A]", other: "Monoidal[B]"
926
+ ) -> "Monoidal[Tuple[A, B]]":
927
+ pass
928
+ return (Monoidal,)
929
+
930
+
931
+ @app.cell
932
+ def _(mo):
933
+ mo.md(r"""fmap g fa = fmap (g . snd) (unit ** fa)""")
934
+ return
935
+
936
+
937
+ @app.cell(hide_code=True)
938
+ def _(mo):
939
+ mo.md(
940
+ r"""
941
+ Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
942
+
943
+ - `unit` provides the identity element
944
+ - `tensor` combines two contexts into a product context
945
+
946
+ More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
947
+ """
948
+ )
949
+ return
950
+
951
+
952
+ @app.cell(hide_code=True)
953
+ def _(mo):
954
+ mo.md(
955
+ r"""
956
+ 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:
957
+
958
+ - Left identity
959
+
960
+ `tensor(unit, v) ≅ v`
961
+
962
+ - Right identity
963
+
964
+ `tensor(u, unit) ≅ u`
965
+
966
+ - Associativity
967
+
968
+ `tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
969
+ """
970
+ )
971
+ return
972
+
973
+
974
+ @app.cell(hide_code=True)
975
+ def _(mo):
976
+ mo.md(
977
+ r"""
978
+ /// admonition | ≅ indicates isomorphism
979
+
980
+ `≅` refers to *isomorphism* rather than equality.
981
+
982
+ In particular we consider `(x, ()) ≅ x ≅ ((), x)` and `((x, y), z) ≅ (x, (y, z))`
983
+
984
+ ///
985
+ """
986
+ )
987
+ return
988
+
989
+
990
+ @app.cell(hide_code=True)
991
+ def _(mo):
992
+ mo.md(
993
+ r"""
994
+ ## Mutual Definability of Monoidal and Applicative
995
+
996
+ We can implement `pure` and `apply` in terms of `unit` and `tensor`, and vice versa.
997
+
998
+ ```python
999
+ pure(a) = fmap((lambda _: a), unit)
1000
+ apply(mf, mx) = fmap((lambda pair: pair[0](pair[1])), tensor(mf, mx))
1001
+ ```
1002
+
1003
+ ```python
1004
+ unit() = pure(())
1005
+ tensor(fa, fb) = lift( ,fa, fb)
1006
+ ```
1007
+ """
1008
+ )
1009
+ return
1010
+
1011
+
1012
+ @app.cell(hide_code=True)
1013
+ def _(mo):
1014
+ mo.md(
1015
+ r"""
1016
+ ## Instance: ListMonoidal
1017
+
1018
+ - `unit` should simply return a emtpy tuple wrapper in a list
1019
+
1020
+ ```haskell
1021
+ ListMonoidal.unit() => [()]
1022
+ ```
1023
+
1024
+ - `tensor` should return the *cartesian product* of the items of 2 ListMonoidal instances
1025
+
1026
+ The implementation is:
1027
+ """
1028
+ )
1029
+ return
1030
+
1031
+
1032
+ @app.cell
1033
+ def _(B, Callable, Monoidal, dataclass, product):
1034
+ @dataclass
1035
+ class ListMonoidal[A](Monoidal):
1036
+ items: list[A]
1037
+
1038
+ @classmethod
1039
+ def unit(cls) -> "ListMonoidal[Tuple[()]]":
1040
+ return cls([()])
1041
+
1042
+ @classmethod
1043
+ def tensor(
1044
+ cls, this: "ListMonoidal[A]", other: "ListMonoidal[B]"
1045
+ ) -> "ListMonoidal[Tuple[A, B]]":
1046
+ return cls(list(product(this.items, other.items)))
1047
+
1048
+ @classmethod
1049
+ def fmap(
1050
+ cls, f: Callable[[A], B], ma: "ListMonoidal[A]"
1051
+ ) -> "ListMonoidal[B]":
1052
+ return cls([f(a) for a in ma.items])
1053
+
1054
+ def __repr__(self):
1055
+ return repr(self.items)
1056
+ return (ListMonoidal,)
1057
+
1058
+
1059
+ @app.cell(hide_code=True)
1060
+ def _(mo):
1061
+ mo.md(r"""> try with Maybe below""")
1062
+ return
1063
+
1064
+
1065
+ @app.cell
1066
+ def _(ListMonoidal):
1067
+ xs = ListMonoidal([1, 2])
1068
+ ys = ListMonoidal(["a", "b"])
1069
+ ListMonoidal.tensor(xs, ys)
1070
+ return xs, ys
1071
+
1072
+
1073
+ @app.cell(hide_code=True)
1074
+ def _(ABC, B, Callable, abstractmethod, dataclass):
1075
+ @dataclass
1076
+ class Functor[A](ABC):
1077
+ @classmethod
1078
+ @abstractmethod
1079
+ def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
1080
+ return NotImplementedError
1081
+
1082
+ @classmethod
1083
+ def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
1084
+ return cls.fmap(lambda _: b, a)
1085
+
1086
+ @classmethod
1087
+ def void(cls, a: "Functor[A]") -> "Functor[None]":
1088
+ return cls.const_fmap(a, None)
1089
+ return (Functor,)
1090
+
1091
+
1092
+ @app.cell(hide_code=True)
1093
+ def _():
1094
+ import marimo as mo
1095
+ return (mo,)
1096
+
1097
+
1098
+ @app.cell(hide_code=True)
1099
+ def _():
1100
+ from dataclasses import dataclass
1101
+ from abc import ABC, abstractmethod
1102
+ from typing import TypeVar, Union
1103
+ from collections.abc import Callable
1104
+ return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
1105
+
1106
+
1107
+ @app.cell(hide_code=True)
1108
+ def _():
1109
+ from itertools import product
1110
+ return (product,)
1111
+
1112
+
1113
+ @app.cell(hide_code=True)
1114
+ def _(TypeVar):
1115
+ A = TypeVar("A")
1116
+ B = TypeVar("B")
1117
+ C = TypeVar("C")
1118
+ return A, B, C
1119
+
1120
+
1121
+ @app.cell(hide_code=True)
1122
+ def _(mo):
1123
+ mo.md(
1124
+ r"""
1125
+ # Further reading
1126
+
1127
+ Notice that these reading sources are optional and non-trivial
1128
+
1129
+ - [Applicaive Programming with Effects](https://www.staff.city.ac.uk/~ross/papers/Applicative.html)
1130
+ - [Equivalence of Applicative Functors and
1131
+ Multifunctors](https://arxiv.org/pdf/2401.14286)
1132
+ - [Applicative functor](https://wiki.haskell.org/index.php?title=Applicative_functor)
1133
+ - [Control.Applicative](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#t:Applicative)
1134
+ - [Typeclassopedia#Applicative](https://wiki.haskell.org/index.php?title=Typeclassopedia#Applicative)
1135
+ - [Notions of computation as monoids](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/notions-of-computation-as-monoids/70019FC0F2384270E9F41B9719042528)
1136
+ - [Free Applicative Functors](https://arxiv.org/abs/1403.0749)
1137
+ - [The basics of applicative functors, put to practical work](http://www.serpentine.com/blog/2008/02/06/the-basics-of-applicative-functors-put-to-practical-work/)
1138
+ - [Abstracting with Applicatives](http://comonad.com/reader/2012/abstracting-with-applicatives/)
1139
+ - [Static analysis with Applicatives](https://gergo.erdi.hu/blog/2012-12-01-static_analysis_with_applicatives/)
1140
+ - [Explaining Applicative functor in categorical terms - monoidal functors](https://cstheory.stackexchange.com/questions/12412/explaining-applicative-functor-in-categorical-terms-monoidal-functors)
1141
+ - [Applicative, A Strong Lax Monoidal Functor](https://beuke.org/applicative/)
1142
+ - [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
1143
+ """
1144
+ )
1145
+ return
1146
+
1147
+
1148
+ if __name__ == "__main__":
1149
+ app.run()
functional_programming/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
  # Changelog of the functional-programming course
2
 
 
 
 
 
3
  ## 2025-03-16
4
 
5
  + Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`
 
1
  # Changelog of the functional-programming course
2
 
3
+ ## 2025-04-02
4
+
5
+ * `0.1.0` version of notebook `06_applicatives.py`
6
+
7
  ## 2025-03-16
8
 
9
  + Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")`