Srihari Thyagarajan commited on
Commit
1c791c5
·
unverified ·
2 Parent(s): 43992cd d0f22f6

Merge pull request #89 from metaboulie/fp/applicatives

Browse files
functional_programming/06_applicatives.py ADDED
@@ -0,0 +1,1161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.12"
3
+ # dependencies = [
4
+ # "marimo",
5
+ # ]
6
+ # ///
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
19
+
20
+ `Applicative Functor` encapsulates certain sorts of *effectful* computations in a functionally pure way, and encourages an *applicative* programming style.
21
+
22
+ Applicative is a functor with application, providing operations to
23
+
24
+ + embed pure expressions (`pure`), and
25
+ + sequence computations and combine their results (`apply`).
26
+
27
+ In this notebook, you will learn:
28
+
29
+ 1. How to view `applicative` as multi-functor.
30
+ 2. How to use `lift` to simplify chaining application.
31
+ 3. How to bring *effects* to the functional pure world.
32
+ 4. How to view `applicative` as lax monoidal functor.
33
+
34
+ /// details | Notebook metadata
35
+ type: info
36
+
37
+ version: 0.1.2 | last modified: 2025-04-07 | author: [métaboulie](https://github.com/metaboulie)<br/>
38
+
39
+ ///
40
+ """
41
+ )
42
+ return
43
+
44
+
45
+ @app.cell(hide_code=True)
46
+ def _(mo):
47
+ mo.md(
48
+ r"""
49
+ # The intuition: [Multifunctor](https://arxiv.org/pdf/2401.14286)
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
+ /// 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
91
+ fg: Callable[[Functor[A], Functor[B], Functor[C]], Functor[D]]
92
+ ```
93
+
94
+ 3. Apply it to n functor-wrapped values
95
+
96
+ ```python
97
+ # fa: Functor[A], fb: Functor[B], fc: Functor[C]
98
+ fg(fa, fb, fc)
99
+ ```
100
+
101
+ 5. Get a single functor-wrapped result
102
+
103
+ ```python
104
+ fd: Functor[D]
105
+ ```
106
+
107
+ We will define a function `lift` such that
108
+
109
+ ```python
110
+ fd = lift(g, fa, fb, fc)
111
+ ```
112
+ """
113
+ )
114
+ return
115
+
116
+
117
+ @app.cell(hide_code=True)
118
+ def _(mo):
119
+ mo.md(
120
+ r"""
121
+ ## Pure, apply and lift
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
129
+ pure: Callable[[A], Applicative[A]]
130
+ # for example, if `a` is
131
+ a: A
132
+ # then we can have `fa` as
133
+ fa: Applicative[A] = pure(a)
134
+ # or if we have a regular function `g`
135
+ g: Callable[[A], B]
136
+ # then we can have `fg` as
137
+ fg: Applicative[Callable[[A], B]] = pure(g)
138
+ ```
139
+
140
+ 2. `apply`: applies a function inside an applicative functor to a value inside an applicative functor
141
+
142
+ ```python
143
+ # F (a -> b) -> F a -> F b
144
+ apply: Callable[[Applicative[Callable[[A], B]], Applicative[A]], Applicative[B]]
145
+ # and we can have
146
+ fd = apply(apply(apply(fg, fa), fb), fc)
147
+ ```
148
+
149
+
150
+ As a result,
151
+
152
+ ```python
153
+ lift(g, fa, fb, fc) = apply(apply(apply(pure(g), fa), fb), fc)
154
+ ```
155
+ """
156
+ )
157
+ return
158
+
159
+
160
+ @app.cell(hide_code=True)
161
+ def _(mo):
162
+ mo.md(
163
+ r"""
164
+ /// admonition | How to use *Applicative* in the manner of *Multifunctor*
165
+
166
+ 1. Define `pure` and `apply` for an `Applicative` subclass
167
+
168
+ - We can define them much easier compared with `lift`.
169
+
170
+ 2. Use the `lift` method
171
+
172
+ - We can use it much more convenient compared with the combination of `pure` and `apply`.
173
+
174
+
175
+ ///
176
+
177
+ /// attention | You can suppress the chaining application of `apply` and `pure` as:
178
+
179
+ ```python
180
+ apply(pure(g), fa) -> lift(g, fa)
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
+ ///
186
+ """
187
+ )
188
+ return
189
+
190
+
191
+ @app.cell(hide_code=True)
192
+ def _(mo):
193
+ mo.md(
194
+ r"""
195
+ ## Abstracting applicatives
196
+
197
+ We can now provide an initial abstraction definition of applicatives:
198
+
199
+ ```python
200
+ @dataclass
201
+ class Applicative[A](Functor, ABC):
202
+ @classmethod
203
+ @abstractmethod
204
+ def pure(cls, a: A) -> "Applicative[A]":
205
+ return NotImplementedError
206
+
207
+ @classmethod
208
+ @abstractmethod
209
+ def apply(
210
+ cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
211
+ ) -> "Applicative[B]":
212
+ return NotImplementedError
213
+
214
+ @classmethod
215
+ def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
216
+ curr = cls.pure(f)
217
+ if not args:
218
+ return curr
219
+ for arg in args:
220
+ curr = cls.apply(curr, arg)
221
+ return curr
222
+ ```
223
+
224
+ /// attention | minimal implementation requirement
225
+
226
+ - `pure`
227
+ - `apply`
228
+ ///
229
+ """
230
+ )
231
+ return
232
+
233
+
234
+ @app.cell(hide_code=True)
235
+ def _(mo):
236
+ mo.md(r"""# Instances, laws and utility functions""")
237
+ return
238
+
239
+
240
+ @app.cell(hide_code=True)
241
+ def _(mo):
242
+ mo.md(
243
+ r"""
244
+ ## Applicative instances
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
250
+ """
251
+ )
252
+ return
253
+
254
+
255
+ @app.cell(hide_code=True)
256
+ def _(mo):
257
+ mo.md(
258
+ r"""
259
+ ### Wrapper
260
+
261
+ - `pure` should simply *wrap* an object, in the sense that:
262
+
263
+ ```haskell
264
+ Wrapper.pure(1) => Wrapper(value=1)
265
+ ```
266
+
267
+ - `apply` should apply a *wrapped* function to a *wrapped* value
268
+
269
+ The implementation is:
270
+ """
271
+ )
272
+ return
273
+
274
+
275
+ @app.cell
276
+ def _(Applicative, dataclass):
277
+ @dataclass
278
+ class Wrapper[A](Applicative):
279
+ value: A
280
+
281
+ @classmethod
282
+ def pure(cls, a: A) -> "Wrapper[A]":
283
+ return cls(a)
284
+
285
+ @classmethod
286
+ def apply(
287
+ cls, fg: "Wrapper[Callable[[A], B]]", fa: "Wrapper[A]"
288
+ ) -> "Wrapper[B]":
289
+ return cls(fg.value(fa.value))
290
+ return (Wrapper,)
291
+
292
+
293
+ @app.cell(hide_code=True)
294
+ def _(mo):
295
+ mo.md(r"""> try with Wrapper below""")
296
+ return
297
+
298
+
299
+ @app.cell
300
+ def _(Wrapper):
301
+ Wrapper.lift(
302
+ lambda a: lambda b: lambda c: a + b * c,
303
+ Wrapper(1),
304
+ Wrapper(2),
305
+ Wrapper(3),
306
+ )
307
+ return
308
+
309
+
310
+ @app.cell(hide_code=True)
311
+ def _(mo):
312
+ mo.md(
313
+ r"""
314
+ ### List
315
+
316
+ - `pure` should wrap the object in a list, in the sense that:
317
+
318
+ ```haskell
319
+ List.pure(1) => List(value=[1])
320
+ ```
321
+
322
+ - `apply` should apply a list of functions to a list of values
323
+ - you can think of this as cartesian product, concatenating the result of applying every function to every value
324
+
325
+ The implementation is:
326
+ """
327
+ )
328
+ return
329
+
330
+
331
+ @app.cell
332
+ def _(Applicative, dataclass, product):
333
+ @dataclass
334
+ class List[A](Applicative):
335
+ value: list[A]
336
+
337
+ @classmethod
338
+ def pure(cls, a: A) -> "List[A]":
339
+ return cls([a])
340
+
341
+ @classmethod
342
+ def apply(cls, fg: "List[Callable[[A], B]]", fa: "List[A]") -> "List[B]":
343
+ return cls([g(a) for g, a in product(fg.value, fa.value)])
344
+ return (List,)
345
+
346
+
347
+ @app.cell(hide_code=True)
348
+ def _(mo):
349
+ mo.md(r"""> try with List below""")
350
+ return
351
+
352
+
353
+ @app.cell
354
+ def _(List):
355
+ List.apply(
356
+ List([lambda a: a + 1, lambda a: a * 2]),
357
+ List([1, 2]),
358
+ )
359
+ return
360
+
361
+
362
+ @app.cell
363
+ def _(List):
364
+ List.lift(lambda a: lambda b: a + b, List([1, 2]), List([3, 4, 5]))
365
+ return
366
+
367
+
368
+ @app.cell(hide_code=True)
369
+ def _(mo):
370
+ mo.md(
371
+ r"""
372
+ ### Maybe
373
+
374
+ - `pure` should wrap the object in a Maybe, in the sense that:
375
+
376
+ ```haskell
377
+ Maybe.pure(1) => "Just 1"
378
+ Maybe.pure(None) => "Nothing"
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:
386
+ """
387
+ )
388
+ return
389
+
390
+
391
+ @app.cell
392
+ def _(Applicative, dataclass):
393
+ @dataclass
394
+ class Maybe[A](Applicative):
395
+ value: None | A
396
+
397
+ @classmethod
398
+ def pure(cls, a: A) -> "Maybe[A]":
399
+ return cls(a)
400
+
401
+ @classmethod
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
+
415
+ @app.cell(hide_code=True)
416
+ def _(mo):
417
+ mo.md(r"""> try with Maybe below""")
418
+ return
419
+
420
+
421
+ @app.cell
422
+ def _(Maybe):
423
+ Maybe.lift(
424
+ lambda a: lambda b: a + b,
425
+ Maybe(1),
426
+ Maybe(2),
427
+ )
428
+ return
429
+
430
+
431
+ @app.cell
432
+ def _(Maybe):
433
+ Maybe.lift(
434
+ lambda a: lambda b: None,
435
+ Maybe(1),
436
+ Maybe(2),
437
+ )
438
+ return
439
+
440
+
441
+ @app.cell(hide_code=True)
442
+ def _(mo):
443
+ mo.md(
444
+ r"""
445
+ ## Collect the list of response with sequenceL
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:
525
+ ```python
526
+ # fa: Applicative[A]
527
+ apply(pure(id), fa) = fa
528
+ ```
529
+ - Homomorphism:
530
+ ```python
531
+ # a: A
532
+ # g: Callable[[A], B]
533
+ apply(pure(g), pure(a)) = pure(g(a))
534
+ ```
535
+ 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.
536
+ - Interchange:
537
+ ```python
538
+ # a: A
539
+ # fg: Applicative[Callable[[A], B]]
540
+ apply(fg, pure(a)) = apply(pure(lambda g: g(a)), fg)
541
+ ```
542
+ 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.
543
+ - Composition:
544
+ ```python
545
+ # fg: Applicative[Callable[[B], C]]
546
+ # fh: Applicative[Callable[[A], B]]
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
+
554
+ ```python
555
+ @dataclass
556
+ class Applicative[A](Functor, ABC):
557
+
558
+ @classmethod
559
+ def check_identity(cls, fa: "Applicative[A]"):
560
+ if cls.lift(id, fa) != fa:
561
+ raise ValueError("Instance violates identity law")
562
+ return True
563
+
564
+ @classmethod
565
+ def check_homomorphism(cls, a: A, f: Callable[[A], B]):
566
+ if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
567
+ raise ValueError("Instance violates homomorphism law")
568
+ return True
569
+
570
+ @classmethod
571
+ def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
572
+ if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
573
+ raise ValueError("Instance violates interchange law")
574
+ return True
575
+
576
+ @classmethod
577
+ def check_composition(
578
+ cls,
579
+ fg: "Applicative[Callable[[B], C]]",
580
+ fh: "Applicative[Callable[[A], B]]",
581
+ fa: "Applicative[A]",
582
+ ):
583
+ if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
584
+ raise ValueError("Instance violates composition law")
585
+ return True
586
+ ```
587
+
588
+ > Try to validate applicative laws below
589
+ """
590
+ )
591
+ return
592
+
593
+
594
+ @app.cell
595
+ def _():
596
+ id = lambda x: x
597
+ compose = lambda f: lambda g: lambda x: f(g(x))
598
+ const = lambda a: lambda _: a
599
+ return compose, const, id
600
+
601
+
602
+ @app.cell
603
+ def _(List, Wrapper):
604
+ print("Checking Wrapper")
605
+ print(Wrapper.check_identity(Wrapper.pure(1)))
606
+ print(Wrapper.check_homomorphism(1, lambda x: x + 1))
607
+ print(Wrapper.check_interchange(1, Wrapper.pure(lambda x: x + 1)))
608
+ print(
609
+ Wrapper.check_composition(
610
+ Wrapper.pure(lambda x: x * 2),
611
+ Wrapper.pure(lambda x: x + 0.1),
612
+ Wrapper.pure(1),
613
+ )
614
+ )
615
+
616
+ print("\nChecking List")
617
+ print(List.check_identity(List.pure(1)))
618
+ print(List.check_homomorphism(1, lambda x: x + 1))
619
+ print(List.check_interchange(1, List.pure(lambda x: x + 1)))
620
+ print(
621
+ List.check_composition(
622
+ List.pure(lambda x: x * 2), List.pure(lambda x: x + 0.1), List.pure(1)
623
+ )
624
+ )
625
+ return
626
+
627
+
628
+ @app.cell(hide_code=True)
629
+ def _(mo):
630
+ mo.md(
631
+ r"""
632
+ ## Utility functions
633
+
634
+ /// attention | using `fmap`
635
+ `fmap` is defined automatically using `pure` and `apply`, so you can use `fmap` with any `Applicative`
636
+ ///
637
+
638
+ ```python
639
+ @dataclass
640
+ class Applicative[A](Functor, ABC):
641
+ @classmethod
642
+ def skip(
643
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
644
+ ) -> "Applicative[B]":
645
+ '''
646
+ Sequences the effects of two Applicative computations,
647
+ but discards the result of the first.
648
+ '''
649
+ return cls.apply(cls.const(fa, id), fb)
650
+
651
+ @classmethod
652
+ def keep(
653
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
654
+ ) -> "Applicative[B]":
655
+ '''
656
+ Sequences the effects of two Applicative computations,
657
+ but discard the result of the second.
658
+ '''
659
+ return cls.lift(const, fa, fb)
660
+
661
+ @classmethod
662
+ def revapp(
663
+ cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
664
+ ) -> "Applicative[B]":
665
+ '''
666
+ The first computation produces values which are provided
667
+ as input to the function(s) produced by the second computation.
668
+ '''
669
+ return cls.lift(lambda a: lambda f: f(a), fa, fg)
670
+ ```
671
+
672
+ - `skip` sequences the effects of two Applicative computations, but **discards the result of the first**. For example, if `m1` and `m2` are instances of type `Maybe[Int]`, then `Maybe.skip(m1, m2)` is `Nothing` whenever either `m1` or `m2` is `Nothing`; but if not, it will have the same value as `m2`.
673
+ - Likewise, `keep` sequences the effects of two computations, but **keeps only the result of the first**.
674
+ - `revapp` is similar to `apply`, but where the first computation produces value(s) which are provided as input to the function(s) produced by the second computation.
675
+ """
676
+ )
677
+ return
678
+
679
+
680
+ @app.cell(hide_code=True)
681
+ def _(mo):
682
+ mo.md(
683
+ r"""
684
+ /// admonition | exercise
685
+ Try to use utility functions with different instances
686
+ ///
687
+ """
688
+ )
689
+ return
690
+
691
+
692
+ @app.cell(hide_code=True)
693
+ def _(mo):
694
+ mo.md(
695
+ r"""
696
+ # Formal implementation of Applicative
697
+
698
+ Now, we can give the formal implementation of `Applicative`
699
+ """
700
+ )
701
+ return
702
+
703
+
704
+ @app.cell
705
+ def _(
706
+ ABC,
707
+ B,
708
+ Callable,
709
+ Functor,
710
+ abstractmethod,
711
+ compose,
712
+ const,
713
+ dataclass,
714
+ id,
715
+ ):
716
+ @dataclass
717
+ class Applicative[A](Functor, ABC):
718
+ @classmethod
719
+ @abstractmethod
720
+ def pure(cls, a: A) -> "Applicative[A]":
721
+ """Lift a value into the Structure."""
722
+ return NotImplementedError
723
+
724
+ @classmethod
725
+ @abstractmethod
726
+ def apply(
727
+ cls, fg: "Applicative[Callable[[A], B]]", fa: "Applicative[A]"
728
+ ) -> "Applicative[B]":
729
+ """Sequential application."""
730
+ return NotImplementedError
731
+
732
+ @classmethod
733
+ def lift(cls, f: Callable, *args: "Applicative") -> "Applicative":
734
+ """Lift a function of arbitrary arity to work with values in applicative context."""
735
+ curr = cls.pure(f)
736
+
737
+ if not args:
738
+ return curr
739
+
740
+ for arg in args:
741
+ curr = cls.apply(curr, arg)
742
+
743
+ return curr
744
+
745
+ @classmethod
746
+ def fmap(
747
+ cls, f: Callable[[A], B], fa: "Applicative[A]"
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]"
767
+ ) -> "Applicative[B]":
768
+ """
769
+ Sequences the effects of two Applicative computations,
770
+ but discards the result of the first.
771
+ """
772
+ return cls.apply(cls.const(fa, id), fb)
773
+
774
+ @classmethod
775
+ def keep(
776
+ cls, fa: "Applicative[A]", fb: "Applicative[B]"
777
+ ) -> "Applicative[B]":
778
+ """
779
+ Sequences the effects of two Applicative computations,
780
+ but discard the result of the second.
781
+ """
782
+ return cls.lift(const, fa, fb)
783
+
784
+ @classmethod
785
+ def revapp(
786
+ cls, fa: "Applicative[A]", fg: "Applicative[Callable[[A], [B]]]"
787
+ ) -> "Applicative[B]":
788
+ """
789
+ The first computation produces values which are provided
790
+ as input to the function(s) produced by the second computation.
791
+ """
792
+ return cls.lift(lambda a: lambda f: f(a), fa, fg)
793
+
794
+ @classmethod
795
+ def check_identity(cls, fa: "Applicative[A]"):
796
+ if cls.lift(id, fa) != fa:
797
+ raise ValueError("Instance violates identity law")
798
+ return True
799
+
800
+ @classmethod
801
+ def check_homomorphism(cls, a: A, f: Callable[[A], B]):
802
+ if cls.lift(f, cls.pure(a)) != cls.pure(f(a)):
803
+ raise ValueError("Instance violates homomorphism law")
804
+ return True
805
+
806
+ @classmethod
807
+ def check_interchange(cls, a: A, fg: "Applicative[Callable[[A], B]]"):
808
+ if cls.apply(fg, cls.pure(a)) != cls.lift(lambda g: g(a), fg):
809
+ raise ValueError("Instance violates interchange law")
810
+ return True
811
+
812
+ @classmethod
813
+ def check_composition(
814
+ cls,
815
+ fg: "Applicative[Callable[[B], C]]",
816
+ fh: "Applicative[Callable[[A], B]]",
817
+ fa: "Applicative[A]",
818
+ ):
819
+ if cls.apply(fg, cls.apply(fh, fa)) != cls.lift(compose, fg, fh, fa):
820
+ raise ValueError("Instance violates composition law")
821
+ return True
822
+ return (Applicative,)
823
+
824
+
825
+ @app.cell(hide_code=True)
826
+ def _(mo):
827
+ mo.md(
828
+ r"""
829
+ # Effectful programming
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
837
+
838
+
839
+ @app.cell(hide_code=True)
840
+ def _(mo):
841
+ mo.md(
842
+ r"""
843
+ ## The IO Applicative
844
+
845
+ We will try to define an `IO` applicative here.
846
+
847
+ As before, we first abstract how `pure` and `apply` should function.
848
+
849
+ - `pure` should wrap the object in an IO action, and make the object *callable* if it's not because we want to perform the action later:
850
+
851
+ ```haskell
852
+ IO.pure(1) => IO(effect=lambda: 1)
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
+ """
860
+ )
861
+ return
862
+
863
+
864
+ @app.cell
865
+ def _(Applicative, Callable, dataclass):
866
+ @dataclass
867
+ class IO(Applicative):
868
+ effect: Callable
869
+
870
+ def __call__(self):
871
+ return self.effect()
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
+
883
+ @app.cell(hide_code=True)
884
+ def _(mo):
885
+ mo.md(r"""For example, a function that reads a given number of lines from the keyboard can be defined in applicative style as follows:""")
886
+ return
887
+
888
+
889
+ @app.cell
890
+ def _(IO):
891
+ def get_chars(n: int = 3):
892
+ return IO.sequenceL(
893
+ [IO.pure(input(f"input the {i}th str")) for i in range(1, n + 1)]
894
+ )
895
+ return (get_chars,)
896
+
897
+
898
+ @app.cell
899
+ def _():
900
+ # get_chars()()
901
+ return
902
+
903
+
904
+ @app.cell(hide_code=True)
905
+ def _(mo):
906
+ mo.md(r"""# From the perspective of category theory""")
907
+ return
908
+
909
+
910
+ @app.cell(hide_code=True)
911
+ def _(mo):
912
+ mo.md(
913
+ r"""
914
+ ## Lax Monoidal Functor
915
+
916
+ An alternative, equivalent formulation of `Applicative` is given by
917
+ """
918
+ )
919
+ return
920
+
921
+
922
+ @app.cell
923
+ def _(ABC, Functor, abstractmethod, dataclass):
924
+ @dataclass
925
+ class Monoidal[A](Functor, ABC):
926
+ @classmethod
927
+ @abstractmethod
928
+ def unit(cls) -> "Monoidal[Tuple[()]]":
929
+ pass
930
+
931
+ @classmethod
932
+ @abstractmethod
933
+ def tensor(
934
+ cls, this: "Monoidal[A]", other: "Monoidal[B]"
935
+ ) -> "Monoidal[Tuple[A, B]]":
936
+ pass
937
+ return (Monoidal,)
938
+
939
+
940
+ @app.cell(hide_code=True)
941
+ def _(mo):
942
+ mo.md(
943
+ r"""
944
+ Intuitively, this states that a *monoidal functor* is one which has some sort of "default shape" and which supports some sort of "combining" operation.
945
+
946
+ - `unit` provides the identity element
947
+ - `tensor` combines two contexts into a product context
948
+
949
+ More technically, the idea is that `monoidal functor` preserves the "monoidal structure" given by the pairing constructor `(,)` and unit type `()`.
950
+ """
951
+ )
952
+ return
953
+
954
+
955
+ @app.cell(hide_code=True)
956
+ def _(mo):
957
+ mo.md(
958
+ r"""
959
+ Furthermore, to deserve the name "monoidal", instances of Monoidal ought to satisfy the following laws, which seem much more straightforward than the traditional Applicative laws:
960
+
961
+ - Left identity
962
+
963
+ `tensor(unit, v) ≅ v`
964
+
965
+ - Right identity
966
+
967
+ `tensor(u, unit) ≅ u`
968
+
969
+ - Associativity
970
+
971
+ `tensor(u, tensor(v, w)) ≅ tensor(tensor(u, v), w)`
972
+ """
973
+ )
974
+ return
975
+
976
+
977
+ @app.cell(hide_code=True)
978
+ def _(mo):
979
+ mo.md(
980
+ r"""
981
+ /// admonition | ≅ indicates isomorphism
982
+
983
+ `≅` refers to *isomorphism* rather than equality.
984
+
985
+ In particular we consider `(x, ()) ≅ x ≅ ((), x)` and `((x, y), z) ≅ (x, (y, z))`
986
+
987
+ ///
988
+ """
989
+ )
990
+ return
991
+
992
+
993
+ @app.cell(hide_code=True)
994
+ def _(mo):
995
+ mo.md(
996
+ r"""
997
+ ## Mutual definability of Monoidal and Applicative
998
+
999
+ We can implement `pure` and `apply` in terms of `unit` and `tensor`, and vice versa.
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
+ )
1012
+ return
1013
+
1014
+
1015
+ @app.cell(hide_code=True)
1016
+ def _(mo):
1017
+ mo.md(
1018
+ r"""
1019
+ ## Instance: ListMonoidal
1020
+
1021
+ - `unit` should simply return a empty tuple wrapper in a list
1022
+
1023
+ ```haskell
1024
+ ListMonoidal.unit() => [()]
1025
+ ```
1026
+
1027
+ - `tensor` should return the *cartesian product* of the items of 2 ListMonoidal instances
1028
+
1029
+ The implementation is:
1030
+ """
1031
+ )
1032
+ return
1033
+
1034
+
1035
+ @app.cell
1036
+ def _(B, Callable, Monoidal, dataclass, product):
1037
+ @dataclass
1038
+ class ListMonoidal[A](Monoidal):
1039
+ items: list[A]
1040
+
1041
+ @classmethod
1042
+ def unit(cls) -> "ListMonoidal[Tuple[()]]":
1043
+ return cls([()])
1044
+
1045
+ @classmethod
1046
+ def tensor(
1047
+ cls, this: "ListMonoidal[A]", other: "ListMonoidal[B]"
1048
+ ) -> "ListMonoidal[Tuple[A, B]]":
1049
+ return cls(list(product(this.items, other.items)))
1050
+
1051
+ @classmethod
1052
+ def fmap(
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
+
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 _(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
1088
+ class Functor[A](ABC):
1089
+ @classmethod
1090
+ @abstractmethod
1091
+ def fmap(cls, f: Callable[[A], B], a: "Functor[A]") -> "Functor[B]":
1092
+ return NotImplementedError
1093
+
1094
+ @classmethod
1095
+ def const(cls, a: "Functor[A]", b: B) -> "Functor[B]":
1096
+ return cls.fmap(lambda _: b, a)
1097
+
1098
+ @classmethod
1099
+ def void(cls, a: "Functor[A]") -> "Functor[None]":
1100
+ return cls.const_fmap(a, None)
1101
+ return (Functor,)
1102
+
1103
+
1104
+ @app.cell(hide_code=True)
1105
+ def _():
1106
+ import marimo as mo
1107
+ return (mo,)
1108
+
1109
+
1110
+ @app.cell(hide_code=True)
1111
+ def _():
1112
+ from dataclasses import dataclass
1113
+ from abc import ABC, abstractmethod
1114
+ from typing import TypeVar, Union
1115
+ from collections.abc import Callable
1116
+ return ABC, Callable, TypeVar, Union, abstractmethod, dataclass
1117
+
1118
+
1119
+ @app.cell(hide_code=True)
1120
+ def _():
1121
+ from itertools import product
1122
+ return (product,)
1123
+
1124
+
1125
+ @app.cell(hide_code=True)
1126
+ def _(TypeVar):
1127
+ A = TypeVar("A")
1128
+ B = TypeVar("B")
1129
+ C = TypeVar("C")
1130
+ return A, B, C
1131
+
1132
+
1133
+ @app.cell(hide_code=True)
1134
+ def _(mo):
1135
+ mo.md(
1136
+ r"""
1137
+ # Further reading
1138
+
1139
+ Notice that these reading sources are optional and non-trivial
1140
+
1141
+ - [Applicaive Programming with Effects](https://www.staff.city.ac.uk/~ross/papers/Applicative.html)
1142
+ - [Equivalence of Applicative Functors and
1143
+ Multifunctors](https://arxiv.org/pdf/2401.14286)
1144
+ - [Applicative functor](https://wiki.haskell.org/index.php?title=Applicative_functor)
1145
+ - [Control.Applicative](https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#t:Applicative)
1146
+ - [Typeclassopedia#Applicative](https://wiki.haskell.org/index.php?title=Typeclassopedia#Applicative)
1147
+ - [Notions of computation as monoids](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/notions-of-computation-as-monoids/70019FC0F2384270E9F41B9719042528)
1148
+ - [Free Applicative Functors](https://arxiv.org/abs/1403.0749)
1149
+ - [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/)
1150
+ - [Abstracting with Applicatives](http://comonad.com/reader/2012/abstracting-with-applicatives/)
1151
+ - [Static analysis with Applicatives](https://gergo.erdi.hu/blog/2012-12-01-static_analysis_with_applicatives/)
1152
+ - [Explaining Applicative functor in categorical terms - monoidal functors](https://cstheory.stackexchange.com/questions/12412/explaining-applicative-functor-in-categorical-terms-monoidal-functors)
1153
+ - [Applicative, A Strong Lax Monoidal Functor](https://beuke.org/applicative/)
1154
+ - [Applicative Functors](https://bartoszmilewski.com/2017/02/06/applicative-functors/)
1155
+ """
1156
+ )
1157
+ return
1158
+
1159
+
1160
+ if __name__ == "__main__":
1161
+ app.run()
functional_programming/CHANGELOG.md CHANGED
@@ -1,14 +1,21 @@
1
  # Changelog of the functional-programming course
2
 
3
- ## 2025-03-11
4
 
5
- * Demo version of notebook `05_functors.py`
 
 
 
 
 
6
 
7
- ## 2025-03-13
8
 
9
- * `0.1.0` version of notebook `05_functors`
10
 
11
- Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
 
 
12
 
13
  ## 2025-03-16
14
 
@@ -34,3 +41,13 @@ Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Ha
34
  - Rename `ListWrapper` to `List` for simplicity
35
  - Remove the `Just` class
36
  + Rewrite proofs
 
 
 
 
 
 
 
 
 
 
 
1
  # Changelog of the functional-programming course
2
 
3
+ ## 2025-04-07
4
 
5
+ * the `apply` method of `Maybe` *Applicative* should return `None` when `fg` or `fa` is `None`
6
+ + add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`, `Maybe`, `List`
7
+ + add description for utility functions of `Applicative`
8
+ * refine the implementation of `IO` *Applicative*
9
+ * reimplement `get_chars` with `IO.sequenceL`
10
+ + add an example to show that `ListMonoidal` is equivalent to `List` *Applicative*
11
 
12
+ ## 2025-04-06
13
 
14
+ - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be generically implemented
15
 
16
+ ## 2025-04-02
17
+
18
+ * `0.1.0` version of notebook `06_applicatives.py`
19
 
20
  ## 2025-03-16
21
 
 
41
  - Rename `ListWrapper` to `List` for simplicity
42
  - Remove the `Just` class
43
  + Rewrite proofs
44
+
45
+ ## 2025-03-13
46
+
47
+ * `0.1.0` version of notebook `05_functors`
48
+
49
+ Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) for reviewing
50
+
51
+ ## 2025-03-11
52
+
53
+ * Demo version of notebook `05_functors.py`