A Type of Programming
A Type of Programming
Renzo Carbonara
Table of Contents
1. Computers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2. Dare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3. Machines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4. Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5. Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6. Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
7. Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
8. Runtime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
9. Function application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
10. Perspective . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
11. β-reduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
12. Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
13. Datatypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
14. Pattern matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
15. Exhaustiveness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
16. Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
17. Parsing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
18. Either . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
19. Isomorphisms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
20. Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
21. Dos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
22. Tuple syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
23. Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
24. Induction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
25. Representation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
26. Halt!. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
27. Too much . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
28. λ-calculus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
29. List syntax. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
30. Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
31. η-conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
32. Partial application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
33. Bliss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
34. Typeclass. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
35. Functor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
36. Instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
37. Parametricity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
38. Law . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
39. Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
40. Polymorphic composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
41. Second functor law . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
42. Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
43. Fixity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
44. List constructors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
45. Abstraction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
46. Functor f. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
47. The right. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
48. The left . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
49. Kinds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
50. Kind notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
51. Type constructors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
52. Kind of fun. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
53. Equational reasoning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
54. Shadow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
55. α-conversion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
56. Churn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
57. Performance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
58. Fleither . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
59. Flip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
60. bimap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
61. Bifunctor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
62. Swap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
63. Bifunctorn’t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
64. Quantification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
65. Tragedy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
66. Subversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
67. Positions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
68. Covariance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
69. Contravariance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
70. Constance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
71. Little boxes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
72. Profunctor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
73. Opa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
74. Profunity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
75. Mundane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
76. Origami. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
77. Accumulate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
78. Pancake . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
79. Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
80. Reverse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
81. Concat. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
82. Discovery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
83. Monoid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
84. Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
85. Vocabulary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
86. Concerns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
87. Comfort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
88. maybe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
89. either . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
90. Bool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
91. Conjunction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
92. Laziness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
93. Thunk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
94. Weak head . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
95. Normal form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
96. Spine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
97. Lazy enough . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
98. Equations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
99. Pattern much . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
100. Types, please . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
101. Closure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
102. Num . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
103. Too eager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
104. Eq . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
105. Lazy mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
106. Naturals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
107. Cavemen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
108. Clingy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
109. Troy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
110. Frogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
111. Dialogue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
112. Foldable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
113. Politics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
114. A hole new world . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
115. Adventure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
116. Rebelión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
117. Or something. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
118. Disjunction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
119. Dot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
120. Disjunctivitis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
121. Parsers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
122. Wild bool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
123. Surjection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
124. Type synonyms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
125. Sequence. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
126. Split . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
127. Guards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
128. Æ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
129. Compost. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
130. Leftovers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
131. Two. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
132. Wot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
133. Twogether . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
134. Tres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
135. To wrap up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
136. Cardinality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
137. Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
138. Invert. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
139. Broken parts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
140. Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
141. Plus one . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441
142. Times one. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
143. Equality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
144. Mon produit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
145. L’addition. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
146. Less is more . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
147. Power . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459
148. Ex falso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
149. Myriad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
150. Spice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
151. Government. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472
152. Another one. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474
153. Alles klar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
154. Mechanical. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
155. Poke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
156. Beauté . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 486
157. So what? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 490
158. Fan. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494
159. Choice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 499
160. Why not . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
161. Não tem fim. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
162. Location . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
163. Relative . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
164. Absolute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516
165. Attention . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 521
166. Candy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523
167. Small things . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
168. Chow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530
169. Bigger things . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
170. Digital . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537
171. Digitoll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
172. Open up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 546
173. Digítame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
174. Nearly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 557
175. Other things. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 559
176. René . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561
177. Cease . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570
178. Soup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Copyright © Renzo Carbonara, 2019.
program :: x -> x
id :: x -> x
As a quick reminder, natural numbers are the zero and all the
integer numbers greater than it. For example, 12, 0 and 894298731
are all natural numbers, but 4.68, π or -58 are not.
That is, this function takes a Natural number as input, and returns a
Natural number as output. In other words, the type of this function
is Natural → Natural. Yes, just like numbers, functions have their
own types as well, and they are easy to recognize because of the →
that appears in them, with their input type appearing to the left of
this arrow, and their output type to its right.
Presumably, if we ask for the id of the Natural number 3, we will get
3 as our answer. Indeed, this would be the behaviour of the identity
function. However, we also said that names can be arbitrarily
chosen. Usually we choose them in alignment with their purpose,
but what if we didn’t? What would happen if we named our id
function for Natural numbers something else instead? Let’s try.
And what if we were told that one of the following functions is our
identity function, renamed once again, and the other one adds as
many numbers as it states? Look carefully. Can we tell which is
which?
On the second line we have some new notation. The first thing to
notice is that we are mentioning the name add_one again, but this
time followed by =. Whereas :: allowed us to specify a type for this
name, = is saying that the name add_one is equal to the expression that
appears to its right. If we were so inclined, we could read this out
loud as “the name add_one, whose type is Natural → Natural, is equal
to the expression \x → x + 1”.
With this new knowledge, we can finally give the unsurprising full
implementation of our beloved identity function, including both
its type and the expression that defines its behaviour.
id :: x -> x
id = \x -> x
We named both the mystery type and the mystery value x because it
makes the whole thing look terribly handsome. But, of course, we
could have named these things anything else. It’s all the same to
Haskell, names are just for us humans.
four = add_one 3
four :: Natural
We also know that the function definiton syntax binds its input to
some name. That is, when we say (\x → x + 1), any input we
provide to this function will be bound to the name x within these
parentheses. And names, we said, can be replaced by the expression
they represent, which in our example will be the number 3. That is,
we could write four = (\3 → 3 + 1) 3. Wait, what? Is this correct? It
looks quite weird, but actually, this happens to be accidentally
correct Haskell. Not exactly what we are looking for, though.
We have some new syntax. To the left of the equals = sign, we are
saying that Season is the name of the new type of data, or datatype,
we are introducing. That is, Season is now a type that we could
mention in places like those where we mentioned Natural so far.
The word data is just part of the syntax that Haskell uses for
defining new data types, we can mostly ignore it. Types, such as
Natural or Season, always start with uppercase letters.
Actually, what we are seeing here is not that different from what
we’ve seen so far regarding Natural numbers, if we consider that in
order to construct a value of type Natural out of thin air, we need to
write one number like 0 or 27 literally. That is, a value of type
Natural is just one natural number among infinitely many of them.
The definition for a type like Natural is a bit more complicated, but
conceptually is not that different from other sum types such as
Season. It is a beautiful thing how in programming, when we stay
close to reason, we invest our time studying small examples, and
once we are confident in our understanding of the principles
involved we can scale up our knowledge to larger problems without
any friction. It really is beautiful.
So, now we can construct values that precisely represent our seasons
by simply using one the four constructors for Season.
my_favorite_season :: Season
my_favorite_season = Autumn
Our case expression starts where we say case x of and extends all the
way to the end of our function. Let’s pay attention to the left side
of the arrows on the following lines. Just like we listed all the
possible constructors when we defined our Season datatype, here we
enumerate again all the possible constructors that could have been
used to obtain x, the Season value we are scrutinizing. Our x will
definitely be one among these four constructors, since they
represent all the possible ways of constructing values of type Season.
So, when one of these constructors matches the actual value of x,
then the expression that shows up to the right side of its arrow
symbol → will be returned. There, to the right, we are just
constructing new Season values as we have done before, making sure
we map every Season to the one in the opposite hemisphere.
case x of
Winter -> …
Spring -> …
Summer -> …
We are mentioning yet another type for the first time here, String,
which already comes with Haskell. A String value is essentially text,
and is usually written between double quotes "like this". So, just
like how writing the digits of a number as 123 allows us to create a
numeric value of type Natural out of thin air, writing some
characters between double quotes " allows us to construct a new
textual value of type String. The word “string” refers to the fact
that a textual value is, metaphorically, a string of individual
characters one after the other. Our season_to_temperature function
is just returning a textual representation of the expected
temperature for a given Season.
data Temperature
= Cold
| Warm
| Hot
| Chilly
This time we are defining our datatype across multiple lines just to
showcase that it can be done. Had we written everything in the
same line as before, the result would have been the same. Not
important at all. With this new Temperature datatype, we can rewrite
our temperature_to_season function.
Maybe. Dare we guess what Maybe is all about? We haven’t really seen
this syntax before, where we have two words next to each other
show up in a type. We’ll talk about that later. What we can say,
however, is that Maybe Temperature must be some kind of sum type,
considering what we just said about the behaviour of this function,
which returns either a value of type Temperature whenever that’s
possible, or it safely reports that it was imposible to do so. Either
this or that. Yes, Maybe Temperature must be a sum type somehow.
Let’s see its definition.
It’s important to keep in mind that Just itself is not a value of type
Maybe a. Instead, it needs to be applied to some value of type a in
order to construct a value of type Maybe a. So, if Just can be applied
like any other function, then it must have a function-like type,
right? Indeed, it must. Constructors have types, and the type of
Just is a → Maybe a. In our example, this mystery placeholder a has
been replaced by Temperature everywhere, so in concrete terms our
particular Just will temporarily take the type Temperature → Maybe
Temperature when used. Again, this is similar to how when defining
our identity function we said id :: x → x, yet later we decided to
use id with a Natural number and id temporarily took the type
Natural → Natural where it was used. This substitution of a is not
something that we need to do explicitly in writing, however. It just
happens. We’ll see more about this phenomenon later.
The names of the constructors Left and Right are not particularly
important. As we said before, Haskell doesn’t really care about
names, all it cares about is that we are consistent when we use them.
Presumably, the words “left” and “right” have something to do
with the fact that the x that is the payload of the Left constructor is
the leftmost parameter to the Either type constructor, whereas the y
that is the payload of the Right constructor is the rightmost one. Or
maybe it means something else. We don’t care.
The interesting thing to notice about Either is that not only both its
constructors carry a payload, but said payloads can potentially be of
different types: One mentions x and the other mentions y. For
example, we could have x be Temperature and y be Season, leading to
the type Either Temperature Season conveying the idea that we have
either a value of type Temperature or a value of type Season. In this
case, we would use the Left constructor if interested in the
Temperature value, or the Right constructor otherwise.
We don’t care much for rhymes, and it shows. We care about safe
programs, though, that’s why we pattern matched against all eight
possible ways of constructing a value of type Either Temperature
Season. We hadn’t seen patterns that mention more than one word
yet, but there shouldn’t be any surprises there. For example, Right
Autumn is exactly the same we would write if we were trying to
construct a value of type Either Temperature Season, we know that.
Those functions satisfy the expected types just fine, yet they don’t
contribute towards our isomorphism proof. Remember, we said
that two types being isomorphic means that it’s possible to convert
back and forth between them without any loss of information, but
our funny functions here certainly lose information. For example,
if they are given Winter as input somehow, then that Winter will be
ignored by the wildcard underscore _ and forever lost, replaced by
Hot or Summer. Our original implementations of fromEither and
toEither were correct and avoided this issue. Essentially, what we
were looking for was a pair of functions that, when applied after
each other, gave us back the same input that was provided to it.
We will continue using this new definition so that we can get used
to differentiating between types and expressions by means other
than just their looks.
Counting from one is strange. You’ll get the joke later, but that’s all
16 of them. There’s no other value that can carry the type Pair
Season Temperature. And of course, these 16 values are the ones we
would need to pattern match against if we were doing some kind of
case analysis.
That is, Right is saying that we’ll be providing a value for the
payload on the Right constructor of the outermost Either, and Left
is saying that we’ll be providing the a value for the payload of the
Left constructor of the inner Either. It should be quite easy to
appreciate visually how the parentheses on the type match the
position of the parentheses on the expression. Visual aids are always
welcome.
Notice that Pair (Pair x y) z and Pair x (Pair y z), or any other
permutation of x, y and z there, are types isomorphic to each other.
And the same is true for Eithers. That is, while these are different
types in the eyes of the type-checker, we can easily convert back and
forth between them without loss of information. For example,
here’s one such silly conversion, and we can easily imagine how the
one in the oposite direction would look like.
f :: Pair (Pair x y) z
-> Pair x (Pair z y)
f = \(MakePair (MakePair x y) z) ->
MakePair x (MakePair z y)
We are growing, aren’t we? Even our function types now span
multiple lines. That’s fine, nothing changes, it’s just a different use
of our whitespace. We mostly just need to make sure we always
indent our text to the right where otherwise we would have
continued on the same line.
It is of course possible to combine Pairs and Eithers as well. We
could have, say, in a type like Either (Pair Natural Season) (Pair
Season Temperature), either a Natural and a Season, or a Season and a
Temperature.
22. Tuple syntax
The Pair type isn’t present in the Haskell language exactly as we
introduced it. It’s there conceptually, sure, but out-of-the box we
get a different syntax for it. Rather than writing Pair a b, we write
(a, b). This notation for pairs is part of the Haskell language
syntax, and we couldn’t define it ourselves if we wanted to. It’s
quite sad that we have exceptions such as this, but at times it can be
practical. For example, whereas a product type of more than two
elements nesting our fundamental Pairs requires us to write Pair
(Pair a b) (Pair c (Pair d e)) or similar, using the built-in syntax
for pairs we can say (a, b, c, d, e), which is arguably easier on the
eyes. Of course, we could have introduced a new product type
capable of holding 5 arbitrary elements, say Pair5 a b c d e, and it
would have been as straightforward and specialized as this five-
element tuple, but without the extraordinary syntax.
data Maybe a
= Nothing
| Just1 a
| Just2 a a
| Just3 a a a
| Just4 a a a a
| Just5 a a a a a
| … more of these …
Notice the type. It’s still a List Natural even though the constructor
is a different one. How come? Well, List a is a coproduct, so we
know we can construct one using any of its constructors. Empty was
one such constructor, but OneMore is one as well. OneMore takes our
new element 8 as payload alongside our Empty starting point, which
fit the expected Natural and List Natural types of the constructor
fields just fine. Can we go further? Of course we can, this was the
plan all along. Let’s add the element 5 to the list.
Still the same type, still the OneMore constructor, still a value of type
Natural, 5, in the first field, and a value of type List Natural in the
second field. We can repeat this until we get bored. We can
continue applying the OneMore constructor to this List, and each
time we do we are effectively making room for yet another value of
type Natural to become part of the list.
Linked lists belong to the classic literature of programming, thus
we’ll refrain from blasphemy and use the names historically given
to its constructors, Nil and Cons, rather than the Empty and OneMore
names that served our didactic pursuit so far. Remember, the
compiler doesn’t care about names, but we and our colleagues do.
We’ve been told that natural numbers are infinite. But we, people
of science, trust nothing but our thirst and our proofs. Thus we’ll
corroborate this lore ourselves using induction.
One of the constructors for our Nat datatype will be Zero, which,
akin to the Nil in our previous List, takes no payload and represents
the smallest Nat. When we say smallest, however, we are not
referring to the fact that zero is indeed numerically smaller than
every other natural number, which happens to be a coincidence.
Rather, we mean that zero can be said to be a natural number
without having to relate it to yet another natural number. It has the
smallest structure, or smallest number of relationships, if you will.
If we recall our definition of natural numbers again, we said that
they are either zero or any integer number bigger than zero. So, if
we were to justify why the integer number five is also a natural
number, we’d have to say “because it is bigger than zero”, whereas
the reason why zero is a natural number is just “because”. Can we
see the difference? Five is a natural number because its relationship
with another natural number says so, zero is a natural number
because it is zero and that’s the base case, the starting point from
where we can start talking about natural numbers.
If we contrast this with another inductive datatype, the negative
integers, which are -1 and every other integer smaller than -1, then
-1 would be the smallest or base case in our induction, even if from
a numerical point of view the number -1 is actually larger than
every other negative integer. Inductive datatype definitions always
start from a base case, which in the case of our List it was Nil, and in
the case of our Nat it is Zero.
Other than the word succ standing for successor and inviting us to
wonder whether we ever stop to read abbreviations out loud lest
they don’t stand scrutiny, Succ should immediately remind us of
Cons, the inductive constructor in our definition of List. What Succ
is saying is that no matter what Natural number we have, we can
obtain its succesor —that is, the natural number immediately after
it— by applying the Succ constructor to it. Let’s count to three to
see how this works.
zero :: Nat
zero = Zero
one :: Nat
one = Succ Zero
two :: Nat
two = Succ (Succ Zero)
three :: Nat
three = Succ (Succ (Succ Zero))
We are saying here that we’ll name Zero “zero”, that we’ll name “
one” the Successor of Zero, that we’ll name “two” the Successor to the
Successor of Zero, etc. We are, essentially, counting how many times
the Succ constructor is used. This gets tiresome rather quickly,
however, which is why by using the power of name binding we
could clean up things a bit if we wanted to.
Do you get the joke now about counting from one now? It’s
strange. We programmers, naturally, always count from zero.
25. Representation
Haskell’s Natural and our Nat are not exactly the same. Say, if we
apply a function expecting a Natural to Succ Zero, the type-checker
will reject it. Conceptually, though, they are “the same”. That is,
they are isomorphic to each other. They carry the same meaning, so
no information would be lost if we were to convert between them.
Let’s implement these conversions so that we can convince
ourselves that we did a good job.
fromNat is saying that given a Nat, which we will call x, we return the
Natural value 0 in case x is Zero. Otherwise, if x Succs, we add 1 to the
result of applying fromNat to y, the name given to the Nat field in the
Succ constructor. What we are doing, essentially, is adding 1 each
time we encounter the Succ constructor, peeling the Succ layers one
by one until we finally reach our base case Zero and the recursion
stops. For example, fromNat Zero would lead to 0, fromNat (Succ
Zero) would lead to 1 + 0, fromNat (Succ (Succ Zero)) would lead to
1 + 1 + 0, etc. Grab a pencil and a piece of paper and try to do this
same exercise yourself: Follow the transformation of fromNat (Succ
(Succ Zero)) step by step until it becomes 1 + 1 + 0.
One such machine, close to our hearts and to everything we’ve seen
so far, is the lambda calculus, stylized λ-calculus if leveraging the
beautiful Greek alphabet. We will build this machine now.
28. λ-calculus
Building a lambda calculus can be a tough exercise, but by doing so
we’ll come to appreciate one of the core ideas of functional
programming: That however complex a problem might seem at
first, however different from everything else we’ve seen before, it
can always be broken down into more fundamental problems that
can be tackled separately, problems that quite often we’ve already
solved before.
It’s OK to feel a bit lost here. We are not learning this because we
need it right away, we are learning it so that we can demystify the
complexities of this field, understand that we are in control, and
scare away any silly ideas that we are not capable of tackling what’s
to come. We are welcome to find solace somewhere else, too, if that
works.
Let’s forget about types while we are here. In our lambda calculus,
initially, we will only be concerned about expressions. Our lambda
calculus tells us what kind of expressions we can use, how, and
what they mean. In its most rudimentary form, an expression in the
lambda calculus is one of three things: It’s either a reference to a
value, just like the names we have in Haskell; or it is a lambda
expression not unlike the ones we have in Haskell; or it is the
application of an expression to some other expression, much like
our own function applications in Haskell. Either, or, or; we know
what that calls for: A sum type, which we will call Expr as a short for
expression.
data Expr
= Lookup String
| Lambda String Expr
| Apply Expr Expr
expr_id :: Expr
expr_id = Lambda "x" (Lookup "x")
id = \x -> x
Since we can’t yet explain how numbers come to be, let’s just
assume that somebody, a superior being, created the number five
for us and bound it to the name "five" by saying Lambda "five". We
can refer to this number while remaining deliberately oblivious of
the complex truth by looking it up by name using Lookup "five". So
now we have Lookup "five", a value of type Expr that magically
means something. How do we apply expr_id to it? Well, we just use
the Apply constructor with expr_id as its first parameter and Lookup
"five" as the second.
expr_five :: Expr
expr_five = Apply expr_id (Lookup "five")
Now expr_five and our mystical Lookup "five" have exactly the same
meaning, just like 5 and id 5 mean exactly the same in Haskell. We
have accomplished a function application in our Expr lambda
calculus.
But wait. Actually, we could have avoided all this mystical mess by
simply realizing that expr_id is itself already a value of type Expr, and
that while Apply needs two values of type Expr, it says nothing about
whether they need to be different. So we might as well use expr_id
twice. What would be wrong with saying Apply expr_id expr_id?
Nothing. There would be nothing wrong with that. It conveys
exactly the same idea as id id does in Haskell, a function
application whose main purpose seems to be to deepen our already
profound appreciation for the beauty of the identity function,
when we realize that id id is also the identity function.
id == id id
But we will put our lambda calculus on standby for now. We don’t
really need to understand how to interpret or calculate these Exprs
just yet. Simply knowing that it can be done, and that
programming is just functions all the way down, should put our
minds at ease. We will come back to this topic later when it is time
to implement our own programming language. For now, let’s go
back to our more immediate goal of becoming proficient in
Haskell.
id == id id id id id id id id id id id id id id id id
29. List syntax
Let’s look at linked lists once more. They will accompany us for a
very long time in our learning, as their simplicity and inductive
nature make them an excellent structure for didactic and practical
purposes.
Yes, add_ones [3, 4, 5] will indeed result in [4, 5, 6], adding one to
each element as we wanted. But, in all honesty, add_ones is a rather
sad thing. For one, it’s assuming a list of three elements. This works,
alright, but what happens if we apply this function to something
else like [1, 2] or to the empty list []? Boom. Our program crashes
at runtime, the passengers on the plane die. Luckily, Haskell’s type-
checker will tell us about our non-exhaustive pattern-matching and
prevent this program from compiling, so mistakes like that one are
easy to avoid. A second and more subtle issue is that we are writing
the same operation, the addition of 1, three times. This is
manageable here, but imagine how lengthy and sad it would be
having to do the same for a list of, say, 100 elements.
Let’s look at the implementation of map using our own List rather
than Haskell’s own weird list syntax, lest we get distracted by it and
miss the point of map.
Saying either f or \x → f x means exactly the same. This fact has its
roots in the concept of Eta-conversion, stylized η-conversion, which
essentially says that this is possible. We sometimes refer to the silly
version of a function, that is, to the one allegedly unnecessarily
wrapping it in an extra lambda expression, as its η-expanded form.
It’s OK to forget this name, however.
32. Partial application
We say that map is partially applied inside our pointfree definition
of add_ones, meaning not all the parameters map was expecting have
been provided yet, only part of them have.
Taking this even further, one could argue that add_ones doesn’t even
deserve its name. We might as well use map add_one directly as in map
add_one [2, 3] to obtain [3, 4] as result. Well, actually, we don’t
even need to name add_one. We could just partially apply our add
function of type Natural → Natural → Natural as add 1, obtaining yet
another function of type Natural → Natural which we could use as
the first parameter to map. We can say [3, 4], we can say map (add 1)
[2, 3], or we can say map add_one [2, 3], and it’s all the same.
What’s more, we can also say map (add 2), map (add 3) or similar for
slightly different results.
33. Bliss
Of course, we can’t confirm nor deny what is map's behaviour
judging solely from its type. According to it, this could be a
perfectly acceptable definition of map:
If we compare the type of fmap with the type of map for List, we shall
notice a striking similarity.
So we are saying that for some arbitrary type f to belong to the class
of types that can be mapped over, which we call the Functor class, it
must implement the fmap method. Yes, fmap is technically just a
function, but the fact that its type is defined as part of a typeclass
makes it deserve the special name “method”, presumably for us
humans to have an easier time talking about it out loud. But
functions, we know, have both a type and a definition. Here,
however, we can only see its type. Where is its definition? Where is
the expression that defines what fmap actually does?
36. Instances
There is only one class called Functor, but there are potentially many
types that satisfy the requirements of that class. That is, types that
can be mapped over. We know of at least two: Our own List, and
Haskell’s own list with that weird square bracket [] syntax. We
establish the relationship between typeclasses and the types that can
implement them through instances. In instances, that’s where fmap's
implementation lives. Let’s write the Functor instance for our List
type.
Here we are defining a new function, add_ones, that given any f that
satisfies the Functor constraint —that is, an f that implements a
Functor instance— will return a transformed f that increases each
Natural number contained in it by one. Any f, that’s what’s
important to notice. This f can be a List, it could be Haskell’s own
list with that weird square brackets syntax, or it could even be
something completely different. Perhaps a bit surprisingly, it could
be Maybe. Yes! Why not? After all, a value of type Maybe Natural
could potentially contain a Natural number that we could increase
by one. In fact, we said before that Maybe is essentially a possibly
empty one element list, so it shouldn’t surprise us that these types
can achieve similar things. Let’s see how the implementation of the
Functor typeclass for Maybe looks like.
instance Functor Maybe where
fmap = \g ya ->
case ya of
Nothing -> Nothing
Just a -> Just (g a)
Our allegedly broken fmap fits this type perfectly, so what is the
problem? Is there a problem at all? Well, we the authors of this
mess can see that fmap will ignore its inputs and always return
Nothing, so yes, there is a problem. How is this possible? Weren’t
typeclasses supposed to save us from this pain? Well, it is a bit more
complicated because the type-checker can’t help us this time.
The first Functor law, which we call the functor identity law says
that mapping the identity function id over some Functor x should
return that same x unmodified:
fmap id x == x
This should be rather unsurprising. And dull, also dull. That’s OK.
In other words, we are saying that applying fmap id to something
should have the same innocuous effect as applying id to it.
fmap id x == id x
Does our broken Functor instance for Maybe satisfy this law? Let’s
see. If we try, for example, fmap id Nothing, where Nothing of type
Maybe Natural takes the place of the x we mentioned above, then the
result is expected to be Nothing. That is, the same x again. Great, it
works, fmap id Nothing is indeed Nothing. However, this law must be
satisfied for all choices of x, not just Nothing. For example, x could
be Just 5. Does fmap id (Just 5) return Just 5? No, it does not. Our
broken fmap implementation within the Functor instance for Maybe
simply ignores its input and returns Nothing every time. Among
other things, this behaviour violates the functor identity law. This
proves that the fmap implementation we gave is indeed incorrect,
thus we can’t say Maybe is a Functor if such is the best
implementation of fmap we can come up with. Luckily, we already
know it’s possible to come up with a correct implementation of
fmap for Maybe; we did it in the last chapter.
We still have two blanks there to fill where we wrote _. But they are
quite easy to fill, aren’t they? We are already applying compose to
two functions multiply_by_ten and add_one, so we simply write
down their types there.
compose
:: (Natural -> Natural)
-> (Natural -> Natural)
-> (Natural -> Natural)
compose
:: Natural
-> Natural
-> Natural
-> Natural
-> Natural
-> Natural
compose
:: (Natural -> Natural)
-> (Natural -> Natural)
-> (Natural -> Natural)
compose
:: (Natural -> Natural)
-> (Natural -> Natural)
-> (Natural -> Natural)
compose = \g f x -> g (f x)
compose
:: (Natural -> Natural)
-> (Natural -> Natural)
-> Natural
-> Natural
compose = \_ _ _ -> 8
If we drop the x from the equality above, we can write both sides of
the equality in an arguably clearer pointfree style.
Did our broken Functor instance for Maybe, the one that always
returned Nothing, violate this law? Not really. Both fmap (compose g
f) and compose (fmap g) (fmap f) would return the same Nothing.
Good thing we had the other law, the functor identity law, to
prevent that nonsense from happening.
42. Comments
So where do we write these laws? Well, Haskell doesn’t provide us
with mechanisms for laying down laws for typeclasses in such a way
that instances of that typeclass are automatically checked for
compliance. Wouldn’t that be nice, though? No, in Haskell we are
unfortunately on our own on this matter, and at best we can write a
comment about it.
We can’t prove that all Functor instances abide by these laws, but
we can prove that this is true sometimes, on a case by case basis. So
far, we have been doing this with pen and paper, writing down in
English the reasons why these laws hold or not. Is this satisfactory?
Of course not, but here we are. Is this an accident? Is this Haskell
abandoning us? Kind of. What’s happening is that the type of fmap
is not rich enough to guarantee the semantics we expect when using
it, so we need to change fmap's type to be more descriptive. Doing
this with Haskell’s type system, however, is not really quite
possible. Or at least, it’s not at all practical. Like the language
without coproducts, like the language with just the string, here we
find ourselves longing for something we do not have, without
which we are forced to acknowledge a problem that could arise at
runtime unless we manually prove that these laws are indeed
respected. In the future we will see how we can write better proofs
that the computer can check for us, but for now let’s just embrace
this handicap and move on.
infixl 7 *
Isn’t this boring and noisy? Isn’t it frustrating to try and guess
where the implicit parentheses may or may not be? Yes, of course it
is. This is why we try to avoid infix functions, at times called infix
operators, as much as possible. To make it easier for us, and to make
it easier for our colleagues who will have to read our code
tomorrow. Yet, arithmetic operators such as + and * are so
ubiquitous that we need to understand this.
44. List constructors
Before we talked about how we could use the : constructor to cons
an element onto the linked lists that come with Haskell and have
that square bracket syntax. We said we could use something like 3 :
[] to prepend 3 to the empty list. It should be apparent now that
what we were saying was that : is an infix constructor. If we
wanted to use : in prefix position, we could do it by simply adding
those extra parentheses we talked about before. For example, (:) 3
[] is the same as 3 : [], except uglier.
Can we tell from its usage whether the : infix constructor associates
to the left or to the right? Where would we put our implicit
parentheses if we wanted to? (2 : 3) : [] wouldn’t type-check,
because while one of our : has a perfectly acceptable [] as its second
parameter, it also has 2 : 3 as its first parameter, which doesn’t
make any sense. Remember, : needs the value of a list element as its
first parameter, and a list as its second argument. 3, however, is no
list. Thus, we can say with confidence that : doesn’t associate to the
left, otherwise these parentheses wouldn’t have prevented the
expression from type-checking. On the other hand, putting
parentheses to the right as in 2 : (3 : []) works perfectly: Both :
have a Natural number to the left and a [Natural] to its right. In
other words, : associates to the right, and now we know why [1, 2,
3], 1 : 2 : 3 : [], 1 : 2 : [3] and 1 : [2, 3] all mean the same.
45. Abstraction
Some things, like natural numbers, we can count with our fingers.
Other things, like temperatures, we can feel on our skin. These are
very concrete things, things that require almost no effort from us in
order to acknowledge their presence and meaning in this world.
But not all things are this way. Some things, like recursion, functors
or love, abandon their corporeal identity in order to become
abstractions. What is a functor? It is something that has a particular
behavior. What is recursion? “What is recursion?” indeed. Can we
touch love? Can we draw a functor in the sand? No. If we are going
to gain a deeper understanding of what we’ve seen so far, of what’s
to come, then we need to abandon our corporeal expectations and
become thought. We call these things, forgetful of their actual
identities, abstractions.
Aesthetics aside, this is the only possible behavior for the fmap
method in this instance. Much like how parametricity forced the
implementation of id and compose to achieve what they do, here it is
forcing fmap to have this behavior, the only one that type-checks.
Try to implement it differently; it won’t work.
This is what we have. It says in the tin that the Left never changes.
But, could we have it the other way around? Tackle the Left
instead?
Natural and String certainly are different, but aren’t all types
different? That’s why we have types at all, because we want to tell
apart expressions that convey different concepts by tagging them
with types that the compiler will reject if different than expected.
Natural and String are different in the same way the numbers 2 and
7 are: If we are planning to add 2 to some number, but we
accidentally add 7, say, the result will be wrong. But the fact that we
were able to add 7, rather than 2, says something about how these
two things are not that different after all. They are both, indeed,
numbers. With kinds, we have a similar situation. To understand
this, let’s look at our identity function once again.
id :: x -> x
Natural :: Type
When writing down kinds, we use the symbol :: to state that the
thing to its left is a type, and the thing to the right its kind. Here we
are saying that the type Natural has kind Type. There are a couple of
confusing things about this. First, we are reusing the same symbol
:: that we used for stating that some expression has a particular
type, like in 2 :: Natural. However, if we consider that this is
something that, out loud, we read as “2 has type Natural”, and that
in turn we read Natural :: Type as “Natural has kind Type”, which is
true, then we should be at ease. This, actually, is valid Haskell code:
2 :: (Natural :: Type)
The second thing that might be a bit confusing about this notation
is that we are saying that Natural :: Type means “Natural has kind
Type”. That is, we are seeing the word “type” appear in the place we
supposedly talk about kinds. But this makes sense, types have kind
Type indeed. Type is just a name somebody chose for this kind of
things. Other kinds of things are named differently.
So let’s just try and embrace this notation. The symbol :: is used
both for conveying the type and the kind of something, and
whenever we see Type, we must remember we are dealing with a
kind, not a type.
51. Type constructors
We can tell types apart from type constructors by looking at their
kinds. While a type like Natural has kind Type, a type constructor
generally has kind Type → Type.
And from here we can see why we can’t use fmap to work on the
Left side of the Either: It is because if we realize that Either x, a
type-constructor with kind Type → Type as expected by the Functor
typeclass, must be the f in our Functor instance, then that f will
continue to be Either x anywhere it appears inside the Functor
instance, which includes the fmap method, a method that doesn’t
provide any way to modify that f. Compare the following two
types:
And, you see, there is no way to modify that Either x. If there was
one, we would have an extra input parameter to fmap indicating
how to modify that x on the Left, but alas, we don’t have one. All
we know is how to modify that a, the one on the Right, into a b.
There are kinds beyond Type, Type → Type, Type → Type → Type, etc.
However, for the time being we don’t care. And anyway, we have
learned enough about this topic for now, so let’s continue our
journey, comfortable with this new knowledge.
52. Kind of fun
Let’s go back to our goal of coming up with a Functor instance for
functions. The first thing we need to acknowledge is that we must
be able to talk about functions as something of kind Type → Type
somehow, since this is what the Functor typeclass mandates. How,
though? Functions like Natural → Natural or a → b are expressions,
values of kind Type, not type constructors. Well, Maybe Natural and
Either a b were Types as well, yet somehow we managed
nonetheless. We simply left the type partially applied, and that
seemed to do the trick. How do we partially apply the type of a
function, though? It might help to learn that the arrow → in a
function type like a → b is an infix type constructor of kind Type →
Type → Type. So when we see a function Type like a → b, we must
realize that we are looking at →, the type-constructor for functions,
being applied to two other Types a and b.
Let’s do something beautiful. Let’s have the types guide us. Many
times, when programming, we have no idea what a solution could
look like, why a problem has been encoded in the way it has, or
what the problem is at all. And most times, actually, it doesn’t
matter. To the disappointment of the salesman, everything is more
or less the same. So we willingly embrace this ignorance and rely on
parametricity, this idea that a polymorphic function like fmap must
always behave the same no matter what values it will deal with.
Let’s do this step by step.
Ugh, let’s clean that up. First, those parentheses around (→) x are
unnecessary: ((→) x) y and (→) x y, for any choice of y, mean the
same. Let’s get rid of them.
Oops, our fmap now seems to take four parameters as input rather
than two. It’s alright, we just forgot to add parentheses to prevent
the arrows → belonging to f from getting intertwined with the ones
that are part of fmap's own type.
Done. We had no idea what to do, we just more or less knew where
we wanted to go, we let parametricity guide us, and we got there
anyway. We can be certain our implementation is correct because
there is simply no other way of implementing this. Try all you
want, but aesthetics aside, you’ll end up here again. We are home.
We are where we need to be.
So when they ask if you can do something, take their money and
say yes. You know parametricity and they think their problem is
special. You will be fine. Just kidding. Don’t do that. Be a
responsible professional. The point is that you can always rely on
parametricity, the ultimate trick.
But not only is this the solution we didn’t know we were looking
for, it is also a beautiful place. Look harder at the type of our fmap.
What do you see? Nothing? Are you sure? What about if we just
rename some of the type parameters?
The first functor law, the identity law of functors, says that
fmapping id over our functor shall result in that same functor,
unmodified. In other words, for any function f, f and fmap id f are
equal. We will use a very straightforward and mechanical technique
to show that this is indeed the case.
fmap id f
We should clarify that we won’t be writing a Haskell program here,
we will just write down expressions that happen to be equal to the
previous one we wrote. This is the kind of logical reasoning that
you can do on a piece of paper. Eventually, if true, we will get to say
that fmap id f is indeed equal to f.
We didn’t inline the definition of fmap because it was the only thing
to do, we only did it because it was one of the possible alternatives.
We could have replaced id with its definition instead, and it would
have been fine as well. We just needed to start somewhere, so we
made an arbitrary choice.
\x -> id (f x)
\x -> f x
fmap (compose g f) a
Now, since we already now that the fmap definition for functions is
just compose, we can simply replace the name fmap for the name
compose in our expression in order to make things simpler.
compose (compose g f) a
It’s also important to keep in mind that g, f and a are just names we
are making up here, names of expressions, and they have no direct
correspondence to the fs and as we have been encountering in
places such as class Functor f or fmap :: (a → b) → f a → f b, where
not only are those the names of types and type-constructors, not
expressions, but they are also made up. During this equational
reasoning exercise we are only paying attention to expressions, not
to types.
It’s like you have a neighbour, Andrea, who you only run into from
time to time. You go back to your place, and maybe you tell your
partner about how you ran into Andrea in the hallway, or how
Andrea rang your bell the other day, asking if we had a tool he
didn’t have. Whenever you or your partner say “Andrea”, you both
know with certainty that you are talking about that neighbour.
Andrea is part of your daily life, of your environment, so you make
sure you remember his name. One day, however, a friend from
your childhood comes to visit you. A friend that just for one day
becomes a part of your private environment, of your home. She is
also called Andrea. In that home, that day, if you talked to your
partner about somebody named Andrea: Who do you think will
come to mind first? The neighbour who is out there, or the
childhood friend who is with you today? That’s right. Andrea, the
name of our friend, shadows the name of our neighbour, even if
only for that day, in that home.
There are very simple rules to identify with certainty the expression
to which we refer when we mention a name anywhere in our code.
We will learn them very soon, but for now let’s go back to our more
immediate concerns.
55. α-conversion
We are still in the middle of our journey, trying to prove the second
functor law for functions by using equational reasoning to
demonstrate that compose (fmap g) (fmap f) a and fmap (compose g
f) a are the same. We decided to start by reducing fmap (compose g
f) a, and we ended up with this:
We have way too many fs, gs and as in our code, and that is a bit
confusing. We know that because of the shadowing of names,
Haskell doesn’t care. Haskell can still figure out the meaning of this
without any cognitive overhead. But we, the fragile humans,
struggle. So let’s use a tool to help us navigate this mess.
Now we can do the same with i, this time substituted with all of \c
→ g (f c).
\b -> g (f (a b))
There’s nothing else to do. fmap (compose g f) a reduces to this at
best. All we have to do now is reduce compose (fmap g) (fmap f) a
and see if we end up with the same expression. If we do, it means
that fmap (compose g f) a and compose (fmap g) (fmap f) a are
indeed the same, which proves the second functor law for
functions. Let’s do it faster this time.
First, let’s inline the definitions of compose and fmap, which we know
are the same. As we do it, let’s also use alpha-conversion to make
sure we pick different names so that this is easier for us humans to
follow.
\c -> g (f (a c))
And we are done. There’s nothing left to reduce, luckily. So, have
we proved anything? On the one hand we had fmap (compose g f) a,
which reduced to \b → g (f (a b)), and on the other hand we just
reduced compose (fmap g) (fmap f) a to \c → g (f (a c)), which is
not exactly the same. Or is it? Aren’t we forgetting what our friend
alpha-conversion is capable of? These two expressions are alpha-
equivalent, we just ended up binding the name b on one of them
and c on the other, but the meaning of these expressions is the
same: We take a value as input, apply the function a to it, then
apply f to this result, and finally apply g to all of that.
Oh, and by the way, that thing about fmap (compose g f) being
more performant than compose (fmap g) (fmap f)? It turns out that
this has impressive implications in the large. We’ll see this pop up
time and time again as a magic trick to make our programs faster in
our studies.
58. Fleither
A while ago we struggled with the Left. We learned that fmap wasn’t
able to do anything with the value contained in a Left constructor,
it was only able to cope with the one on the Right one. It had
something to do with kinds. Essentially, the Functor typeclass
expects its instances to be defined for type-constructors of kind Type
→ Type, where that input Type is necessarily the rightmost type
parameter in our type constructor. So, for example, Either x,
because of its kind Type → Type, is a suitable candidate for a Functor
instance. And what happens to the type of the fmap method in this
instance? It becomes (a → b) → Either x a → Either x b, a function
that suddenly can only modify the payload on the Right. Nothing
interesting happens on the Left.
What if we flip it, though? What if Either x y meant that x was the
type of the payload on the Right, and y the type of the one on the
Left? Well, in the Functor instance for this brand new Either x, also
of kind Type → Type, we would still get fmap :: (a → b) → Either x a
→ Either x b. That static x however, the one that stays the same,
would now be talking of the Right payload, not the Left. And vice-
versa, of course, which means that fmap would indeed allow us to
modify the Left. The problem, however, is that we can’t do that.
Someone other than us came before, created Either the way we first
met it, and shipped it with the Haskell language as such. We are
kind of stuck with it, forever, we have to embrace it as it is. What
can we do?
Look how the b type parameter, the one our fmap method would
allow us to modify, is now on the Fleft as we wanted.
Here we are defining a new datatype named Flip, with two type
parameters b and a. This datatype has a single constructor, also
called Flip, which takes an entire Either a b as payload. That is, we
construct a Flip by saying either Flip (Left …) or Flip (Right …).
The interesting thing to notice is that the type parameters in Flip b
a and Either a b are flipped, which would force a Functor instance
for Flip b to target that a on the Left, rather than the b on the Right
as fmapping over Either a b would normally do.
Ok, so how is Flip better than that failed Fleither from before?
Well, it is still not ideal, so if that’s what we are looking for we are
not going to find it here. But look at this:
Can you tell what this horrible yet didactic function does? What
would be the result of foo (Left 3)? What about foo (Right
Winter)? Well, it doesn’t matter what the exact result would be, but
suffice to say that the bar function we assume here would transform
that Natural number 3 on the Left side of the Either into a String
somehow, and the qux function would transform the Season on the
Right into a [Temperature]. Remember, the type-level parameters to
Flip appear deliberately in the opposite order than those in Either
do, let’s be aware of that. The interesting thing in this exercise is
that we manage to target both sides of an Either by relying solely on
the behaviour of fmap. Unfortunately, we always end up with a Flip
rather than an Either as output. For example, foo (Left 5) could
result in the value Flip (Left "bird") rather than just Left "bird".
Luckily, that’s quite easy to fix. We just need to remove that
wrapper. After all, it doesn’t serve any purpose after we are done
fmapping over it.
The problem, once again, is that we know too much about the
payloads that go into that Either. We need to forget. How? Why
with parametricity, of course. So, rather than going from an Either
Natural Season to a Either String [Temperature], we will go from an
Either of mysterious things to an Either of even more mysterious
things. Let’s make up some names.
Let’s copy this, then, but taking two functions rather than one.
One for each side.
Sure, foo doesn’t apply our fantasy bar nor qux on its own anymore,
but that’s alright, we could now say foo bar qux to achieve the same
result. That’s what we wanted, after all. Now, originally we named
this thing foo because, this function being as ugly as it was, didn’t
deserve a better name. We punish ad-hoc code by not giving it a
decent name, that’s what we do. It keeps us from developing
affection for it. But now, behold, foo is one with beauty, so let’s
give it a proper name.
bimap
:: (a -> c) -> (b -> d) -> Either a b -> Either c d
bimapPair
:: (a -> c) -> (b -> d) -> Pair a b -> Pair c d
bimapPair = \f g (Pair a b) -> Pair (f a) (g b)
bimapPair
:: (a -> c) -> (b -> d) -> Pair a b -> Pair c d
With these instances in place, bimap (add 1) (add 10) will take us
from Pair 2 5 to Pair 3 15, from Left 2 to Left 3, from Right 5 to
Right 15, and the like.
62. Swap
Here’s a quick interlude, a curiosity. We just learned that the bimap
method of the Bifunctor typeclass allows us to comfortably work on
both the as and bs showing up in types like Either and Pair. And
seeing how as and bs are all these types have inside, we should be
able to tackle anything concerning Either, Pair and the like using
just bimap. Right? Not so fast. Consider the swap function.
Like the identity function, swap is one of those functions for which
there’s only one possible implementation. It’s parametricity, again,
telling us what to do. Anyway, that’s beside the point. We are
trying to focus on something else here. We are trying to address
how to implement swap in terms of bimap. It should be possible, if
bimap is as powerful as we made it out to be.
Well, well, well… it seems bimap is not as powerful after all. Let’s
recall the type of bimap.
bimap :: Bifunctor f
=> (a -> c) -> (b -> d) -> f a b -> f c d
In our case, the chosen f, our Bifunctor, is Pair. Let’s specialize that.
bimap :: (a -> c) -> (b -> d) -> Pair a b -> Pair c d
Moreover, the expected output type is not Pair c d, but rather Pair
b a. We can replace c with b and d with a throughout to convey this.
Alright. So, all we need to do is have the first function, the one that
modifies the a value as it appears in the input Pair a b, return the b
value that appears in that same input. And vice-versa in the case of
the second function. Alas, we can’t.
… and then it dies. Just ponder at the apparent beauty, the abyss, of
the type bimap gets for a function.
The mystery, the conundrum, must thus be in c, the one type that
fmap doesn’t mention at all.
If we look at what bimap returns, we see a function that receives a c
as input, and somehow returns a d as output. Well, let’s try and find
the ways we could obtain a d. There is a b → d function, which
implies that if we somehow manage to get our hands on a b, then a d
will easily follow. Alright, let’s get a b then. There is a function a →
b that returns a b if we just give it an a. Well then, let’s find a a. Oh,
the disappointment. There’s no a, and there is no way to obtain one
either. All we have is a c, a bunch of functions we can’t use, and the
queen of them all, a → c, just shamelessly laughing at us. How did
we end up here? We need c → a, not a → c. Who flipped that arrow?
We have the c already, it’s the a the one we need, not the other way
around. Just flip it. Flip it. Flip it! Ahh!
So this is the place. This is where many a programmer has lost it.
And understandably so, for this is a fundamental problem we are
facing. But we are going to get through this, don’t worry, and
things will be much, much brighter on the other side. It will be
alright.
64. Quantification
Let’s have a quick interlude before we continue. We have been
hiding something quite important about the types we write, and it’s
time we made this explicit.
id :: a -> a
id = \x -> x
none :: Maybe a
none = Nothing
So far, the only type of quantification we’ve dealt with, and the
only one we care about for the time being, is the universal
quantification of type variables. And it turns out that this type of
quantification, being the most common and Haskell being so user-
friendly, is also the default one that Haskell infers for us unless we
somehow ask for something else. That is, when we write a type like
id's:
id :: a -> a
How does this solve the issue of the a in the type of id being
different from the a in the type of none? Well, let’s see the properly
quantified type of none again.
It’s also possible to leave out the explicit kind information for a
quantified name, whenever it can be inferred from its usage. For
example, we could have written the type of fmap without explicitly
mentioning the kinds of a, b and f, and it would have been the
same. Haskell infers that a and b have kind Type, and that f has kind
Type → Type.
fmap
:: forall f a b
. Functor f
=> (a -> b)
-> f a
-> f b
And, by the way, one last charming detail. Rather than writing
forall, we can write ∀. It’s the same, but looks arguably nicer.
Unsurprisingly, we read ∀ out loud as “for all”.
id :: ∀ x. x -> x
But of course, none of this is real. If we dare look at the abyss for a
second, we’ll see that nope 3, say, beta-reduces to nope 3, which itself
reduces to nope 3, which again reduces to itself and forever
continues to do so. That is all there is to nope. And while to the
distracted reader this may sound a bit like what id is doing, it is
most definitely not. When we say id 3, that application reduces to 3
and there’s nothing more to reduce. But trying to reduce nope 3, on
the other hand, brings us back to nope 3. Intuitively, nope never
reduces to a smaller expression. Or, to make an analogy with
inductive recursion, it never gets closer to a base case. We saw
something like this many chapters ago, when we discussed a broken
fromNatural which got farther and farther away from its base case as
time went by.
Nope, there are no useful answers ever coming out of nope. Like a
dog trying to catch its tail, like going down Escher’s stairs, all we get
from nope is a promise, an ever lasting futile attempt, and an
innocent caller forever waiting for this function to return.
Theoretically, nope would use up all time and energy in the universe
and still fail to deliver its promise. Technically speaking, we say that
nope diverges. And that vertigo? That sense of infinity? That’s what
a paradox feels like. What happens in practice, however, if we ever
use nope, is that our program hangs. It gets stuck for as long as the
computer, unaware, continues to execute our program. Forcing the
termination of our program, perhaps by rebooting our computer,
mitigates this. Have you tried turning it off and on again? It really is
fascinating how we can model and tame the infinite within a box,
on a small piece of paper.
But this gets even more interesting, for even in this mud we can still
use our tools to improve the implementation of nope. The first
thing we can do is notice that nope is a function written in eta-
expanded form \x → nope x, which we could simplify by eta-
reducing to just nope.
The tautology is now even more obvious: nope is nope. Sure, quite
helpful, thank you very much. However, something changed.
Other than its type, nothing in the definition of nope suggests it is a
function anymore. Has input → output now become an overly
specific type? If we say nope is a function, then nope = nope is simply
stating that “a function is a function”. But wouldn’t this tautology
also apply to types other than functions? Can’t we say a Natural
number is a Natural number? Can’t we say a Maybe String is a Maybe
String? Of course we can. We can say that any value of a particular
Type is indeed a value of that Type, meaning that the type of nope
could in principle be more general.
nope :: ∀ (x :: Type). x
nope = nope
Here, we are saying that nope is a value of every type out there. Do
you want to add two Natural numbers? Try nope + nope. Do you
need something to put in your Just? Maybe Just nope is your
answer. And can we compose some nopes? Sure we can, try compose
nope nope. But of course, none of these expressions accomplishes
anything, even while they all type-check. So, what does this mean?
undefined :: ∀ (x :: Type). x
But this exercise is not so much about undefined, Haskell, nor about
general purpose programming languages. It’s about understanding
our limitations and who we are. We programmers are not in the
business of lying, but in that of transparency and dealing with facts.
Knowing about our shortcomings means we can continue our
journey aware of the limb that we lack, of the allergies we have, and
still live a full factful life.
Is this all? Of course not. So far all we’ve done is say that b → c, a →
b and a are negative arguments whereas c is a positive argument. We
might as well have said they are inputs and outputs, respectively, and
it would have been sufficient. So what else is there? Let’s take it step
by step. First, let’s put those redundant rightmost parentheses back
in the type of compose.
Let’s continue looking at compose and its positions, but at the same
time, let’s not do it. Let’s stop calling it compose, let’s call it fmap
instead.
fmap :: ( a -> b)
-> (f a -> f b)
But let’s forget about fmap for a bit and compare these two
functions, a → b and f a → f b, on their own merit. They both have
an a somehow appearing as part of their input, and a b somehow
appearing as part of their output. In other words, directly or not,
the as are part of arguments in negative positions and the bs are part
of arguments in positive ones. That is, despite that f, the positions
of the arguments in a → b is preserved in f a → f b. Lifting a → b
into f didn’t change this. There is a name for this rather
unsurprising phenomenon, it’s covariance.
69. Contravariance
When we say Functor f, we are saying that f is a covariant functor,
which essentially means that for each function a → b, there is a
corresponding function f a → f b that is the result of lifting a → b
into f. The name “covariant”, with the co prefix this time meaning
with or jointly, evokes the idea that as the a in f a varies through a →
b, the entirety of f a varies to f b with it, in the same direction.
But as with many precious things in life, it’s hard to appreciate why
this is important, or at all interesting, until we’ve lost it. So let’s lose
it. Let’s look at this matter from the other side, from its dual, a dual
we find by just flipping arrows. So let’s try that and see what
happens.
contramap :: ( a -> b)
-> (g a <- g b)
contramap :: ( a -> b)
-> (g b -> g a)
In Haskell, contramap exists as the sole method of the typeclass called
Contravariant, which is like Functor but makes our minds bend.
Bend how? Why, pick a g and see. Maybe perhaps? Sure, that’s simple
enough and worked for us before as a Functor.
Like the identity law for fmap, the identity law for contramap says
that contramapping id over some value x should result in that same x.
contramap id x == x
Just like we have a composition law for fmap, we have one for
contramap as well. It says that contramapping the composition of two
functions f and g over some value x should achieve the same as
applying, to that x, the composition of the contramapping of g with
the contramapping of f.
contramap (compose f g)
== compose (contramap g) (contramap f)
Isn’t this the same as the composition law for fmap? Nice try, but
take a closer look.
fmap (compose f g)
== compse (fmap f) (fmap g)
contramap (compose f g)
== compose (contramap g) (contramap f)
But this time we’ll leave it to you and parametricity to figure out
what const does. Don’t worry, you got this. You’ve been preparing
for this moment all along.
71. Little boxes
Let’s make something very clear once and for all. We said it before,
but here we say it again. Functors —that is, Functors— are not
containers, they are not little boxes.
This 5, our a, is being produced here out of the blue. This proves
that a is indeed an argument in positive position from the
perspective of foo. Sure, from the perspective of the Just value-
constructor, the a it expects to receive is certainly in negative
position, but a value-constructor’s perspective doesn’t matter at all
when judging the variance of a datatype. What matters, instead, is
the perspective of the already constructed value. In our case, it’s the
point of view of the Maybe Natural value itself, foo, not the Just
constructor of type Natural → Maybe Natural, what matters.
I trust you arrived at the same place during your own exploration of
const. But here it is, in case you didn’t. It’s possible you ended up
with \a _ → a, \a → \b → a, or similar instead. That’s fine, it all
means the same.
So if you dare profit from your innocence and entertain the idea of
an alternative implementation of Maybe, where rather than having a
value of type a we have a function of type b → a in it, there shall be
no doubt whatsoever of the covariance of this datatype.
The experienced reader will notice that, for unrelated reasons, this
Haskell code doesn’t work. But experienced reader, we are just
playing, so let’s pretend it does.
And finally, here is foo again, accommodating the needs of our new
representation.
And this same reasoning applies to List, Either and any other
Functor we may encounter. They are not little boxes, they are type-
constructors covariant in the rightmost of their type-parameters,
that’s all they are. The functor instance for functions makes it quite
clear, as the covariance of b in a → b can be seen from far, far away.
72. Profunctor
So what about Bifunctor? Two little boxes, perhaps? No, definitely
not. Let’s recall the definition of the Bifunctor typeclass.
We have so many type variables here that, to the untrained eye, it’s
not obvious what’s going on. So let’s explore this together.
Essentially, bimap is two fmaps in one. We already knew this, of
course, considering how the whole reason for reaching out to
Bifunctor in the first place was that we wanted to act on both the
Left and the Right side of an Either at the same time. So yes,
Bifunctor is two fmaps, two Functors in one. One that turns the a in f
a b into c, and another one that turns the b in it into d. Together,
they turn f a b into f c d.
fmap :: ( b -> c)
-> ((->) a b -> (->) a c)
That is, the unchanging (→) a part is our f here, our Functor. And
we can see how, from the point of view of fmap, all the bs appear as
arguments in negative position and all the cs appear in positive
ones. The implementation of this function proves that functions
are indeed covariant in their outputs. What about their inputs,
though? Well, let’s try to lift a function that changes the a in a → b
into c and see what happens.
This type is similar to fmap's, to compose's, but not quite the same, so
pay careful attention. We’ll call it foo because this function doesn’t
fit in any of the typeclasses we have described so far. We’ll figure
out where to put this code later on. Remember: Here we are trying
to modify the input of a → b, not its output. So let’s try to
implement foo, and let’s also drop those redundant rightmost
parentheses while we are at it.
foo :: ( a -> c )
-> ((->) a b -> (->) c b)
fmap :: ( a -> c)
-> (f a -> f c)
contramap :: ( a -> c)
-> (f c -> f a)
And now that we have this, let’s replace that f back with (→) _ b
—again, notice the hypothetical placeholder _ in there.
bar :: ( a -> c )
-> ((->) c b -> (->) a b)
That’s right, all we had to do was flip the order of the input
parameters to compose. Anyway, seeing how bar here is essentially
contramap, this proves that functions are contravariant in their
input parameter, the one we were focusing on.
So no, functions are not Bifunctors because they don’t have two
covariant type parameters. They have one covariant parameter, its
output, and another one that is contravariant, its input. Functions
just don’t fit bimap. Notice, however, that Bifunctor is also a terrible
name, because just like how functors, semantically, can be either
covariant or contravariant, the mathematical bifunctor can in
principle have any of its type parameters be covariant or
contravariant as well. Terrible naming indeed. So no, functions are
not Bifunctors in Haskell’s parlance, but they are bifunctors
nonetheless, in the true mathematical sense. Naming mistakes stay
with us forever.
If we compare bimap and dimap side by side, we’ll see they are
essentially the same, except the as and cs appear in opposite
positions.
bimap :: (a -> c) -> (b -> d) -> p a b -> p c d
dimap :: (a -> c) -> (b -> d) -> p c b -> p a d
This time we chose names pre and pos, rather than f and h, to
highlight that the application of pre precedes the application g —our
Profunctor, the function we are transforming— and that the
application of pos is posterior to it. This is quite a common
nomenclature. Of course, we could have composed pos and g first,
and only then composed this with pre, as in compose (compose pos g)
pre. It’s all the same, for even if we haven’t said so explicitly,
function composition is associative.
So that’s it. Functions are indeed bifunctors, yes, but the Profunctor
kind, where their first type-parameter is contravariant and only the
second is covariant.
73. Opa
We know that a function a → b is contravariant in a, yet we have
neglected writing a Contravariant instance for it. Let’s fix that.
Look, Flip and Op are essentially the same. It’s only their names and
the type they wrap what changes.
data Op b a = Op ((->) a b)
data Flip b a = Flip (Either a b)
contramap = \f g a -> g (f a)
contramap = \f (Op g) -> Op (\a -> g (f a))
dimap :: Profunctor p
=> (a -> c) -> (b -> d) -> p c b -> p a d
dimap id id == id
This function, sum, adds together all of the Natural numbers present
in a list. The type of sum doesn’t say much, but if we look at the
implementation of this function we will see that it recursively adds
each element in the list. For example, applying sum to [2, 4, 3]
would result in 9, for it reduces to 2 + sum [4, 3], which further
reduces to 2 + (4 + sum [3]) and then to 2 + (4 + (3 + sum []))
before finally becoming 2 + (4 + (3 + 0)). If we add those numbers
together, we end up with 9.
The funny thing is the 1 there. It was easy to make sense of 0 in the
case of sum because of its relation with the absence of things. If we
have no numbers to add, we get 0 as result. Quite reasonable.
However, in product we are saying that if we have no numbers to
multiply, then we get 1 as result. Where does the 1 come from, if we
have nothing to multiply at all? It is a bit funny, yes, I’ll give you
that. Yet, multiplying 2 * (4 * (3 * 1)) together really leads us to
24, which according to the abacus is exactly the number we expect
as the result of multiplying all of 2, 4 and 3 together. So where’s the
trick? What’s going on?
Identity, darling, you are everywhere. You never leave, you never
cease to amaze us. Yes, yes indeed, adding zero or multiplying by
one are identities as well. We can write a function that adds zero,
another that multiplies by one, call them id, and it would be alright.
However, this time things are a bit different, and we are looking for
something else. For one, we are assuming that the input to our
identity function will be some kind of number, for we should be
able to multiply it or add to it at times. This alone already violates
the parametric polymorphism in ∀ x. x → x, which says nothing
about x, let alone it being a number. But beside this, we need to
understand that it’s not 1 nor 0 who are the identities, but instead,
it’s the combination of one these numbers with a particular
operation, here * and +, who are. So much so, actually, that as every
other interesting thing in our journey, these numbers have special
names. We say 0 is the additive identity and 1 is the multiplicative
identity. We sometimes call them the units or identity elements of
their respective operations, too. So no, we can’t really separate 0
from +, nor 1 from *, if we want to keep calling them identities.
But perhaps more importantly, at least insofar as sum and product are
concerned, we need to acknowledge that being able to add and
multiply numbers is necessary, notwithstanding whether these
numbers are identity elements or not. When we arrived to 2 * (4 *
(3 * 1)), for example, we were using * to multiply many numbers
beside just 1, and that was fine. That is, while we certainly have a
motivation for keeping the concepts of 1 and * together so that we
can speak of them as an identity, we also have a motivation for
keeping them separate, so that we can use * without involving 1 at
times. In other words, we need to pair 1 and * without conflating
them. So from now on, conceptually at least, we will pair elements
with the operation for which they behave as identities, so that we
are able to use them together or separately as we see fit. And these
pairs, being as interesting as they are, will have a name too. We will
call them monoids. We will explore them in detail later on, but for
now let’s just remember their name monoid and what it means.
Interestingly, what told sum and product apart brought them closer
together as well, furthering our thesis that nothing mundane is as
special as we are made to believe. The different behaviors of sum and
product are simply due to their conscious choice of one of these
monoids. But this is only half of the truth, of course, for if we leave
monoids aside for a moment, the rest of sum and product are exactly
the same, which should immediately catch our attention as
something special too. So let’s abstract away what’s unique about
sum and product, focusing on what’s not.
foldr
:: (Natural -> Natural -> Natural)
-> Natural
-> [Natural]
-> Natural
foldr = \op unit xs ->
case xs of
[] -> unit
x : rest -> op x (foldr op unit rest)
Things got a bit hairy. We’ll clean this up soon enough, don’t
worry, but for now let’s try to follow along. Fundamentally, only
two things changed between product, sum and this new function
foldr. Whereas before we mentioned 1 or 0 directly, respectively
hardcoded inside the definitions of product and sum, here we are
making it possible to receive this value as an input to this function.
Its name, unit, should remind us of its purpose. Furthermore, we
also abstract away the operation we perform on these numbers. We
call it op, and it occupies the place that * and + did before. This time
op appears in a prefix position, but that’s a cosmetic matter that
shouldn’t surprise us: We know that a + b, say, is the same as (+) a
b, so if (+) ever becomes that op, it will be alright.
The types that unit and op take can be inferred from their use, so
don’t let that scare you. First, let’s keep in mind that foldr
ultimately returns a value of type Natural. It says so in the tin. And,
since unit is the value foldr will return when we encounter an
empty list [], then unit must necessarily be of type Natural too. We
use a similar reasoning for inferring the type of op, which we see
being applied to two arguments. The first of these arguments, x, is
an element found inside a list of Natural numbers, so it must be a
value of type Natural itself. The second argument to op is the result
of a recursive call to foldr which, we just said, results in a value of
type Natural. And finally, not only must op take two Natural
numbers as input, but it must also return yet another Natural,
because like in unit's case, the return type of foldr mandates that
the expression foldr returns always be Natural. And that’s it. We
have concluded that x must be of type Natural and op must be of
type Natural → Natural → Natural, so these are the types of the
expressions we see foldr take as new inputs, beside the actual list of
Natural numbers to process.
With foldr in place, we can now redefine sum and product in a more
concise and fundamental way by partially applying foldr, the
function that somehow knows about the structure of a list, to the
operations and units of the monoids that know how to combine
the elements in that list.
2 + (4 + (3 + 0))
2 : (4 : (3 : []))
That’s right, we can define the identity function for lists using foldr
too. We said before that we could use foldr to convert lists into
more or less anything we wanted, which suggests that it should be
possible to convert them to themselves too, and our definition of id
here is the proof of that. However, when we introduced foldr
before, we did it in a context where we wanted to multiply and add
numbers, so we focused on lists containing Natural numbers and on
ultimately returning Natural numbers as well, so we can’t really
implement this id function with it. But all that Natural selection
was unnecessary, and we could have made foldr a bit more
polymorphic instead.
foldr :: ∀ a z. (a -> z -> z) -> z -> [a] -> z
foldr = \op unit xs ->
case xs of
[] -> unit
x : rest -> op x (foldr op unit rest)
Then, we said that the elements in our list are of some type a, and
the only place we ever deal with these elements directly is in our call
to op, where an a is its first parameter.
The only thing remaining is the second parameter to op. But this is
rather easy, too, since we can see how this parameter is a full
application of op, which we already know returns a value of type z.
So z it is.
Folds, with their origami name reminding us that we are just giving
a different shape to all the material present in a datatype, are the
higher-order functions through which we can transform a datatype
in its entirety to something else of our own choice. That’s it.
But we talked about foldr being the right fold of a list, right? Yes we
did, and intuitively, that means that when we write something like
foldr (+) 0 [2, 4, 3], we end up with 2 + (4 + (3 + 0)), where the
parentheses seem to be accumulating to the right side of this
expression. But this is not the only way to fold a list, no. For
example, we also have the left fold of a list, which we call foldl, and
intuitively, accumulates parentheses on the left. That is, foldl (+) 0
[2, 4, 3] would result in ((2 + 4) + 3) + 0. Now, this difference is
not particularly important if we are trying to add our numbers
together, because whether the parentheses are to the left or to the
right doesn’t matter at all to our addition operator +. Both 2 + (4 +
(3 + 0)) and ((2 + 4) + 3) + 0, if we try them in our calculator, will
result in 9. However, many other times it does matter. For example,
when we try to use [] and (:) as our unit and op. In this case, foldl
(:) [] would in theory result in ((2 : 4) : 3) : []. However, that’s
nonsense that doesn’t even type-check. Think about it. When we
say 2 : 4, for example, we are trying to apply the (:) to two values
of type Natural. But we know that if the first input parameter to (:)
is a Natural, then the second must necessarily be a list of them, of
type [Natural]. So this just fails to type-check, meaning that our
choice of foldl vs foldr matters here.
The reason why + and * succeed here is that they are associative
operators, whereas (:) is not. We saw this before, but just as a
reminder: A function f is associative if f a (f b c) and f (f a b) c
are equal. Or, arranging things in an infix manner as in the case of
+, we say that an operator + is associative if a + (b + c) equals (a +
b) + c. The “cons” operator (:) is not associative, because a : (b :
c) is not equal to (a : b) : c. Actually, this last example doesn’t
even type-check, so any discussion about associativity or equality is
moot.
Lies. What’s happening to the units of our foldl are lies. Well, kind
of. When we introduced foldr, the relationship between its unit and
the idea of the identity element of a monoid was so obvious, that
we decided to profit from this accident by taking the opportunity
to explore it. You see, monoids are important, they bring clarity to
everything they touch, and luckily for us they touch many, many
things. And yes, we’ll divert our attention to a monoid whenever
we see one, and we’ll see quite a few of them. So get used to the
idea, profit from it, and learn to recognize those monoids. But no,
monoids and folds don’t have much to do with each other after all.
In fact, to give a concrete example, (:) and [] are not a monoid, yet
we were able to foldr (:) [] just fine. So let’s refine our perspective
to understand the truth that’s unfolding here.
77. Accumulate
Folding lists abstracts the idea of iterating over their elements.
Rather than rewriting all the recursive machinery each time we
need to transform a list, here we have some higher-order order
functions that will do the boring bits for us. And we don’t tackle
the entire list at once. Instead, we address the elements of the list
one by one. This allows us to focus, at each step, only on what is
important. But we don’t do this in isolation, no. If we did, we
would be looking at something more like fmap, which allowed us to
treat each element of our list oblivious of that element’s relationship
to the rest of the list. That’s not a bad thing, though. Not knowing,
actually, is what we love about fmap and functors. But folds are
different, the are a more nosy, neighbourly abstraction. Let’s take a
look at foldr again.
Let’s try and follow the execution of foldr step by step, and let’s
reduce any expresions that can be reduced, such as 5 + 1 into 6, as
soon as we encounter them. As a high-level overview, here goes a list
of all the reduction steps in this expression, written line by line, so
that we can more easily visualize how we get from foldr (+) 0 [2,
4, 3] to 9.
And now we do the same process all over again, but this time
focusing on 4, and with 3 rather than 0 as the initial value of our
accumulator. op, that is (+), is looking at the number 4 within the
context of 4 : rest, and it knows that the parameters it’s getting as
inputs are 4 itself and the accumulated result of right-folding that
rest, which we just said is 3. All (+) needs to do now is improve this
result somehow, which, as before, it does by adding the element
and the accumulated result togeter. So 2 + (4 + 3) becomes 2 + 7,
and finally we do the same once again to improve 7, the result of
right-folding 4 : (3 : []), by adding it together to the leftmost
element of our list, 2, who has been waiting for us to finish this trip
all along. Incredibly patient, that 2. And at last, we arrive to our
longed numerical result, 9.
foldr saw [] as the beginnig of the list, which caused it to recurse its
way from [2, 4, 3] to [] so that it could finally start operating on
those list elements, starting from the rightmost 3, with the
accumulator always being the rightmost input to op. But foldl, on
the contrary, sees the list’s most recently added element as its
beginning, so it can start operating on lists elements right away,
recursing with the accumulator as the leftmost input to op until the
list has been exhausted, at which point the result accumulated so far
is simply returned.
The result is the same 9 in both cases. foldr, however, did much
more work to get there. Twice as much. So why would we ever use
foldr? Well, for two reasons. First, because whenever op is not an
associative operation, the results will definitely not be the same. We
witnessed this when we picked the non-associative (:) as our
operator. And, somewhat tangential, do you remember that thing
we said about monoids bringing clarity to everything they touch?
Well, this is another situation where they do. The operation of a
monoid, like + or *, is always associative. There is no such thing as a
monoid whose operation is not associative, which, at least for
semantic purposes, takes the weight of picking between foldl and
foldr off our shoulders. And second, foldr is necessary because it
allows us to work with infinitely long lists, whereas foldl just can’t
cope with that. That’s right. We haven’t seen this yet, but Haskell
has no problem at all in representing infinitely large datatypes, and
by means of foldr we can operate on them within a finite amount
of time and space. Yes, Haskell can do that.
What a ride. What a boring ride. Good thing we almost never have
to follow through the execution of a program this way. This is the
kind of stuff we let computers and type-checkers do for us. What
matters is understanding the ideas and interesting concepts such as
folding, developing an intuition for them, and learning how to
tickle the computer in the right place so that it’ll do them for us.
Our ultimate goal is for our knowledge and skills to become latent,
for that’s how we get to spend time worrying about the important
things instead. There will come a time in our journey when we
won’t need to worry about details unless we really want to. We will
have decoupled meaning from execution, it will be our reward.
78. Pancake
Here’s another function for you to think about and implement on
your own. And try to come up with a less distracting name for it, it
will help.
We take the white plate, the one most recently added to the pile,
and put it on the side. Now we have two piles of plates, rather than
one. There’s one with a red plate on top of a black plate, and
another one with just a white plate. Next, we grab the red plate and
put it on top of the white plate. Now we have a pile with the red
plate on top of the white, and the original pile with just the black
plate left in it. Finally, we take this black plate and put it on top of
the red one. Now the original pile is empty, and the new pile has
black on top of red, on top of white. We have reversed the order of
the plates.
Linked lists are like these piles of plates. Or worse, perhaps. Imagine
you can only look at this pile from above, so all you see is the plate
at the top of the pile, the one that was added to it more recently,
and you can’t tell how many plates are below it. Maybe there’s one,
maybe there are none, or maybe there’s infinitely many of them.
You can’t see what’s beneath the last added plate without first
taking it out of the pile. Last in, first out. That’s what linked lists
are.
Like we learned, we achieve this by taking the plate that’s at the top
of the stack, the leftmost element of our list, and putting it on top
of the new pile we are building instead. In Haskell parlance, the list
we are building is our accumulator, and the leftmost element is the
element to which foldl applies its accumulating function. So let’s
write that accumulating function for foldl, let’s have it put the
element on top of the accumulated list.
That’s it. Yes, reverse does the trick. Here’s how the reduction of
reverse [1, 2, 3] happens, written using the verbose infix
constructor : to make a point.
reverse (1 : (2 : (3 : [])))
== foldl (\acc a -> a : acc) [] (1 : (2 : (3 : [])))
== foldl (\acc a -> a : acc) (1 : []) (2 : (3 : []))
== foldl (\acc a -> a : acc) (2 : (1 : [])) (3 : [])
== foldl (\acc a -> a : acc) (3 : (2 : (1 : []))) []
== 3 : (2 : (1 : []))
Elements flock from the input pile to the new pile acc as we peel,
one by one, the “cons” constructors (:). And, in case you didn’t
notice, \acc a → a : acc is just flipping the order of the input
parameters to (:). If you think of (:) as a function of type a → [a] →
[a], then \acc a → a : acc is a function of type [a] → a → [a]. All
that’s happening in our chosen op is that the order of input
parameters to (:) are being flipped. Does this remind you of
something?
reverse [1, 2, 3]
== foldl (flip (:)) [] [1, 2, 3]
== foldl (flip (:)) [1] [2, 3]
== foldl (flip (:)) [2, 1] [3]
== foldl (flip (:)) [3, 2, 1] []
== [3, 2, 1]
But let’s write this using (:) and [] instead, adding superfluous
parentheses and arranging things differently so that Prägnanz can
do its thing.
== (1 : (2 : (3 : (4 : []))))
That’s right. All that concat is doing is replacing the [] in its first
argument, 1 : (2 : ([])) with the entirety of its second argument 3
: (4 : ([])). Everything else stays the same, all we do is replace [].
Replace [], replace [], couldn’t foldr replace []? Yes, yes it could. In
foldr op acc as, that acc ultimately replaces the [] in as. Well then,
let’s put this to use in concat. All we need to do is make sure the
second argument to concat becomes that acc.
concat :: [a] -> [a] -> [a]
concat = \as bs -> foldr _ bs as
That’s it. That’s concat. Try it. Use equational reasoning to prove
the concatenation of any list you want. Actually, it’s quite
interesting to see what happens when either as or bs are the empty
list [], so maybe let’s do that exercise here. Let’s see it for bs first.
That is, let’s use equational reasoning to see how concat as []
reduces.
concat as []
== foldr (:) [] as
== as
Wait, what? Well, yes, we said many times before that foldr (:) []
was the same as id for lists, so here we are essentially asking for the
identity of as, which is indeed as. And what about making [] our as
instead?
concat [] bs
== foldr (:) bs []
== bs
We didn’t say it explicitly before, but it turns out that \bs → foldr
(:) bs [] is also an identity function for lists. Think about it, foldr
op acc [] has no more elements to process, so it simply returns acc,
the result accumulated so far, unmodified. So if we make bs our acc,
concat [] bs will effectively return that same bs given as input. In
other words, the following is true.
First, we know that concat [] xs and concat xs [] are the same as xs.
Generalizing this a bit, we can say for any three lists a, b and c, this is
true:
There is a name for this, and we know it. Perhaps it would be a bit
easier to see if concat, rather than being a function expected to be
used in prefix manner, was an operator intended for infix usage
such as + or *. Let’s have that infix operator, let it be ++. That is, two
plus symbols.
(a ++ b) ++ c == a ++ (b ++ c)
a ++ [] == a
[] ++ a == a
Superficially, things might look a bit messier, but essentially, all the
type of concatenation is saying is that the contents of the list we
want to concatenate are irrelevant. Our a, universally quantified,
could be blackholes or rainbows. It wouldn’t matter, for we’d be
able to concatenate them nonetheless.
In concatenation we have the surprise [a] all over the place, and in
addition we have the same Natural everywhere too. Both monoids
work just fine, but repeating the same thing over and over again is
tiresome. Let’s avoid this by using a custom product type, rather
than Haskell’s tuples.
It looks neat. Rather than repeating our chosen type every time, we
just repeat it once inside the definition of the Monoid datatype,
where we have a value constructor also called Monoid with two fields
in it: One with a value of the mystery type x mentioned as the type-
parameter of the Monoid type-constructor, and another one with a
function making reference to that same x.
Alright, we have Monoid. What can we do with it? Well, for example,
we said that while foldl and foldr don’t really need a monoid to
work, we can most certainly fold a list according to a monoid’s
intention if we have one. Let’s try and wrap foldr, say, so that it
takes a Monoid x rather than separately taking an initial accumulator
and an accumulating function.
With mconcat in place, we can redefine our sum and product from
before in a semantically more agreeable way.
It’s the type-checker who decides what compiles and what doesn’t,
so all we need to do is speak its language, types, and ask for it.
Luckily for us, we have a good starting point. Our monoids always
refer to some type in them. Both addition and multiplication talk
about Natural, say, and concatenation talks about [a]. But therein
lies our problem.
It is true that Natural numbers can be added, yes, and it’s also true
that they can be multiplied. All that is true. But we can enrich these
numbers a bit, with their intentions, and let the type-checker worry
about any misplaced expectations.
And, by the way, we just accidentally proved that Sum and Natural
are isomorphic. Using Sum and unSum we can convert between them
without any loss of information. Isn’t that nice? Not all accidents
are bad.
Here, a and b are of type Sum rather than Natural, but the whole
thing means the same anyway, so it doesn’t really matter which
version we choose.
We need to change sum so that rather than having the type [Sum] →
Sum, it can keep its previous type [Natural] → Natural. That is, we
need to modify the types of both the input and the output of sum.
Alright, that’s not a problem, we know how to do it.
dimap :: Profunctor p
=> (a -> c) -> (b -> d) -> p c b -> p a d
Don’t let the rather big type scare you, what’s happening is really
easy. Let’s start from the very end. This use of dimap returns a value
of type [Natural] → Natural, which is exactly the type we want sum to
have. Great. Then, we have an argument of type [Sum] → Sum, which
is exactly the type that mconcat addition has. Excellent. Let’s start
writing this down.
sum :: [Natural] -> Natural
sum = dimap _ _ (mconcat addition)
We will use unSum as our value of type Sum → Natural. There’s not
much to say about it beyond what we have already said. unSum is
here and it has the right type, so let’s just use it.
What about the other placeholder? The function that takes its place
is expected to convert every element in a list of Naturals into a value
of type Sum. We know how to convert one Natural into a Sum by
simply applying the Sum value-constructor, alright, but an entire list
of them? Crazy stuff.
Just kidding. Remember lists are a Functor too, meaning we can use
fmap, of type (a → b) → ([a] → [b]), to lift the Sum value-constructor
of type Natural → Sum into a function of type [Natural] → [Sum],
which is exactly what we need.
Beautiful. Now sum has exactly the same type as before, but
internally it relies on Sum and the addition monoid. Of course, we
could have written this in a different way, but any difference would
be just superficial. The fundamental issue of converting between
Sum and Natural back and forth will always be present.
Yes, of course we could mix them up, but since we are not dealing
directly with Naturals anymore, making that mistake would involve
three accidents, rather than one, for we’d also need to mistake fmap
Sum for fmap Product and unSum for unProduct. Chances are minimal.
How is this better? Let’s see. First, and unrelated, notice that this
typeclass is the first one we see having more than one method. This
is fine. Just like product types can have many fields, typeclasses can
have many methods too. Here, the Monoid typeclass has two
methods named mempty and mappend. And if we compare this
typeclass with the Monoid datatype we defined before, we’ll see the
resemblance is remarkable.
It’s not what we have gained from this instance what matters, but
what we’ve lost. Which, actually, is also what we gained, so I guess
it’s both. Anyway, addition, the previous definition of our monoid,
is gone. Poof. We don’t need it anymore because its code lives in
the Monoid instance for Sum now. And this is perfect, because while
previously we intended the Sum type to determine the meaning of
our monoid, in reality it was addition, who happened to mention
Sum, who did. But now Sum must decide who it wants to be, for there
can only be one instance of the Monoid typeclass for Sum, and
whatever that one instance decides, will be the monoidal behavior
of Sum. It will be the truth.
So let’s profit from this. Let’s see, for example, how the sum
function, the one that adds Natural numbers, would be
implemented using this function.
The only thing that changed here, compared to our last definition
of sum, is that we are not applying mconcat to addition anymore. So
how does mconcat know that it needs to add the elements in our list?
The answer is in mconcat itself.
And something else. Considering how monoids have laws, and how
we appreciate seeing laws in typeclasses so that we know all their
instances behave as expected, the Monoid typeclass in Haskell is
accompanied by some comments requiring compliance with two
laws. First, an identity law, whereby mempty is required to behave as
an identity element:
mappend mempty x == x == mappend x mempty
foldMaybe :: ∀ z. … -> z
First, we’ll make sure foldMaybe is given a z that it can use whenever
it encounters the Nothing constructor, analogous to the empty list
constructor [].
Yes, that’s it. This is foldMaybe. Let’s put it next to foldr so that we
can compare their types.
That is, it wouldn’t have used f at all. And how do we know what’s
expected of a well behaved maybe? Well, we don’t. It really is up to
the designer of this function to come up with a description for it
and make sure its implementation matches that description.
However, our fluffy intuition for what a “fold” is should suggest
some sensible ideas. We could say, for example, that the following
equality is expected to be true:
y == maybe Nothing Just y
Indeed. In logic, things are either true or false, and Bool correctly
represents that. For example, we could use a boolean as the answer
to “is this list empty?”.
isEmpty returns True when the given list is [], and False whenever it’s
not. Remember, that wildcard _ will successfully pattern-match
against anything that has not successfully pattern-matched before.
There’s not much more to say about Bool itself. It’s just a rather
boring datatype with two possible values.
We expect bool to replace True with one of the z inputs, and False
with the other one. Which one, though? Which of the following
implementations of bool is the correct one? And to make it a bit
more interesting, let’s say that if we pick the wrong one, we die.
Thus spoke identity, with a very weak voice. This time, nothing
mandated this choice.
That’s ugly. It gets the job done, sure, it only returns True if both a
and b are True, but it is nonetheless ugly. Let’s try to clean this up.
First, let’s realize that as soon as we discover that a is False, we can
just return False without having to look at b.
and :: Bool -> Bool -> Bool
and = \a b ->
case a of
False -> False
True ->
case b of
False -> False
True -> True
Great, that’s looking better. But could we use our bool fold instead?
Sure, why not.
This is true, and in general this is right. But in Haskell, this is wrong
too.
92. Laziness
Contrary to almost every programming language out there, Haskell
is a lazy one. And contrary to civil prejudice and family values,
being lazy is often quite good.
Haskell is lazy like that. When we ask for the conjunction of two
Bool values, when we say and a b, we are asking for them to be
considered and eventually conjoined in that order: First a, then b.
That is, when we ask for the logical conjunction of we_have_utensils
and we_have_ingredients, we want Haskell to avoid us that trip to
the market if possible by first evaluating whether we_have_utensils,
and only if this is True, force a trip to see whether
we_have_ingredients or not. However, when we implemented and as
bool False, we broke this.
we_can_cook :: Bool
we_can_cook = and we_have_utensils we_have_ingredients
we_can_cook
== and we_have_utensils we_have_ingredients
== bool False we_have_utensils we_have_ingredients
== case we_have_ingredients of
False -> False
True -> we_have_utensils
So we just change the order of the arguments, right? That is, we say
and we_have_ingredients we_have_utensils rather than and
we_have_utensils we_have_ingredients. Yes, sure, that works.
However, booleans and this left-to-right way of dealing with them
lazily have become so ingrained in programming, that flipping the
order of these arguments in order to accommodate and encourage a
sociopath and might not be a sane long-term choice. Can we fix it?
Of course we can. All we have to do is go back to that time when
and was behaving perfectly fine, before we decided to play with
commutativity. We can accomplish this by reusing one of our
previous definitions, or by using flip to swap the order of the input
parameters in our current definition.
zeros :: [Natural]
zeros = 0 : zeros
The first thing to keep in mind is that just like how defining
undefined, an expression that looped itself to oblivion, didn’t make
our programs crash, merely defining zeros doesn’t make our
program loop forever chasing its tail either. Essentially, zeros is a list
of type [Natural] obtained by applying (:) to the Natural number 0
and to a recursive use of the name zeros. The type-checker accepts
this because zeros has type [Natural], which is exactly what our use
of (:) expected. Let’s use equational reasoning to see how the 0s in
zeros come to be.
zeros
-- We inline the definition of 'zeros'
== 0 : zeros
-- And then we do the same again and again …
== 0 : (0 : zeros)
== 0 : (0 : (0 : zeros))
== 0 : (0 : (0 : (0 : zeros)))
== 0 : (0 : (0 : (0 : (0 : …))))
zeros :: [Natural]
zeros = <thunk>
Mind you, this is not proper Haskell. We are just using <thunk> to
pinpoint where Haskell sees a thunk.
This function, isEmpty, doesn’t really care about the contents of our
list. All it wants to do is scrutinize the outermost constructor, the
one farthest from [], and check whether it is [] or something else.
Once we apply isEmpty to zeros, we get our False result as expected,
but something happens to zeros as well:
zeros :: [Natural]
zeros = <thunk> : <thunk>
It might help to learn how programs run once they have been
compiled. Essentially, every executable program has some sort of
magical entry point, a function, that will always be evaluated in full
so that the computer can find there the orders it will blindly follow.
So if we decide to use isEmpty zeros within this function, this would
force a Bool out of our isEmpty zeros thunk, which would in turn
force zeros to be evaluated to weak head normal form. But none of
this matters for the time being, we are not in a rush to run anything
anywhere, we are lazy, so let’s go back to our thunks.
95. Normal form
Let’s continue with the assumption that zeros has been evaluated to
the extent it needed to be evaluated in order for isEmpty zeros to
deliver its False result. That is, let’s assume that zeros is known to be
<thunk> : <thunk>.
zeros is in weak head normal form, for “cons” (:) is known, yet
acquiring this knowledge didn’t force the evaluation of any of the
thunks that are mentioned as its payload. But what about the
expression isEmpty zeros itself, known to be False? Is it also in weak
head normal form? Surprisingly perhaps, yes, yes it is, because
discovering False didn’t force the evaluation of any other thunk
within this constructor. Sure, there was never a thunk inside False
that could be evaluated to begin with, seeing how this constructor
has no room for carrying another expression as payload, but that’s
beside the point. Nevertheless, this scenario of an expression being
fully evaluated, like False, with no thunks in it at all, is curious
enough that it has its own name. Very confusingly, we say that
False is an expression in normal form, and this is in addition to its
being in weak head normal form.
zeros :: [Natural]
zeros = <thunk>
When we apply sum to zeros, which inside sum we call xs, the first
thing that happens is that a case expression scrutinizes this xs in
order to decide where to go next depending on whether we
encounter [] or (:). This forces xs to be evaluated to weak head
normal form. Encountering [] means we are done, but in our case
we know this will never happen because the list is infinite and
there’s no [] in it, so we pattern match on x : rest instead. Up
until this point, zeros has been evaluated to <thunk> : <thunk>, just
like in isEmpty zeros. However, we are not finished yet.
length [9, 4, 6]
== 1 + (length [4, 6])
== 1 + (1 + (length [6]))
== 1 + (1 + (1 + (length [])))
== 1 + (1 + (1 + 0))
== 1 + (1 + 1)
== 1 + 2
== 3
Indeed, 3 is the length of [9, 4, 6]. But what about the length of an
infinite list like zeros? How do we calculate it? Well, we don’t.
Intuitively, expecting a finite number measuring the size of an
infinite structure is kind of silly, isn’t it?
If you look hard enough, you’ll notice that the only difference
between sum and length is that whereas sum adds together the
elements of the list themselves, length adds a 1 instead, each time it
encounters an element. That’s all. In other words, it ignores the
elements themselves, it never tries to evaluate them, they stay inside
their thunks. Still, the use of 1 + length rest forces length rest itself
to be evaluated to normal form, which diverges just like sum rest
did, because given our defition of zeros as 0 : zeros, rest and zeros
are really the same. So length rest means length zeros, which is
exactly what we are failing to evaluate to normal form at the
moment, so our program goes into an infinite loop.
Anyway, the fact that length doesn’t try to evaluate the elements in
the list implies that length, contrary to sum, is not trying to force the
given list to normal form. Yet, both sum and length diverge. In other
words, it doesn’t take a normal form evaluation to have an infinite
structure blow up, all it takes is being strict enough, so be careful.
Would head work with an infinite list like zeros? Well, let’s see. head
scrutinizes the given list so as to tell apart [] from (:), just like
isEmpty did. At this point, zeros has been evaluated to <thunk> :
<thunk>. Next, head completely disregards rest, which in the case of
zeros means just zeros. This is great. It means that we don’t have to
worry about the infinite zeros anymore, which suggests that yes,
head works with infinite lists. And what about a? Well, we just put
it inside another constructor, Just. This doesn’t force the
evaluation of a either. It simply preserves the evaluation state of a,
thunk or not, inside a new constructor. But even if this did force
the evaluation of a, head would still be able to cope with infinite
lists, since it’s never a in a : as who can make our programs diverge.
Well, except when a is undefined or similar, but why would it be?
head zeros, when evaluated, safely results in Just 0.
98. Equations
Alright, using head we are able to extract the first element of a list.
But lists can have more than one element in them, so in theory we
should be able to write a function that takes the first few elements
of a list, rather than just one of them. Let’s write this function, let’s
call it take.
Actually, no. No, no, no. This is just too ugly. Let’s clean it up
before trying to make sense of it.
If you recall, we said that foldr was able to cope with infinite lists,
so in theory we could rely on foldr to implement a fine looking
take. And yes, yes we could, but the solution would be a bit tricky
to follow, so maybe let’s try something else first. In Haskell, the
following two definitions of the id function are the same:
id :: x -> x
id = \x -> x
id :: x -> x
id x = x
That is, rather than binding the input value to the name x as part of
a lambda expression, we do it on the left-hand side of the equals
sign =. This is another syntax for defining functions, different from
the \… → … syntax we’ve been using so far. But mind you,
conceptually, from the point of view of the lambda calculus, id is
still a lambda expression, a function, and will be treated as such.
This is mostly just a syntactic difference, not a semantic one. And
why do we have two ways of defining functions? Well, it can be
convenient at times, we’ll see. We call these alleged cosmetic
improvements syntactic sugar. Haskell is sweet and sticky like that.
There are two interesting things about these equations. First, not
only can they bind expressions to a name, they can also pattern-
match on them. For example, let’s redefine that isEmpty function
from before, but this time using equations.
Ah, that’s nice. We are saying that if the input to this function is
the empty list [], we return True, and if it’s anything else, as caught
by the wildcard _, we return False. Just like in case expressions, the
different patterns are tried from top to bottom, and the first one
that matches gets to decide what happens next.
Sweet, sweet sugar. What take is doing is quite clear now. If the
given list is empty, we won’t be able to select any elements from it,
for there are none, so we just return another empty list [].
Incidentally, notice that as we completely ignored the first
parameter to this function, we didn’t force its evaluation. If it was a
thunk, it would still be a thunk.
take 2 zeros
== -- 3rd equation
0 : take 1 zeros
== -- 3rd equation
0 : (0 : take 0 zeros)
== -- 2nd equation
0 : (0 : [])
== -- Syntactic sugar
[0, 0]
Similarly, we can see how take 3 [4] has an early encounter with []
as we hit the first equation, even though 3 demanded more.
take 3 [4]
== -- 3rd equation
4 : take 2 []
== -- 1st equation
4 : []
== -- Syntactic sugar
[4]
Alright, take works. take takes a list, infinite or not, and consumes it
to the extent that it needs to do so without diverging. This is
already a useful achievement, but perhaps more importantly,
contemplate how this lazy consumption of infinite structures
allows us to keep the beautiful definition zeros as it is, reading like
poetry, and take too, with its striking straightforwardery. But the
real treat, actually, is how take lazily produces a list while lazily
consuming another one.
This looks cute, but it is actually wrong. The problem is the same as
before: and shouldn’t be trying to scrutinize its second parameter
until it has confirmed that the first one is True, because this forces it
into normal form, which will in turn make us do that trip to the
market, perhaps in vain. We need to avoid scrutinizing the second
Bool until we are certain we need it.
Do you recognize this new take? Pay close attention. That’s right,
it’s the identity function once again. So be careful about the order
in which you write your patterns, or you might end back where you
started.
Moreover, in case you didn’t notice, our broken take with its
equations rearranged leads to the silly situation of trying to apply n
- 1 even when n is 0, in which case the resulting value will not be a
Natural number, for there’s no natural number less than zero.
Unfortunately, this type-checks and the problem goes completely
unnoticed because the evaluation of this Natural never happens.
Sure, take is failing in other ways too, so hopefully that makes our
program collapse sooner rather than later, but we can’t guarantee
that. Nothing good can come out of this.
100. Types, please
There are two reasons why our broken take was trying to come up
with these non-existent “negative natural numbers”. One could be
seen as an accident, but the other one was the irresponsibility that
allowed the accident to take place.
Now, if you recall, the smallest Natural number that can possibly
exist is 0. However, the type of (-) allows us to say things like 3 - 4,
where both 3 and 4 are perfectly acceptable Natural numbers, yet the
expected result, also of type Natural, can’t possibly exist. There is no
such thing as a “negative one natural number”, as 3 - 4 :: Natural
would want us to believe. This is the problem our wronged take
accidentally faced. But the culprit was not take, it was (-) who
enabled the accident in the first place. So let’s fix (-), let’s change its
type. Or maybe let’s keep (-) as it is, for reasons that will become
apparent later on, but let’s write a wrapper around it that will
perform the subtraction safely. Let’s call it safeSubtract, and then
let’s have take use it.
Hey, look at that, it worked. And, actually, since this third equation
is already dealing with the fact that n might be 0, perhaps we can
drop that second equation altogether. Let’s see.
Hmm, no, that’s not quite right. For example, consider take 0 [5,
3, 8].
take 0 [5, 3, 8]
== 5 : case safeSubtract 0 1 of
Nothing -> []
Just m -> take m [3, 8]
== 5 : []
== [5]
That is, we are selecting the first element of our list before
scrutinizing safeSubtract n 1 to see if we were asked for any element
at all. Now, this is not necessarily a bad thing. Perhaps this is what
we wanted take to do. It is important to tell apart “mistakes” like
this one, where we are successfully repurposing a function, from
actual silly ideas such as allowing “negative natural numbers” to
exist. We said it before: At some point we need to tell the computer
what to do, and it’s not the computer’s responsibility to judge
whether we asked for the right thing or not. Of course, as much as
possible, once we know what it is that we want to solve, we want
the type-checker to help us achieve this and nothing else. However,
the type of take says nothing about its intended behavior, so in
return we don’t get any help from the type-checker and make
“mistakes”. But don’t let this scare you, because we can be precise
enough in our types so that it becomes impossible to write a
misbehaving take. We will get there. This book is all about that,
after all.
Ah, yes, this works as expected, just like our very first take. Is it
better? Well, no, why would it be? Improving take was never our
goal. Our goal was to make Natural subtraction safe, and that is all
we did.
And by the way, perhaps surprisingly at first, we could have
implemented safeSubtract as follows:
This works just fine because even if it looks like we are trying to
subtract a - b right away, in reality, due to the laziness of bool, this
subtraction doesn’t even happen unless it is a < b is known to be
False. Ultimately, this is why we cherish laziness. It’s not so much
about the lazy lists nor the other lazy structures, but about how
expressive and ergonomic our programming experience becomes
when we don’t have to worry about when something will be
executed. We write our expressions optimistically, and leave it up to
Haskell to figure out if and when they deserve execution. It’s so
comfortable that we sometimes forget this even exists, even after it’s
taken over our ways.
101. Closure
Adding two natural numbers results in another natural number.
Multiplying two natural numbers results in another natural
number. Subtracting two natural numbers, however, does not
necessarily result in another natural number. Specifically,
subtracting a - b results in a natural number only if a is greater
than, or equal to, b. There is a pattern here, this is not an accident.
Monoids too, for example, are closed under their mappend operation
of type a → a → a, for some choice of a such as Sum or [x].
Really? I don’t think so. We just said that the integer numbers
include all of the natural numbers plus their negative counterparts.
In other words, we are saying that the naturals are a subset of the
integers, meaning that rather than having Either Integer Natural,
artificially segregating the natural subset of the integers in this sum
type, maybe we could just use Integer as the return value of
safeSubtract, as Natural → Natural → Integer. We could, we could.
However, since we are now returning Integers, we might as well
take Integers as input so that we can do things such as safeSubtract
-3 7. What we really want is a function of type Integer → Integer →
Integer that works perfectly fine because the integer numbers,
contrary to the naturals, are closed under subtraction. Of course, if
now we want to subtract Naturals through this function, we’ll need
to convert them to Integers first. That’s easy, though, considering
how naturals are a subset of the integers, meaning that any Natural
can be represented as an Integer too. In fact, Haskell comes with a
function toInteger of type Natural → Integer that does just this.
The polymorphic type of (-) says that you can subtract values of
types for which there is a Num instance, such as Integer and Natural.
So yes, we can use (-) to subtract integer numbers too.
Our new takey delivers the result it promises, and even deals with
infinite lists just fine. So what’s the problem? What’s happening is
that in takey's third equation, before making use of the lazy
constructor (:), we are forcing the evaluation of takey (n - 1) rest
by scrutinizing it through a case expression. Otherwise, how would
case know whether to return Just or Nothing without first learning
whether Just or Nothing is the result of the recursive call to takey
itself?
takey took the laziness take had away, for take never had to
scrutinize its tail in order to construct a list with an x and a mere
promise that there could be more xs, thus preserving the list’s
laziness. Remember, a list doesn’t really need to know about its
contents, not even when it’s being constructed. It is only when we
try to observe it beyond its weak head normal form that we start
forcing secrets out of its thunks.
And why do we care about preserving laziness? Well, that’s the
wrong question. The right question is: Given that lists can be
produced lazily, allowing lazy consumptions to be performed
efficiently at a later time, if at all necessary, why wouldn’t we allow
so? We don’t know what the future may call for, nor what this
laziness may allow. We shouldn’t arbitrarily restrict things just
because we can. Not unless we can justify these constraints.
104. Eq
Many times, though, we need to be certain that our list has an exact
number or elements in it. But even in those cases we may want to
keep the core functionality of take as it is, and use it in combination
with another function like length to ensure that the obtained list is
of a particular length. Generally speaking, we’d rather avoid this
because there are data structures other than lists that are more
efficient at knowing their own lengths. Remember, a list must
iterate itself completely to learn its length, it must uncover its spine,
which is certainly a more expensive computation than just reading a
number somewhere saying “oi, it’ll be one-thousand seventy-five
alright, the length”. Alternatively, we could modify lists so that they
carry this length information with them somehow, and we will do
so later on, it will be fun, but for now let’s explore this whole idea
using our boring and lazy linked lists.
We are using the first case expression just to give take n xs a name,
as we’ll be using it twice afterwards. Whatever take n xs may be,
we’ll call it ys. Then, we are checking whether the length of ys
equals n as requested. When this is True, we return ys wrapped in
Just. Otherwise, Nothing.
We are using the infix function (==) for the first time here. This
function returns a Boolean value indicating whether it is True or
False that its two arguments, here length xs and n, are equal.
Ah, some new noise trying to distract us. Let’s take this step by
step. First, the high-level overview. Eq is a typeclass for Types with
two methods in it, just like the Monoid class from before. One named
(==) and the other (/==). However, there’s also a pragma, a special
instruction that the authors of Eq wrote for the compiler, that says
when we implement an Eq instance for a type of our choice, we
don’t really need to implement both methods. Rather, we can get
by implementing only the minimum requirements. Namely, either
(==) or (/=). That is what the {-# MINIMAL (==) | (/=) #-} pragma is
saying. Why, though?
Contrary to typeclasses we’ve seen before, both (==) and (/=) come
with default definitions that will serve as the implementation of
these methods for the instances of Eq in which we have chosen not
to override some of these methods. In particular, a == b is defined
to be not (a /= b), and vice-versa.
not negates a boolean value. not True is False, not False is True. So
what a == b = not (a /= b) is saying, concretely, is that comparing a
and b for equality using a == b equals the negation of a /= b, which
suggests that a /= b tells us whether a and b are different, rather
than equal. And, indeed, if we look at the default definition of a /=
b as not (a == b), we can see that this conveys the idea of a and b not
being equal.
As for why equality (==) and non-equality (/=) are both part of this
Equality typeclass: It is mostly a matter of convenience. Sometimes
it’s easier or faster to decide that two things are equal, while other
times it’s easier to notice that they are different. We are studying
this Eq because it gives us an opportunity to showcase the role of
default method definitions and MINIMAL pragmas in typeclasses, but
conceptually, we could have Eq, (==) and (/=) defined this other
way, and it would have been enough:
That is, we only kept the (==) method in the Eq typeclass —without
a default definition, because there’s not one definition that could
possibly work for all choices of x— and we converted (/=) into a
function that simply negates whatever (==) says. The Eq x
constraint on (/=) requires that there be an Eq instance defined for
the chosen x. Otherwise, without it, we wouldn’t be able to say a ==
b within the definition of a /= b.
We used a strange syntax this time to define our infix operators, but
there’s nothing special going on beyond a high dose of syntactic
sugar. All the following mean the same thing:
a == b = not (a /= b)
(==) a b = not (a /= b)
The type of the list changes right away, though. We are always eager
to know the types of expressions, even if they don’t exist yet. This
makes complete sense, considering how the type-checker knows
nothing about the evaluation of expressions, so it couldn’t possibly
care about whether they have been evaluated or not. Types are
types, expressions are expressions, and they have different concerns.
Well, kind of. There will come a time when we’ll blur that
boundary too, but it won’t be today.
So yes, fmap is also one of those functions that lazily produces a list
while lazily consuming another one. It could choose not to do so,
but what would be the point of doing that? It’s only when
somebody needs to observe one of the elements of the produced list
that the application of f will be evaluated. Only when one needs to
iterate over it will its tail be uncovered insofar as necessary.
Can we fmap infinite lists? Sure we can, why wouldn’t we? Infinity
is not an alien concept in Haskell, it’s reasonable to expect things to
work perfectly fine with infinite structures most of the time, unless
something in their description suggests otherwise. For example,
here is an infinitely long list of eights, obtained by replacing each
element in the infinitely long zeros with an 8.
eights :: [Natural]
eights = fmap (const 8) zeros
This funny-looking code works just fine. Well, except for the
pointy labels which are there just to make a point. Ignore them.
naturals, of type [Natural], is indeed an infinite list of natural
numbers. Every natural number that exists is somewhere in this list.
That is, naturals gives us a never ending list starting with 0 : 1 : 2
: 3 : ….
naturals :: [Natural]
naturals = 0 : fmap ((+) 1) naturals
This, I’m told, is a bit easier on the eyes. And, well, as long as we are
cleaning things up, we could also partially apply (+) to 1 in infix
form instead, as (1 +). Notice how + is not isolated in its own pair of
parentheses anymore. With this syntactic sugar, saying (1 +) x is the
same as saying 1 + x or (+) 1 x. That is, the operand that appears
partially applied to the left of + will remain on the left once (+) is
fully applied to its two arguments. Alternatively, we can partially
apply the operand on the right as (+ 1), which when used as (+ 1) x
will preserve 1's rightful place as x + 1. In the case of addition, a
commutative operation, it doesn’t matter whether we say (+ 1) x or
(1 +) x because the result is the same either way. But this syntactic
sugar works for any infix operator, and the order of the arguments
might make a difference for them. For example, compare (x :) xs
and (: xs) x, both meaning x : xs. The order of the arguments to
the list constructor (:) does matter.
naturals :: [Natural]
naturals = 0 : fmap (1 +) naturals
0 : fmap (1 +) naturals
0 : (1 : fmap (2 +) naturals)
0 : (1 : (2 : fmap (3 +) naturals))
We could repeat this process over and over again, but I trust seeing
the first handful of Naturals come to be is more than enough proof
that naturals in fact contains an infinite amount of Natural
numbers in it, all of them, starting from 0 and incrementally
growing by one. There’s nothing in our reasoning suggesting we
will ever stop lazily producing new numbers.
107. Cavemen
So far we’ve been using our brain, like cavemen, to understand
when things are evaluated. We haven’t had any help from the type
system, we’ve been on our own. This is sad. There are ways to
encode laziness in our types. For example, we can have a strict
language, one that fully evaluates everything as soon as possible, but
still allows us to mark some things as “lazy” when necessary. In this
hypothetical language, we can imagine lazy lists being defined as
follows:
data List a
= Nil
| Cons (Lazy a) (Lazy (List a))
It’s unclear what’s the right default approach, they each have their
virtues and shortcomings. We’ll talk about Haskell’s shortly. But
generally speaking, even if for a particular problem we’d prefer to
have a different evaluation strategy, that doesn’t mean that we need
to change the default evaluation strategy of the entire language. We
can usually override the defaults on a case by case basis. For
example, we might want to define a strict list, one where evaluating
the list to weak head normal form will force the evaluation of its
constituent parts to weak head normal form too.
data List a
= Nil
| Cons !a !(List a)
This function still doesn’t use its second input parameter. Yet, if we
ever try to evaluate the result of const a b, then b will be evaluated
to week head normal form too. This function exists in Haskell, it is
called seq, and it is not as useless as it seems. Its arguments,
however, are in different order.
So yes, foldl is way too lazy in the way it produces its output.
Ironically, it achieves this by being overly clingy while also being
way too strict in the way it consumes its input, which prevents it
from working with infinite lists at all. Way to go, foldl.
With this one change we are preventing foldl from creating more
thunks that it needs, thus reducing its memory consumption. This
strict, unclingy version of foldl, is actually called foldl' in Haskell,
and we should almost always use it instead of the way too lazy
foldl, which is only useful if f happens to be lazy in its first
argument, but it almost never is. The choice is usually between
foldr and foldl', rarely is foldl worth considering. It’s a shame the
most beautiful name foldl is wasted. Foldl prime though —that’s
how we prounounce foldl'— has a rather heroic ring to it, so we’ll
take it.
In practice, it turns out that worrying about things being too lazy is
more important than worrying about them being too strict. Mostly
because when things are too strict, we experience divergence in
some form right away. But with an overly lazy program, we may
not immediately notice what may cause our system to collapse later
on, once it has more data to process. There is a trick to avoiding this
situation. Perhaps we shouldn’t talk about it because, after all, this
is not a book about tricks, but here it goes anyway just in case.
When writing a function like foldl which uses some kind of
accumulator, we always make that accumulator strict. We bang
that acc. And when defining a new datatype, we bang each field in
its constructors too. We’ll have opportunities to repent and make
things lazy later on if we realize they needed to be lazy after all, but
by default we make them strict just in case. That’s it. Our program
continues to be lazy nonetheless because the non-accumulating
inputs to our functions continue to be lazy, and by far they are the
majority of what we write.
and False ⊥ /= ⊥
Notice the /= sign, conveying the idea that the things to its sides are
different. We tend to write “⊥” because it’s not only undefined who
embodies divergence, and we don’t want people to make
assumptions about this. In every other case, and will need to
scrutinize both input parameters, both of these thunks, which will
cause and to diverge if any of the parameters themselves diverges.
and True ⊥ == ⊥
and ⊥ x == ⊥
In practice, these cases are not worth highlighting since they are the
rule, rather than the exception. Of course we can expect things to
collapse if we irresponsibly sprinkle bottoms here and there, so we
only ever talk of how functions like and are extraordinary, not of
how they are not. And what makes and special is that it doesn’t
always make use of all its inputs, so that’s what we talk about.
const x ⊥ /= ⊥
flip seq x ⊥ == ⊥
⊥ : ⊥ /= ⊥
length [⊥] /= ⊥
head (x : ⊥) /= ⊥
How do we try these things, though? Are we supposed to compile
bottomful programs, run them, and wait for them to explode? Not
quite, not quite.
110. Frogs
Haskell, or GHC to be more precise, the Haskell compiler whose
acronym doesn’t officially stand for “Glorious Haskell Compiler”,
comes with a REPL, a read-eval-print loop. Acronyms, right? At
least if we repeatedly say “REPL” out loud we get to sound like a
frog.
> True
True :: Bool
The lines with a leading > are lines we typed manually in GHCi, and
below each of them we see the feedback from the REPL. We didn’t
need to type the > ourselves, though, that was our prompt. As soon
as we open GHCi, we are welcomed with something along the lines
of the following, and we can start typing expressions right away.
> :sprint xs
xs = _
> length xs
2 :: Natural
> :sprint xs
xs = [_, _]
> xs
*** Exception: Prelude.undefined
> :t True
True :: Bool
> :t Just
Just :: a -> Maybe a
> :t Nothing
Nothing :: Maybe a
> :t (:)
(:) :: a -> [a] -> [a]
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
> True
True :: Bool
> 3 + 4
7 :: Num a => a
Here we are not explicitly saying that True is of type Bool, yet Haskell
realizes this and shares this fact with us. Neither are we saying
whether 3 + 4 is an Integer or something else, yet Haskell reminds
us that 3 + 4 is actually a polymorphic expression, and that the
resulting 7 could be a value of any type a for which there exists a Num
instance. Lucky us. Of course, if we are not satisfied with this
polymorphism, we can force Haskell to assign a particular type to
the expression if we so desire by explicitly using :: as we’ve done
many times before.
> 3 + 4 :: Integer
7 :: Integer
> 3 + 4 :: String
<interactive>:2:1: error:
• No instance for (Num String) arising from a use of ‘+’
• In the expression: 3 + 4 :: String
In an equation for ‘it’: it = 3 + 4 :: String
Haskell, as much as possible, will infer the types and kinds of our
expressions. We can write entire programs without giving a single
explicit type annotation, tasking Haskell with inferring the type of
each and every expression on its own. This works. However, it is
also a terrible idea. As fallible beings with limited time and capacity,
we need to see the types written down if we are to have any realistic
hope of efficiently understanding what goes on in our programs
and how to evolve them, recognizing common patterns in them as
we go. Remember, our programs are rarely as special as we think
they are, so the earlier we find structure in them, the better off we’ll
be. Let’s do a small experiment regarding this. Say, what does this
program do?
dog = fmap fmap id const seq (3 +) flip const seq True flip
We also explicitly type our programs here and there because the
type-inference mechanism tries to be as accommodating as possible.
This is great. However, it also means that the inferred types will
often be way more general than we want them to be. For example,
consider the type Haskell infers for the expression 3 id.
> :t 3 id
3 id :: Num ((a -> a) -> t) => t
Here, Haskell sees that we are trying to apply the number 3 to the
function id, and infers the type of this expression according to that.
A fair logical deduction, considering 3 is a polymorphic number that
can be converted to a value of any type for which there is a Num
instance. Sure, 3 could be a function, why not? So, since we are
trying to apply this function 3 to yet another function of type a → a,
id, in order to obtain a value of value some arbitrary type t, Haskell
demands that there be a Num instance for (a → a) → t. Silly, I know,
yet a perfectly logical idea as far as the type-system is concerned,
which is why we, cognizant of a more sensible goal, should nudge
the type-inference mechanism in a sane direction by constraining
some of these wild thoughts.
> :t 3 id :: Bool
<interactive>:1:1: error:
• No instance for (Num ((a -> a) -> Bool))
arising from the literal ‘3’
(maybe you haven't applied a function
to enough arguments?)
• In the expression: 3
In the expression: 3 id :: Bool
This time, rather than Haskell inferring a silly type for our
expression, we get an error from the type-checker saying that the Num
instance our previous example was expecting could not be found.
This makes sense, why would there be a Num instance for (a → a) →
Bool that could be represented with the number 3? If we ever find
ourselves writing 3 id, chances are we wrote this by accident.
Maybe we “haven’t applied a function to enough arguments”, as
the error message kindly suggests.
Alternatively, if we know 3 is supposed to be an Integer number,
say, we can give 3 itself an explicit type, which will lead us to an
even better type error.
> :t (3 :: Integer) id
<interactive>:1:1: error:
• Couldn't match expected type ‘(a -> a) -> t’
with actual type ‘Integer’
• The function ‘3 :: Integer’ is applied to one
argument, but its type ‘Integer’ has none
In the expression: (3 :: Integer) id
But not only can Haskell tell us about the types of the expressions
that we have, it can also tell us about the types and the expressions
that we don’t. It sounds silly, I know, and you are right to be
skeptical. Nevertheless, skepticism doesn’t make this any less true.
Let’s see how.
112. Foldable
The and function we’ve been talking about so much is not really
called and but (&&). But beside its different name and fixity,
everything about (&&) is as we learned for and. We remain uncertain
as to the origin of this fascination with infix operators, but we’ll
play along and go with it. Mostly, and fits nicer in prose, that’s why
we’ve been using this fake name instead. Well, actually, it is not so
fake. There is in Haskell a function called and closely related to (&&),
but it has a different type, as reported by :type in GHCi.
Herein lies our quest. We’ll implement and using not much beyond
GHCi. Well, first we need to understand what and is supposed to
do, and as and is not a fully parametrically polymorphic function,
we’ll have to rely on human documentation for this. The
documentation says that “and returns the conjunction of a
container of Bools. For the result to be True, the container must be
finite. False, however, results from a False value finitely far from
the left end”. Alright, that’s our goal. Let’s implement and.
The main thing to notice is that the type of this foldr is a bit
different to the type we saw before. It talks about folding a t a,
where that t is a Foldable type-constructor, whereas before we
always talked about folding [a], a list of as.
Well, it turns out that lists are Foldable too. I mean, we already
knew that, of course, but moreover, if we look again at GHCi’s
output from before regarding :info Foldable, we can see some of
the existing Foldable instances listed as well. Among them, there’s
one for lists.
instance Foldable []
We haven’t really specified any instances for lists using the weird
Haskell list syntax before, so this might look a bit strange. So far,
we’ve only done this for the List we defined ourselves, the one with
the Nil and Cons constructors, but never for the one made out of
sweet [] and (:). The main thing to keep in mind is that at the type
level [] x and [x] mean exactly the same. In other words, much like
List or Maybe, [] is a type constructor of kind Type → Type which can
be applied to another type a by saying [] a in prefix form as every
other type-constructor, but it can also be applied to a as [a], using
some questionable syntax sugar. So, when the type of foldr takes []
to be its t, we end up with this:
foldr :: (a -> b -> b) -> b -> [] a -> b
Which is exactly the same as the type of foldr we’ve been seeing all
alone, but without some of its sugar:
politics :: [Bool]
politics = False : politics
Our politics is infinite, yet without any doubt we won’t find any
truth in it. Of course, Haskell doesn’t know this at runtime, but as
it starts inspecting politics it will encounter a False value right
away, enough evidence to confirm that no, it is not true that all of
the Bools in this container are True. Thus, without further
exploration, we can confidently announce that the conjunction of
these booleans is False, even if this is an infinite container. Of
course, none of this should come as a surprise, considering the
documentation already talked about it when it said “False,
however, results from a False value finitely far from the left end”.
114. A hole new world
We have (&&), a function of type Bool → Bool → Bool, and we want to
create and, a function of type Foldable t ⇒ t Bool → Bool. Ideally, as
long as we are dreaming, we would like to find a function that
simply takes (&&) as input and gives us and as output. We can use
Haskell’s hole mechanism to search for this function if it exists, or at
least to discover some hints that could help us implement it
ourselves if necessary. Of course, we could also come up with an
implementation for this function by using our brains, as cavemen
did, but let’s be smart and use that brainpower to learn a new tool
instead, so that in the future we can avoid thinking about these
utterly boring matters. This will be our adventure.
_foo here is the function we are trying to find. It takes (&&) as its sole
input parameter and becomes an expression of type Foldable t ⇒ t
Bool → Bool as requested. The underscore at the beginning of the
name _foo tells Haskell that it should treat _foo as a hole, rather than
complain that something called foo is not defined, as it would
without the underscore. A hole? Let’s take a look at the output
from GHCi to appreciate a bit more what this means.
<interactive>:61:1: error:
• Found hole: _foo :: (Bool -> Bool -> Bool) -> t Bool ->
Bool
…
A rigid type variable. This means that while many concrete things
could take the place of this t, it is known that the t must satisfy
some expectations, it can’t be just any t. In our case, we are
demanding that t be an instance of Foldable of kind Type → Type.
These expectations about t were conveyed, bound by, the explicit
type signature we gave to the whole _foo (&&) expression, but of
course, it could have been different. For example, consider this
other example.
> _x + 3
<interactive>:61:1: error:
• Found hole: _x :: a
Where: ‘a’ is a rigid type variable bound by
the inferred type of ‘it’ :: Num a => a
…
Here our hole is one of the inputs to (+). This time we are not
explicitly giving a type to anything, yet Haskell knows that a, the
type of our hole _x, is a not just any type variable, but a rigid one
expected to be an instance of Num as required by the type of (+),
which also says that its inputs and output must have the same type.
Haskell inferred this precise knowledge about a which the rest of
the type system can now use, including the hole mechanism. And
that it thing that’s mentioned there? In GHCi, it is a magical name
given to the expression the REPL is currently dealing with, so in
our case it is _x + 3, meaning that it, according to (+), has the same
type as 3, and from this Haskell infers the type of _x. What we are
seeing here, essentially, is type inference at work, determining what
the type _x should be even when we haven’t come up with an
expression to take its place yet.
Things are a bit different if we let type inference run wild by, say,
not constraining the hole at all.
We don’t really know what these do, but their names foldl1 and
foldr1 suggest a relationship with left folding and right folding.
And since we know that only right folding works with infinite lists,
which we’ll most likely need, as discussed before, we’ll focus our
attention on foldr1. Although we are a bit hesitant, because
somewhere in and's documentation we see something about values
“finitely far from the left end”, which suggests maybe a left fold is
better. Hmm, let’s try with foldr1 first.
foldr1 :: Foldable t => (a -> a -> a) -> t a -> a
Excellent. So, we have something with the same type as and. Let’s
try it and see what happens.
What? Well, yes, this is fine. Remember that through the eyes of
Foldable, the expression (False, True) is a container of just one
element, True, just like [True] is.
We can’t really display the infinite loop here, though. What would
that even look like?
116. Rebelión
Are we done? Have we successfully impletemented and? Well,
foldr1 (&&) certainly delivered all the answers we wanted, but there
is something we neglected. Let’s take a look at the type of foldr1
(&&) again.
All the functions we have seen so far have been total functions,
meaning that they deal with all of their possible inputs by
returning a suitable value of their output type as expected. This
makes sense, why would functions do something other than this?
Yet, here we have foldr1, failing to deliver the Bool it promised.
foldr1 is what we call a mistake, also known as a partial function,
meaning it is not total, meaning it diverges for some of its inputs.
We can easily conclude that foldr1 is a partial by analyzing its type.
We can see in this type that if the container t a is empty, foldr will
have to output the b that was provided as input. So, what should
this initial b be? When the container is empty, should foldr return
True or False? We hinted a while ago that True seemed to be a good
choice, but not for its own merit, but rather because False didn’t.
Can we do better? Can we justify our choice?
It’s easy to see that this identity element can’t possibly be False,
because each time False appears as one of the operands to (&&), the
resulting conjunction becomes False too, even if the other operand
was True. On the other hand, True always preserves the truth value
of the other operand. It’s as if True wasn’t even there. In other
words, True is our identity element. We found it. Let’s write our
Monoid instance for the conjunction of Bools.
Hmm, not quite. mconcat expects a list as input, whereas and was
supposed to take a Foldable container.
and :: Foldable t => t Bool -> Bool
However, we did say that a Foldable container was one that could
be seen as a list. And, actually, we even talked about a function
toList of type Foldable t ⇒ t a → [a] that could convert the
container to a list. So maybe all we need to do is convert our
container to a list using toList before feeding it to mconcat. That is,
we should compose these two functions. Let’s ask GHCi to
confirm whether our idea makes sense.
We can see now how the conjunction of zero elements comes to be:
It is simply the identity element, True, that’s all. And similarly, the
conjunction of one element is simply that one element, which is
semantically the same as conjoining that element with True, the
identity element. Mystery solved.
118. Disjunction
Actually, giving a Monoid instance to Bool maybe isn’t such a great
idea. Perhaps there are other monoids for booleans beyond
conjunction, and by giving Bool itself a Monoid instance we are
somewhat implying that conjunction is the most important
monoid there is, or something along those lines. Indeed, we saw this
happen for Natural numbers too, where both the addition and
multiplication of Naturals were a monoid, so we opted to create two
distinct datatypes Sum and Product, and give monoid instances to
them instead. Maybe we should do that here too.
Second, we can see that the identity element for the disjunction of
booleans is False. We can tell this by observing that a True operand
forces the result to be True even if the other operand was False,
whereas a False operand lets the other operand state its truth.
Or, assuming a function called unAll of type All → Bool that could
be used to remove the All wrapper, we could make and's
implementation a bit cleaner, in a point-free style, as follows:
So far we’ve been talking about compose because it’s easier to use it in
prose, but in reality compose doesn’t even exist with that name out of
the box in Haskell. We use (.) instead.
This time, for no particular reason, we use foldMap Any rather than
mconcat . fmap Any . toList as we did in our latest version of and. If
we recall, beside foldr, foldMap was the other function we could
have implemented when defining a Foldable instance instead of
foldr, so we figured maybe we could use it.
foldMap
:: (Foldable t, Monoid m) => (a -> m) -> t a -> m
Our first parser will be one that parses a natural number out of our
string. That is, it will turn a String like "125" into the Natural
number 125. Failing, of course, to parse things like "chocolate".
In Haskell, we can use the Char datatype to talk about the individual
characters in a String. For example, “a”, “b” and “c” are the
individual characters in the string “abc”, and just like how we can
use the syntax "abc" to talk about this string literally, we can use 'a',
'b' and 'c' to talk about the individual characters, using this special
single quotes ' syntax. We can ask GHCi to confirm this.
> :t 'a'
'a' :: Char
> :t '7'
'7' :: Char
> :t 'ж'
'ж' :: Char
Indeed, these are all values of type Char. So, presumably, we want
isDigit to check whether the given Char is one of the digits between
'0' and '9'.
Is this good? Well, no. It does work for the Chars that happen to be
digits as told by isDigit, sure, but it completely falls apart for every
other Char like 'r' or '?'. digitFromChar is a partial function, and
that makes it bad. Luckily, Haskell will let us know that our
patterns are insufficient, that they don’t handle all possible Char
inputs, nudging us in the right direction so that we fix this. Now,
we only plan to use this function internally within
naturalFromString after isDigit confirms that we are indeed dealing
with a digit, so we might argue that this is acceptable. However, if
that was the case, why would we be exposing digitFromChar for
everybody else to use at all, giving it such a prominent name? No,
no, no, this is unacceptable. We need to make digitFromChar a total
function, we need it to return something even when there’s nothing
to return. Nothing to return.
digitFromChar :: Char -> Maybe Natural
digitFromChar '0' = Just 0
digitFromChar '1' = Just 1
digitFromChar '2' = Just 2
digitFromChar '3' = Just 3
digitFromChar '4' = Just 4
digitFromChar '5' = Just 5
digitFromChar '6' = Just 6
digitFromChar '7' = Just 7
digitFromChar '8' = Just 8
digitFromChar '9' = Just 9
digitFromChar _ = Nothing
Ah, yes, much better. We can simply wrap the resulting Natural in a
Maybe and return Nothing when the given Char is not a digit. And
what is digitFromChar now if not another parser? It takes a Char, a
“less precise” data type from the point of view of what it means to
be a digit, and converts it into a Natural number if possible. And
with this, we don’t even need isDigit anymore, seeing how
digitFromChar can already deal with Chars that do not represent
digits.
122. Wild bool
We find a Bool in the wild. What does it mean? Hard to tell, isn’t it?
A boolean says “true” or “false”, but that’s all it ever does. Without
knowing what the fact the bool was talking about in the first place,
we can’t really assign a meaning to it. We must do better than wild
bools.
We call this problem boolean blindness, and we’d like to avoid it. So,
instead of booleans, we prefer to use sum types able to carry a proof
of some fact, like Maybe. To understand how, let’s take a look at
filter, a traditional function not exactly worth perpetuating.
filter :: (a -> Bool) -> [a] -> [a]
This function takes a list of as and returns a new one where some of
the original elements have been excluded according to a decision
made by the function a → Bool. However, we can’t really tell by just
looking at filter's type whether a value is excluded when the
function returns True or when it returns False. And the function’s
name “filter” doesn’t help either. So let’s grow up, disregard this
misleading tradition and try something else instead.
foldMap
:: (Foldable t, Monoid m) => (a -> m) -> t a -> m
Anyway, for completeness, know that the original filter that cared
for Bools discards every element for which the function a → Bool,
which we call the predicate, returns False. That’s what the filter
function we’ll often encounter in Haskell does.
123. Surjection
Anyway, even if digitFromChar doesn’t encourage boolean
blindness, there is still a small issue with it. Or perhaps an infinitely
large one, depending on how we look at it. We know that a value of
type Natural can represent any natural number, including the ones
from 0 to 9 we are interested in. However, it can also represent
values outside this range. So, while in digitFromChar we acquired
some very precise knowledge about a Char being a single digit
number, we lost that knowledge as soon as we picked Natural as the
type we use to represent the concept of a digit. And as soon as we
lose this knowledge, we can’t rely on the type-system to guarantee
that the Natural in question is a single digit anymore. Not unless we
remember the relationship between this Natural and the original
Char.
Much better. Now, not only is the returned Digit a proof that we
were able to obtain what we desired out of this Char, but it is also a
proof that what we obtained is one among ten possibilites, not
more.
Whether returning 0 in the case of the empty list makes sense or not
is an interesting conversation starter. It probably does, though,
considering how sum did it, and how fromDigits and sum are similar.
You see? We are already having a conversation. Like bringing up
the weather, it always works.
124. Type synonyms
We know how to turn a Char into a Digit, if possible, and we know
how to turn many of them into a Natural. We have most of the
pieces we need to implement naturalFromString, except the one that
will take a String and convert it to a list of Chars.
Huh. What? Yes, it turns out that a String is just a list of Chars.
From a didactic point of view, Strings being a plain old list is quite
handy. We get to reuse all the knowledge we have about lists, such
as the fact that we can pattern match on them:
If we dare ask GHCi about it, we’ll find that the type of this aptly
named function, sequence, is much more general. But for now, we’ll
just say sequence works with lists and Maybes as we see here. The
name sequence won’t make sense for the time being either, but we’ll
see later on that it is perfect.
> sequence []
Just []
> sequence [Nothing]
Nothing
> sequence [Just 1]
Just [1]
> sequence [Just 1, Nothing, Just 2]
Nothing
> sequence [Just 1, Just 2]
Just [1, 2]
> sequence [Just 1, Just 2, Nothing]
Nothing
> :t twoNaturalsFromString
twoNaturalsFromString
:: String -> Maybe (Natural, Natural)
So, how we can break this problem into smaller parts? On the one
hand, we have the matter of parsing the Natural numbers
themselves. We already solved that using naturalFromString, great.
On the other hand, we have the issue of that comma ,, which we
haven’t talked about yet. Well, let’s.
This function returns Just the prefix and suffix surrounding the
comma as long as there is a comma somewhere in the input String.
Otherwise, it returns Nothing.
And it works for infinite Strings too —yes, a String can be infinite
like any other list— seeing how splitAtComma produces a result as
soon as it encounters a comma without having to force the spine of
the suffix first. Obviously, if the given list is infinite and there is no
comma in it, splitAtComma will diverge just like and did, wanting for
an absent False. But if there is a comma somewhere, we will get a
finite prefix and an infinite suffix as result.
twoNaturalsFromString
:: String -> Maybe (Natural, Natural)
twoNaturalsFromString = \s ->
case splitAtChar ',' s of
Nothing -> Nothing
Just (a, b) ->
case naturalFromString a of
Nothing -> Nothing
Just na ->
case naturalFromString b of
Nothing -> Nothing
Just nb -> Just (na, nb)
twoSeparateThings
:: (String -> Maybe a)
-> (String -> Maybe b)
-> String
-> Maybe (a, b)
twoSeparateThings = \fya fyb s ->
case splitAtChar ',' s of
Nothing -> Nothing
Just (a, b) ->
case fya a of
Nothing -> Nothing
Just na ->
case fyb b of
Nothing -> Nothing
Just nb -> Just (na, nb)
Alright, this is better. In a sense uglier, but better, for we can reuse
twoSeparateThings to implement parsers like twoNaturalsFromString
and similar on top of this core without repeating ourselves.
twoNaturalsFromString
:: String -> Maybe (Natural, Natural)
twoNaturalsFromString =
twoSeparateThings naturalFromString naturalFromString
twoSeparateThings
:: (String -> Maybe a)
-> (String -> Maybe b)
-> (String -> Maybe (a, b))
composeParsers
:: (String -> Maybe a)
-> (String -> Maybe b)
-> (String -> Maybe (a, b))
If we imagine a being Natural and b being Bool, we can see how this
should be able to handle the parsing of our (Natural, Bool) pairs.
Yes, this should work.
Let’s start by writing a parser for Bool that takes leftovers into
account. We want the String "t" to become True, we want "f" to
become False, and anything else should fail parsing.
Well, that was easy. Notice how our parser doesn’t just return Bool
anymore. It now returns any unused leftovers as well. Which, by
the way, could be the empty String if that’s all what’s left in the
input String after consuming that first Char. Why not?
We now have this other type, which also includes leftovers as part
of the result:
composeParsers
:: (String -> Maybe (String, a))
-> (String -> Maybe (String, b))
-> String
-> Maybe (String, (a, b))
This is nice. And yes, we still have the marching “if this, then that,
otherwise nothing” from before, but the difference is that
composeParsers will be the last time we ever write this. One of them,
anyway. From now on, we will use composeParsers to, well, compose
parsers, and all of this will happen behind the scenes without us
having to care about it. Lovely.
Does it work? Sure, why wouldn’t it? For example, here is a parser
that parses two Bools, one after the other.
> parseTwoBools = composeParsers parseBool parseBool
> :t parseTwoBools
parseTwoBools :: String -> Maybe (String, (Bool, Bool))
> parseTwoBools ""
Nothing
> parseTwoBools "t"
Nothing
> parseTwoBools "tt"
Just ("", (True, True))
> parseTwoBools "ft"
Just ("", (False, True))
> parseTwoBools "ttfx"
Just ("fx", (True, True))
132. Wot
Still wearing your subversive hat? Pick it up, you’ll need it, go on.
composeParsers
:: (String -> Maybe (String, a))
-> (String -> Maybe (String, b))
-> (String -> Maybe (String, (a, b)))
But still, even if we make that choice, nothing in the types prevents
us from accidentally running the parser for b first. Here is the
proof, look.
composeParsers
:: (String -> Maybe (String, a))
-> (String -> Maybe (String, b))
-> (String -> Maybe (String, (a, b)))
composeParsers pa pb = \s0 ->
case pb s0 of
Nothing -> Nothing
Just (s1, b) ->
case pa s1 of
Nothing -> Nothing
Just (s2, a) -> Just (s2, (a, b))
What changed was that all the Strings were replaced by universally
quantified type variables x, y and z. And crucially, we can see now
how the input to b's parser matches the type of the leftovers from
a's parser, which will force the execution of the parser for a to
happen before that of b's. This is beautiful.
The idea to take away from this exercise is that even if we ultimately
intend to limit our concrete use cases to just one, we may not
actually need to limit our reasoning to that one case. In our types,
through parametric polymorphism, we can convey meaning and
determine the fate of our expressions in ways that with concrete
types, also known as monomorphic types, we just can’t.
composeParsers
:: (x -> Maybe (y, a))
-> (y -> Maybe (z, b))
-> (x -> Maybe (z, (a, b)))
On the one hand there is the composition of a and b into the pair
(a, b). This is not particularly surprising for us, considering how
achieving this composition, this pairing, was our goal all along. This
composition is merely the result of using the tuple constructor (,)
on a and b to form a new product type (a, b).
composeMaybes
:: (y -> Maybe z)
-> (x -> Maybe y)
-> (x -> Maybe z)
It doesn’t matter what these functions do, what matters is that foo's
Char output, if any, could serve as bar's input, and that either foo or
bar could fail at whatever it is that they do, resulting in Nothing,
which will make their composition return Nothing too, either
because there is no Digit, or because there’s no Char. So, yes, failure
composes too.
composeMaybes
:: (y -> Maybe z)
-> (x -> Maybe y)
-> (x -> Maybe z)
composeMaybes g f = \x ->
case f x of
Nothing -> Nothing
Just y -> g y
composeMaybes Just f == f
composeMaybes f Just == f
id . f == f
f . id == f
But what about the identity when pairing two things using (,), as
we are doing with our parser outputs a and b?
(‽, x) == x
(x, ‽) == x
But actually, we like the type of functions like these so much that
we want it to be its own different type.
We have seen datatypes like this one before, having just one
constructor and carrying a function as a payload. Op, for example,
was one of them.
data Op a b = Op (b -> a)
fail :: ∀ x. Parser x
fail = Parser (const Nothing)
Does it type-check? Indeed it does, const Nothing has type ∀ a b. a
→ Maybe b, so if we pick String as our a, and (String, x) as our b,
everything will click. It is is not the most interesting Parser, but it is
a Parser nonetheless.
So, what do we do with these Parsers? Well, the same thing we did
with them back when they were still functions: We provide some
input to them and see what happens. Doing this when parsers were
functions was easy, all we had to do was apply them to an input
String, that was all. But what can we do now? A Parser is not a
function anymore. Or is it? Sure, at a very superficial level, a Parser
x is not a function, but if we look beyond that Parser wrapper, all
we’ll find is a plain old boring function taking String as input and
returning Maybe (String, x) as output. So, presumably, what we
need to do is somehow remove the Parser wrapper and provide the
String input directly to the underlying function.
You see, there is a Natural value inside the Sum value constructor, but
in order to operate on it directly, we first need to remove the Sum
wrapper. In fact, we introduced a function named unSum just for
this.
We can now talk about runParser, the function that runs the given
Parser on the given String, resulting in a parsing result and parsing
leftovers, if any.
Look at that handsome type, all the noise is gone. All we did was
replace the previous parsing functions for values of type Parser, use
runParser each time we need to run a Parser on some input, and
finally wrap the entire expression in a Parser value constructor. The
rest is the same as before. Well, almost. We lost some of the
parametricity we had, seeing how the Parser datatype fixes the
parsing input and leftover types to be Strings, so we can’t tell
anymore the order in which of these Parsers are run just by looking
at the type of composeParsers. We now need to rely on
documentation, routine and prayer. It’s alright, though. Even
though we made a big deal out of this before, that was mostly to
make a point. In practice, this is a minor issue. In Haskell, tradition
says things go from left to right, so that’s the order in which we
normally expect things to happen unless something says otherwise.
composeParsers
:: Parser x y a
-> Parser y z b
-> Parser x z (a, b)
The cardinality of Bool is two because Bool has only two inhabitants
True and False. And what if we pair two Bools in a product type?
What would be the cardinality of that pair?
(Bool, Bool)
2 × 2 == 4
2 + 2 == 4
It is, and we can see this manifest itself as the cardinality of a sum
type of Bools, each of them having a cardinality of two.
Either Bool Bool
Again, there are four values that could have type Either Bool Bool.
They are Left False, Left True, Right False and Right True. From
here, sum types take their name. Interestingly, the cardinalities of
(Bool, Bool) and Either Bool Bool are equal.
2 + 2 == 2 × 2
And surprisingly perhaps, this implies that (Bool, Bool) and Either
Bool Bool are isomorphic. That is, we can convert between values of
these two types back and forth without any information loss. Here
is one proof of this.
fromEither . fromPair == id
fromPair . fromEither == id
And why is injectivity good? Well, it’s neither good nor bad, it’s
just a property. Like enjoying a particular song or having been born
in June, it doesn’t say much about us.
138. Invert
What’s interesting for us is that when a function is both injective
and surjective, this function is said to be bijective, and all bijective
functions have an inverse function which, essentially, undoes
everything the bijective function did. We can also call these inverses
anti-functions, but only if we like science fiction.
And all of this for what? Because this is what an isomorphism is: A
pair of functions that are the inverse of each other. That’s it. At
least insofar as types and functions are concerned.
139. Broken parts
A product type multiplies the cardinalities of its parts, a sum type
adds them. So what?
For example, the Either sum type applied to two types a and b
having a particular cardinality, corresponds to the addition of the
two natural numbers representing those cardinalities. Say, if we
acknowledge that the cardinalty of Bool is two, and that the
cardinality of Season is four, then the algebraic expression adding
these two numbers corresponds, in Haskell, to a use of Either on
the corresponding types Bool and Season.
Notice that in our use of the symbol ==, we are not saying that these
things are equal. They couldn’t possibly be equal because we have
numbers on one side and types on the other. All we are saying is
that there is a correspondence between these algebraic expressions
and these types.
a + b == Either a b
a × b == (a, b)
a × (b + c) == (a, Either b c)
a × (b + c) == (a × b) + (a × c)
Now, these two types are not exactly equal. If we pass a value of
type (a, Either b c) to a function expecting a value of type Either
(a, b) (a, c), our program will fail to type-check. But they are
isomorphic, as signaled by our usage of the symbol “≈” above, and as
proved by this pair of inverse functions.
1 + a == Maybe a
Maybe? How? Well, let’s recall its definition and see for ourselves.
3 == 1 + 2 == Maybe Bool
Digits
== 10
== 2 × 5
== Bool × 5
== Bool × (Bool + 3)
== Bool × (Bool + (1 + 2))
== Bool × (Bool + (1 + Bool))
== Bool × (Bool + Maybe Bool)
== (Bool, Either Bool (Maybe Bool))
Are Digit and (Bool, Either Bool (Maybe Bool)) isomorphic? Well,
they better be. If we are able to come up with a bijective function
from Digit to (Bool, Either Bool (Maybe Bool)) we will have proved
this, seeing how every bijection has an inverse, and together they
form an isomorphism.
fromDigit :: Digit -> (Bool, Either Bool (Maybe Bool))
fromDigit D0 = (False, Left False)
fromDigit D1 = (False, Left True)
fromDigit D2 = (False, Right Nothing)
fromDigit D3 = (False, Right (Just False))
fromDigit Dog = (False, Right (Just True)) -- Woof!
fromDigit D5 = (True, Left False)
fromDigit D6 = (True, Left True)
fromDigit D7 = (True, Right Nothing)
fromDigit D8 = (True, Right (Just False))
fromDigit D9 = (True, Right (Just False))
So, why do we come up with types like Digit if Eithers, pairs, Bools
and the like seem to be enough? Well, humans, that’s why.
Internally, computers and programming languages use simple sum
and product types like Either and pair a lot. Digit, ten, Either Bool
(Maybe Bool), 3 + 7, 10, it doesn’t matter what we call it, computers
don’t care. But we humans, we care about names. The words
“digit” or “season” mean something to us, so we come up with
technically irrelevant stuff like names in order to improve our
quality of life, and that’s fine.
140. Unit
What about the one? That is, one, literally.
> :t Unit
Unit :: Unit
141. Plus one
Just kidding. Of course there’s more to Unit than its definition. For
example, we recently said that the algebraic expression 1 + a
corresponds to the Haskell datatype Maybe a. But that’s not the sole
truth, is it? If we are saying that Unit corresponds to one, and that
Either corresponds to addition, then what’s wrong with this?
1 + a == Either Unit a
1 + a == a + 1
1 × a == (Unit, a)
a × 1 == a == 1 × a
It’s interesting to reason about why a and a pair of a and Unit are
conceptually equal, isomorphic, even if (a, Unit) clearly has more
information than a, and converting between (a, Unit) and a clearly
loses some information. Namely, Unit.
Unit differs from every other type in that it has only one inhabitant,
so if we know that a value has the type Unit, then we also know that
this value must be the constructor also named Unit, simply because
there is no alternative. So if we are asked to come up with the
inverse function of byeUnit, of type a → (Unit, a), we can simply
create the Unit that we need, right there on the spot.
But if Bool and Either Unit Unit are obviously isomorphic, or more
generally, if any two datatypes with the same cardinality are, why
doesn’t the compiler magically come up with an isomorphism
between them and use that to reason about Bool and Either Unit
Unit as equals? It would be nice, wouldn’t it? They mean the same,
don’t they? Well, you tell me.
data Hire = HireHer | HireHim | HireThem
So, no, it would not be such a great idea to allow the type-system to
automatically guess whether two types should be treated as equal
based simply on their extensional properties. One could envision a
type system where we explicitly allowed some types to be treated as
equal, such as (Unit, a) and a, even if ultimately they are not. This
is closely related to the idea of univalence, which we won’t be
covering here, but is different from equality and lies at the core of
modern type theories that among other things attempt to tackle
this matter.
144. Mon produit
A while ago we said that the multiplication of natural numbers,
with one as its identity element, was a monoid. And we are saying
now that multiplication corresponds to product types, and that one
corresponds to Unit. Are we implying that product types are
monoids too? Why, of course we are.
a and b are most certainly not the same type. They could be, sure,
but their separate universal quantifications say this is not necessary.
But even if a and b were the same type c, say, the output type (c, c)
would still be different to the inputs of type c.
If we were to describe this function out loud, we would say that (*)
is the function that takes two values of type Natural and outputs yet
another value of type Natural. That is, we are talking about the
things we do to values of type Naturals.
(,) is the binary operation of our monoid, yes, but rather than
taking two values of a same type into another value of that same
type, it takes two types of a same kind into another value of that
same kind. But if we are now talking about types and kinds rather
than values and types, we better look at type constructors, not value-
level functions. In other words, our binary operation is not so
much the value constructor (,) of type a → b → (a, b), but the type
constructor (,) of kind Type → Type → Type.
And, while not equal, the following types are all isomorphic, which
is good enough for us.
So, yes, we can reason about the product of types as monoids in the
same way we could reason about the product of numbers as
monoids.
And in doing this we kinda left Haskell behind, which brings our
attention to the fact that many of the ideas we are learning,
showing up here as values and types, have a place outside Haskell
too. It’s just that in Haskell they can run, and that makes this place
so particularly interesting.
data Void
And no, we didn’t forget anything. We are saying that Void is a new
datatype, but since it has no inhabitants, we are listing no
constructors for it, and this is fine.
You see, that second equation will never happen, so we can happily
make our function bottom out there, confident that this code will
never be evaluated.
The Void type corresponds to the idea of zero, the identity element
for the addition of types, for Either. And indeed, once we
acknowledge the relevant isomorphisms, we find in sum types a
monoid too.
146. Less is more
What about other common algebraic operations such as subtraction
and division? Well, no luck there. It doesn’t make sense to subtract
types, nor to divide them, so there’s no correspondence between
those operations and our algebraic datatypes.
(Bool, Void)
See? This type has zero inhabitants, for we can never create the
value of type Void we need in order to construct a value of type
(Bool, Void). This corresponds to the idea that multiplying by zero
equals zero. But other than this corner case, types don’t shrink, they
only ever grow, so shrinking operations such as division and
subtraction do not have a correspondence with operation on types.
Having said that, look at this datatype:
data List a = Nil | Cons a (List a)
list(a) = 1 + a × list(a)
list(a) - a × list(a) = 1
1 × list(a) - a × list(a) = 1
list(a) × (1 - a) = 1
(list(a) × (1 - a)) / (1 - a) = 1 / (1 - a)
And finally, seeing how on the left-side, 1 - a appears as part of a
multiplication and a division that cancel each other out, we can just
remove them.
list(a) = 1 / (1 - a)
1 / (1 - a) = 1 + a + a² + a³ + …
And so on, infinitely. But look, we only have additions and powers
there, and powers are just multiplications repeated over and over
again.
1 / (1 - a) = 1 + a + a × a + a × a × a + …
And yes, those first two lines saying f1, …, f9 :: Bool → Maybe Bool
are valid Haskell syntax too. When we have many expressions with
a same type, we can write down their names separated by comas
and assign a type to all of them at the same time using this syntax.
data X = X1 | X2 | X3 | X4 | X5 | X6 | X7 | X8 | X9
toX . fromX = id
fromX . toX = id
So, yes, functions are isomorphic to other types with the same
cardinality. But of course, functions embody mystery and
computation too, which is why we use them instead of their
isomorphic types. In other words, this exploration about a
function’s isomorphism was mostly a curiosity, and we can forget
all about it now.
148. Ex falso
Wait, wait, wait. Sorry, false alarm. Don’t forget about functions
and exponentiations just yet. There’s actually something else we
should bring our attention to, first. Two things, actually, two
fascinating situations. The first one is x¹, which in algebra equals x,
and in Haskell translates to an isomorphism between Unit → x and
x.
Unit -> x ≈ x
Well, that’s even easier. oof is a function that takes an x, and returns
a function that ignores any Unit it recives and outputs the original x.
We could have pattern matched against Unit, of course, but const,
of type ∀ a b. a → (b → a), works too. Remember, those rightmost
parentheses are redundant. Curious, right?
And what about x⁰, which algebra says equals one, which in Haskell
corresponds to Void → x being isomorphic to Unit?
Strange, isn’t it? Well, let’s see. Let’s try to implement a bijective
function from Void → x to Unit.
That was easy. Values of type Unit can be created out of thin air, so
we just create one and return it, why not? In any case, even if we
wanted, we wouldn’t be able to use that function Void → x because
there’s just no Void to which we could apply it, so all we can do with
that function is ignore it.
What about the inverse of bar, the function that converts Unit into
a function of type Void → x? Well, we know there’s only one such
Unit, and we just learned that there’s only one such Void → x, so the
implementation is rather straightforward, even if absurd.
And now we can finally forget about all this. For real this time.
Poof. The correspondences between algebraic expressions and
types continue, sure, but we will stop here. All we wanted to say,
really, is that the product of types is a monoid with Unit as its
identity element, and we already mentioned it a couple of chapters
ago, before taking our long diversion—a word that in Spanish, by
the way, means fun. So, having said that, we can now get back on
track.
149. Myriad
The reason why the product of types being a monoid matters is this:
But composeParsers and (,) are not exactly the same monoid, the
product of types. If we were to pair two Parsers for a and b as a
product type with (,), we’d end up with (Parser a, Parser b), not
Parser (a, b). Of course, we could always create a function that
takes one such (Parser a, Parser b) and returns a Parser (a, b) to
mitigate this fact.
Wait, what? All this function is doing is taking Parser a and Parser
b out of the tuple, and giving them to composeParsers as separate
inputs. Other than the fact that the input Parsers come inside a
pair, this function does exactly the same as composeParsers. What’s
going on?
150. Spice
Any two functions (x, y) → z and x → y → z are isomorphic to each
other. This has nothing to do with parsers nor monoids, but with
that delicious Indian cuisine.
curry . uncurry == id
uncurry . curry == id
curry
:: ((a, b) -> c)
-> (a -> b -> c)
curry . curry
:: (((a, b), c) -> d)
-> (a -> b -> c -> d)
Is unit a silly idea, then? No, it’s not, and we’ll soon explain why.
But first, let’s just assume it’s right, and while we have unit at hand,
let’s finally state the identity law that composeParsers must satisfy:
composeParsers unit pa ≈ pa
composeParsers pa unit ≈ pa
That is, composing any Parser with unit must result in that same
Parser right away. Of course, we know that if pa has type Parser a,
then composeParsers unit pa, say, will have the different type Parser
(Unit, a). But we are grown ups now, and we understand that
while these two expressions don’t even have the same type, they
could still be isomorphic. There’s hope.
But, is there truth? Hope will accomplish nothing. Not here, not
anywhere. We need truth. Heroes would prove this to be true using
equational reasoning. We, subject to the physical constraints of
these pages, but more importantly, to our love for these words, will
just spell it out. Yes, it is true that these things are isomorphic.
No, of course not, it says so in the tin. But if history has taught us
something, it’s that people tend to misunderstand blatantly
obvious things. People will see the type and assume the wrong
thing. So let’s dig deeper. If not_the_unit was our identity element,
our unit, composing it with a Parser a would lead to a Parser (Unit,
a) isomorphic to Parser a. Which, among other things, implies that
the newly composed Parser (Unit, a) would succeed in parsing any
input which the original Parser a would. Alas, it would not, for
whether the original Parser a succeeds or not, not_the_unit would
cause the newly composed Parser (Unit, a) to fail.
Easy enough. And, by the way, specifying the type of fmap in this
instance, or more generally, of any method in any instance, is
unnecessary. It’s fully determined from the instance head, where we
said that Parser is who we are giving this instance to.
In fmap, we are creating a new Parser b that, when run, will actually
provide its entire input String to the original Parser a, reuse any
leftovers from it as its own, and preserve any parsing failure too.
That is, this new Parser b, insofar as “what it means to be a Parser”,
behaves exactly like the original Parser a did. However, rather than
eventually producing a value of type a, it produces a value of type b
obtained by applying the function a → b to the a produced by
Parser a.
And, of course, like with every other Functor, we must ensure that
the functor laws, which talk about identity and composition, hold.
fmap id == id
Feel free to grab your pen and paper to prove, using equational
reasoning, that indeed they hold.
You see, there used to be some truth in the output of parseBool. But
now, silenced by the regime, the truth is gone and the Unit took its
place. Nevertheless, regime still sequesters the relevant String, the
evidence that led to the stolen Bool.
In Parser x, the Parser and the x have lives of their own. In the
grand scheme of things, it’s only a coincidence that from time to
time the x and the input String are related. fmap is how we deal with
the x output, and functions poking inside the guts of a Parser, like
composeParser, are how we deal with Parsers themselves.
So, the regime does something as a Parser, but as a Unit it’s rather
moot. Can we have the opposite? Can we have a Parser that doesn’t
do anything interesting as a Parser but produces a meaningful
output nonetheless? Sure we can, and the process is almost the
same.
What changes is that we take unit as our starting point, the Parser
that does nothing. All we need to do is use fmap to modify the Unit
value which unit normally produces. In our case, we are replacing it
with False.
> runParser problem "help"
Just ("help", False)
Luckily for us, Maybe is a Functor too, meaning that using fmap we
could lift fmap f, our function of type (String, a) → (String, b),
into a function of type Maybe (String, a) → Maybe (String, b) which
would apply said fmap f only when the Maybe is a Just containing a
(String, a).
But we are not done yet, because in between runParser and Parser
we are transforming a function of type String → Maybe (String, a),
not just a Maybe (String, a). That is, what we are actually trying to
modify is the output of a function. Well, this is what function
composition is for, isn’t it? And function composition goes by
many names, one of them being fmap. In other words, using fmap
once again, we could lift our function fmap (fmap f) of type Maybe
(String, a) → Maybe (String, b) into a function of type (String →
Maybe (String, a)) → (String → Maybe (String, b)), which is exactly
what we need.
So, much like unit was the composition of three different identity-
like things, the Functor instance for Parser is just the composition of
three other Functors, of three fmaps.
If we ask GHCi what the type of said composition is, the answer is
rather beautiful.
fmap . fmap . fmap
:: (Functor f, Functor g, Functor h)
=> (a -> b)
-> f (g (h a))
-> f (g (h b))
So, you see, it doesn’t really matter what our Parser does. Our
implementation of its Functor instance, which deliberately ignored
inputs, leftovers and failure altogether, proves that.
155. Poke
What’s important, now, is that Parser is a Functor, which means we
can peek and poke its output any way we see fit without taking its
guts apart each time, without even mentioning we are dealing with
a Parser at all. For example, if we imagine a function foo of type Bool
→ String, then fmap foo would have this type:
The cases where we fmap foo over the function id and parseBool are
particularly interesting because they are modifying the output of the
function and the Parser, respectively, even if no input has been
received by then yet. Which, depending on where you are coming
from, could be unsurprising and expected, or completely shocking.
156. Beauté
So, like many other beautiful things in life, Parser is a Functor. And
while it doesn’t really fit the Monoid typeclass, Parser is a beautiful
monoid too. But moreover, seeing as this convergence of beauty is
unlikely to be an isolated event, we give the situation a name,
somewhat hoping we have more of them. We call a monoid known
to be a Functor, a Functor known to be a monoid, a monoidal
functor. And this name has a home, too.
But the Monoidal typeclass also says something else, using a new
syntax that we haven’t seen before. Monoidal f says that f must be a
Functor too.
But this doesn’t just mean that A and B are constraints that need to
be satisfied in order for some instance of X to exists. It also means
that whenever X itself is satisfied, then A and B are satisfied too. We
can observe this useful feature with a small GHCi example:
When we ask GHCi about the type of the expression fmap id unit,
it says its type is Monoidal f ⇒ f Unit, even though we are clearly
using both unit from the Monoidal typeclass, and fmap from the
Functor typeclass. If satisfying a constraint like Monoidal f didn’t
imply that the superclasses of Monoidal f are satisfied too, then the
type of fmap id unit would need to mention both Monoidal f and
Functor f as their constraints.
And, by the way, if while learning this you are reminded of logical
implication but baffled at the fat arrow ⇒ going in the opposite
direction, then you are right: It goes in the opposite direction.
Technically, this law can be split into two. There is a left identity
law where Unit appears on the left.
pair unit a ≈ a
pair a unit ≈ a
We don’t care much about that difference for the time being.
Anyway, these are essentially the monoid laws. Nothing new for us
here. There is, however, an additional law.
This law, called the naturality law, says that fmapping f and g
separately over two monoidal functors ma and mb before pairing
them, must be equal to fmapping f and g together over pair ma mb,
something we can easily accomplish using bimap. Luckily, we don’t
really need to think about this law. Thanks to parametricity, it will
always be automatically satisfied by all Monoidal instances.
157. So what?
Parser is a monoidal functor. So what? What can we do with this
knowledge that we couldn’t do before? Technically speaking,
nothing. We already knew that Parsers could be composed in this
way, we just didn’t know why. But we must acknowledge that we
only arrived to that composition by chance, after many failed
attempts. We could have avoided that pain and gone straight to the
right answer had we known what to ask for. Well, now we know. Is
it too late? No, not at all.
Parser is just one among many monoidal functors, so all the pain
we went through to understand the composition of Parsers was not
in vain, for we shall be able reuse this knowledge with any other
monoidal functor. That we couldn’t readily recognize the structure
in what we believed to be unique didn’t make those structures any
less true. Going forward, it’ll be our loss if we don’t try to identify
them as soon as possible.
Let’s take another Functor, then, to see how this works. Let’s take
Maybe. Do we think it makes sense to try and combine a value of
type Maybe a with a value of type Maybe b, into a value of type Maybe
(a, b)? Maybe.
What about unit, the value of type Maybe Unit which, when paired
with a Maybe x, results in a value isomorphic to that Maybe x? Well,
considering there are only two inhabitants of this type, we can try
both of them and see what happens. If said Maybe Unit was Nothing,
then pairing it with anything would lead to Nothing as result, which
is fine whenever our Maybe x is Nothing, but terribly wrong
otherwise. It ain’t Nothing, no. It must be the other inhabitant then,
Just Unit. And indeed, if we pair Just Unit with Just x, the
resulting Just (Unit, x) would be isomorphic to Just x. Or the
other way around if we switch the order of the input parameters to
pair. And what if the Maybe unit was Nothing? Well, we get Nothing as
output, which is exactly the same we put in. So, yes, Just Unit is our
unit.
unit :: Maybe Unit
unit = Just Unit
And even if we haven’t really focused our attention on it, just like
in the case of Boolean conjunction using (&&), the laziness in our
definition of pair will make sure we don’t go looking for
ingredients until after we’ve made sure we have Utensils nearby.
Laziness is one of those things most easily noticed when ripped
away from us. Cherish it, it’ll prove terribly useful as we advance.
We’d be at a disadvantage without it.
We’ll probably need drinks for dinner too.
And so on, and so on. We can keep on pairing like that for as long
as we want. What have we achieved? Well, how many times have we
checked so far whether we had the things we wanted or not? Zero.
How many times we would need to check if we wanted to start our
dinner? One, we’d just have to check if everyting is Just or Nothing,
even though there are three things that could potentially be missing.
That’s something.
So, you see, Parser and Maybe accomplish two completely different
things. One parses Strings and the other helps us organize dinners.
Yet, we can reason about both of them in the same Monoidal way.
Haskell embraces diversity. Types can be whatever they want to be
in order to achieve whatever they need to achieve. They can pick
their own names, they can pick their own identities. And despite
said beautiful uniqueness, through well understood structures like
the Functor, the Monoid or the Whatnotoid, they can belong. They’ll
share, they’ll compose, they’ll have a clear and loud voice in the
conversation.
158. Fan
Let’s tackle our next Monoidal functor, but this time let’s do
something different. Let’s not think about what pairing these
things even means. Let’s just do it. Let’s ride the parametric
polymorphism and see where it takes us.
That’s right, (→) x shall be our Functor today. That is, the function
that takes x as input. Where, of course, x is a universally quantified
type-variable that could be any Type we want.
This is the only implementation unit can have. Is it a bit silly? Sure.
Nonetheless, it’s the only behavior that makes sense. If unit must
return a Unit, then it will be forced to ignore any value it is given as
input. What about pair?
What is this good for? Well, imagine we have two functions that we
want to apply to a same input:
double :: Natural -> Natural
double = \x -> x * 2
In this case the types of our inputs and outputs are the same, but
that’s just a coincidence. We could as well have paired a function
Natural → Bool with a function Natural → String to obtain a
function of type Natural → (Bool, String), and it would have been
fine. Anyway, let’s try our function in GHCi.
> double_and_triple 5
(10, 15)
Great, it works. And, of course, we can pair this further with any
other function that takes a Natural as input. Like even, say, the
function that takes a Natural number and returns True if the
number is even, or False otherwise.
> double 5
10
> pair unit double 5
(Unit, 10)
> pair double unit 5
(10, Unit)
Actually, the type of this function is a bit more general than this
because it’s designed to work with function-like things, rather than
just functions. Remember Profunctor? Anyway, we’ll pretend it
only works with functions for the time being.
We can see how (&&&) associates to the right. This doesn’t mean
much, however, considering how (&&&) must be an associative
function as mandated by the way of the monoid. It could have
associated to the left and the result would have been the same. Up
to isomorphism, that is.
159. Choice
Another Functor, Either x. Let’s see if this one is Monoidal too.
We’ll see the implementations of pair and unit soon, but first, let’s
highlight something. Notice how x, the type of the eventual
payload of a Left constructor, if any, is fixed to be x everywhere.
This is not unlike the x in (→) x, which was fixed to be x everywhere
too.
So, what? Isn’t Either x a Monoidal functor after all? Well, actually, it
can be, but in order to be one it must make a choice.
pair :: Either x a -> Either x b -> Either x (a, b)
pair (Right a) (Right b) = Right (a, b)
pair (Left x) _ = Left x
pair _ (Left x) = Left x
However, that’s not the entire truth. Later on we’ll learn there are
stronger forces at play here mandating that this be the case. The
leftmost Left shall always be prioritized. So, in reality, there’s was
no choice to be made here. Sorry, false alarm. The take-away from
this experience is that when we don’t know the whole truth, we
may be tempted to make uninformed choices that could be, in fact,
wrong. This is what dangerous humans do. Please, don’t do that.
Being aware of one’s lack of understanding is good. We ask
ourselves not how we are right, but how we are wrong. Faith and
mysticism won’t take us anywhere.
And what about unit? Right, we almost forgot. Well, there’s not
much we can say about it that we haven’t said about the unit for
Maybe.
Only Right Unit can behave as the identity element in our Monoidal
instance for Either, for exactly the same reasons that only Just Unit
could behave as one in Maybe's. Similar beasts, Either and Maybe. So
similar, actually, that we’ll frequently benefit from these two fully
parametric functions, hush and note, able to convert between them.
head only ever “fails” when the input list is empty, and it
communicates this failure by returning Nothing. In other words, we
embraced the type-constructor Maybe to communicate the
possibility failure.
But what if we had a function that could fail for more than one
reason? In that case, Nothing wouldn’t be enough anymore. Let’s go
back to cooking.
This pairing still perfectly conveys the idea of both the Utensils and
the Ingredients being there or not. However, if either one or both
of them are missing, all we get as result is Nothing, without any
knowledge of who’s to blame for our failed attempt at cooking.
Using Either, however, we can achieve a bit more.
And notice that while it makes sense to use WhyNot in this pairing, it
wouldn’t make much sense to use it in a standalone utensils, say.
case utensils of
Right u -> … We have some utensils. Nice. …
Left NoUtensils -> … Too bad. We have no utensils. …
Left NoIngredients -> … What is this nonsense? …
Well, we can’t have a cake and eat it too. We either look at both
Eithers in order to judge their contents, accumulating the error
reports from all the Lefts we discover, or we bail out as soon as we
encounter the first Left, without having to evaluate any other
Eithers beside it. With Either, we only get the latter behavior.
However, we could have a different Monoidal functor that did
something else, why not? However, this alternative behavior will
have to wait. First, we need to address some parsing issues.
161. Não tem fim
The composition of Parsers shares an unfortunate trait with the
composition of Maybes, where Nothing becomes the result whenever a
Nothing is among the Maybes we attempt to pair, yet we can’t tell
which of the Maybes in question is the Nothing leading to this result.
When pairing Parsers, if one of them fails, we are not able to
identify which one either.
Why did the Parser fail? We’ll never know unless we manually
inspect someString and try to deduce it ourselves, something
completely unmanageable when rather than two Parsers we find
ourselves composing hundreds of them. Remember, as soon as we
can compose two things, we can compose infinitely many of them.
Composition não tem fim.
So, how do we solve this? Well, we know how. Rather than using
Maybe for conveying failure, we need to use Either. We need to
change the definition of our Parser datatype.
We’ve chosen to put a String on the Left side of our Either. The
idea is that a Parser can now cry something interesting like Left
"Expected 't' or 'f'" as it fails, rather than just saying Nothing.
parseBool :: Parser Bool
parseBool = Parser (\s0 -> case s0 of
't':s1 -> Right (s1, True)
'f':s1 -> Right (s1, False)
_ -> Left "Expected 't' or 'f'")
Wait, this fmap has exactly the same implementation as before. Why
yes, both Either String and Maybe are Functors, so we don’t really
need to change anything here. The fmap that was lifting a function
(String, a) → (String, b) into Maybe (String, a) → Maybe (String,
b) before is now lifting it to Either String (String, a) → Either
String (String, b), but other than that nothing changes. What
about pair?
But if we consider the use case for Parsers, which could fail parsing
for a myriad different reasons, none of them which we expect to
automatically recover from, then a String makes a bit more sense.
The errors reported by our Parsers are exclusively intended for
human consumption, not for computers. How would a program
automatically recover from an error saying that a particular
character in our input is not a digit? Would the program just go
and replace the character in question with a digit? Which digit?
That doesn’t make much sense. What if the input was right,
actually, and it was the Parser who was expecting the wrong thing?
If a parsing error happens, any error, it’s either because the input is
malformed or because the Parser is wrong, and fixing any of those
situations requires human intervention. So the best we can do,
really, is to try and be precise about where the parsing error
happened. Even more so if we consider that the number of Chars in
our input String could very well be in the millions. Why not?
Well, how do we want to fix it? There are at least two obvious
solutions. One of them could say at which absolute character
position in the input String we encountered the error. Say, at
character position 0, 3 or 43028. This solution manages to be both
very precise and very uninformative at the same time. Alternatively,
we could say something along the lines “failed to parse the user’s
birthdate month number”, which gives a higher level overview of
what the Parser was trying to accomplish when it failed, without
telling us exactly where the crime happened. Luckily, we don’t have
to chose between these alternatives. They complement each other,
and we can have them both.
We’ll tackle this in the same way we tackle every other problem.
We’ll correctly solve the individual small problems we identified,
and then we’ll just compose these correct solutions into a bigger
and still correct solution. Correctness composes.
163. Relative
Let’s tackle the matter of the absolute position of a problematic
Char in the input String first. The idea is that, when a Parser fails, we
want to see both an error message and an absolute character
position indicating where the parsing failed.
In the first example above, it’s the leftmost parseBool in the pair the
one that first fails to parse the input "xy". And seeing as 'x' is the
Char that’s causing the whole thing to fail, and how said 'x' appears
as the very first Char in the input String, then we report the Natural
number 0 as the absolute position of the first conflicting Char in the
input String. Remember, we always start counting from zero, not
from one.
Well, well, well… Things are getting a bit insane, aren’t they? That’s
fine. Ours is an excercise in taming complexity, and we are
deliberately trying to show how the solution to these so-called
complex problems boils down to finding the right types and getting
their composition right.
This new type, just like the runParser function, says that if our
parsing function is going to fail, then it better provides information
about where the failure happened. The rest is the same as before.
Let’s rewrite parseBool to take this into account.
parseBool :: Parser Bool
parseBool = Parser (\s0 -> case s0 of
't':s1 -> Right (s1, True)
'f':s1 -> Right (s1, False)
_ -> Left (0, "Expected 't' or 'f'"))
Since parseBool only ever deals with the first Char of its input String,
it will always report 0 as the failing position when it receives some
unexpected input. Of course, we could easily imagine a different
Parser that took more than the first Char of the input String into
consideration. For example, a Parser expecting the String "blue" as
input but getting the String "bland" instead, could report 2 as the
position of the first unexpected Char, "a".
The Functor instance stays the same as before because all we did was
change the payload on the Left, which fmap completely ignores.
Let’s see if this works, then.
> runParser (pair parseBool parseBool) "xy"
Left (0, "Expected 't' or 'f'")
> runParser (pair parseBool parseBool) "ty"
Left (0, "Expected 't' or 'f'")
What? This ain’t right. The 0 reported in the first example is fine,
yes, because the Char at position 0 in the input String "xy" is
certainly wrong. But in the second example, the error also reports 0
as the failing position, even if we know that this time the Char at
position 1, the 'y', is the culprit. What’s going on? Well, not
composition, that’s for sure.
164. Absolute
The problem we are seeing is that when individual Parsers such as
parseBool fail, they report the relative position where they fail with
respect to their own input, not with respect to the entire input that
was initially provided to runParser by us, in GHCi. That is, these
errors do not report the absolute position of the conflicting Chars in
the input String.
Indeed, the Righteous scenarios now report how much input they
consumed. The Left situation is the same as before. In any case, all
of these scenarios still talk about the relative positions of the Chars
they consume as they appear in the input String this Parser receives.
parseBool still doesn’t care about how much input other Parsers
consume.
Notice that, seeing as both the Left and the Right must produce a
Natural number now, conveying essentially the same meaning in
both cases, we could have chosen to abstract that common Natural
away in an isomorphic product type like (Natural, Either String
(String, x)). This is not unlike algebra, where a × b + a × c equals
a × (b + c). We’ll encounter this situation time and time again in
our travels. Anyway, we picked a representation already, so let’s stay
with it.
That’s it. All we did is put a bang ! in front of the na and the nbs
that are eventually used in the expression na + nb. This new code
keeps the expression na + nb itself a thunk, but this time the na and
the nb in it are guaranteed to have been evaluated by the time they
are used in this addition. By doing this we prevent creating more
thunks than we actually need.
Are we abandoning laziness by doing this? Not really. By the time
we scrutinize the Either resulting from runParser in order to
disambiguate whether is Left or Right, the parsing function has
already consumed as many Chars from the input String as it needed
in its attempt to successfully parse something, so it clearly knows
the number of Chars in question.
This issue almost went unnoticed, didn’t it? Well, yes, but
remember what we said back when we were studying laziness:
Always make your accumulators strict. And what is this Natural
value, which accumulates the number of Chars consumed so far, if
not an accumulator?
166. Candy
It’s a bit disappointing having to worry about counting those Chars
even when we successfully parse something, isn’t it? Well, that’s
kinda our fault, for we had parseBool do too many things at once.
All we wanted to say, really, is that if the first Char is either 't' or
'f', then we would be able to turn it into True and False, otherwise
parsing should fail with some message of our choice. We don’t
really want to worry about consumed input length nor leftovers.
\case A -> …
B | foo -> …
| bar -> …
Mind you, this is just syntactic sugar for what we already know. We
are simply avoiding the unnecessary introduction of a name, thus
keeping ourselves from having to worry about it. Additionally, we
are saving ourselves from typing a handful of characters. That’s all.
But, as it sometimes happens with syntactic sugar, it makes things
beautiful. And what would life be without beauty? We live, we die,
and if we do things right, at best beauty is what we leave behind.
This use of the word case reminds us that some words like case,
forall or instance are magically reserved by Haskell for some very
specific purposes, and not for anything else. If we try to implement
a function, say, using these words, the compiler will yell at us.
These words are forbidden, like , or . It’s just a
syntactic matter, not a semantic one.
Anyway, with the help of \case, our parseBool becomes a bit more
pleasing to contemplate.
So, how do we write this? Well, we don’t, not yet. expect would be
doing too many things at once, and we need to start getting more
comfortable with the idea of correctly solving the small issues first,
composing the small solutions into a bigger solution afterwards.
expect1 'x', say, only succeeds if “x” is the Char at the beginning of
the String being parsed. Otherwise, it fails with a useful error
message.
> runParser (expect1 'x') "xf"
Right (1, "f", Unit)
> runParser (expect1 'x') "yf"
Left (0, "Expected 'x' got 'y'")
Excellent.
168. Chow
We introduced a new function in expect1, one named show.
> :t show
show :: Show a => a -> String
show can be used to convert values of types for which there is a Show
instance, into a String representation of said value. Mainly for
internal display purposes like ours. In expect1 we are trying to
construct a String to use as an error message, and we are using (<>),
of type String → String → String here, to concatenate the smaller
Strings that are part of our message. But the a and the b which we
want to mention in our message are not Strings, so we need to
convert them first. That’s when show comes handy.
See? Easy. Recursive, even. We can now show our values of type Foo.
Actually, we can just not write show and GHCi will automatically
apply show to our values and print the resulting String.
> Bar
Bar
> Qux 8
Qux 8
> id
<interactive>:89:1: error:
• No instance for (Show (a -> a)) arising from …
See? Still working. There are actually many typeclasses like Show
which Haskell can automatically derive for us, we’ll talk about
them as they appear. Some we’ve encountered already. Like Eq, the
typeclass that gives us (==) and (/==). We can ask Haskell to derive it
too.
With this, we can now compare values of type Foo for equality using
(==), something that previously would have resulted in a type-
checking error.
In this example, expect1 'a', the leftmost Parser in our pair, fails, so
the rightmost Parser, our tricky undefined, is not evaluated at all.
This is clever. Not only was the execution of this rightmost Parser
prevented, but zero resources were spent on forcing it out of its
thunk. This is very handy for us when constructing potentially very
large Parsers out of many small Parsers. Because of laziness, the large
Parsers won’t really exist as a process until their existence has been
proven to be necessary by the actual input we are dealing with at
runtime. Very beautiful.
Anyway, lists. Strings are a list of Chars. And if we are saying that we
are performing unit on every occurrence of [], and pair on every
occurrence of (:), then maybe we could express all of this as a fold
instead.
What remains now is to make sure we work with lists of xs, rather
than with pairs of xs. And as soon as we have that, we have sequence.
Hmm. Funny. We keep fmapping const whatever over unit time and
time again. Maybe we should introduce a function that does just
that, and thus avoid some of the noise.
id :: a -> a
Great. Let’s move on. On the second pattern, when there’s at least
one such value of type f x in that list, then we pair it with a
recursive call to sequence rest. This pairing, however, results in a
value of type f (x, [x]), not f [x] as we wanted. But, by fmaping the
function \(x, xs) → x : xs of type (x, [x]) → [x] over this result, a
function which we could also have written as uncurry (:), we can
convert our Parser's output to the expected type.
This definition of expect first fmaps expect1 over the input String,
which is just a list of Chars, to create a value of type [Parser Unit].
This list is a suitable input to sequence, which will turn it into a
Parser [Unit], whose output of type [Unit] we are not really
interested in, yet we must convert to Unit in order to keep the
compiler happy. So we fmap the function const Unit over it to finally
get our Parser Unit.
To sum up, while pair allows us to compose two values of in some
Monoidal functor, sequence allows us to compose zero or more of
them. However, this wouldn’t be possible without unit or pure
giving us a sensible answer to the baffling question of what it means
to sequence zero things. sequence brings “successful” things together
in a way that reminds us of how and, the conjunction of many
booleans, does it. Which, by the way, suggests that another way to
look at pair is to think of it as the conjunction of just two functorial
values. If both of these values are “true”, by which we mean that
none of them fail in the sense Left, Nothing or a failing Parser would,
then pair gives us a “true” value as result.
171. Digitoll
Alright, we can parse Digits, and even lists of Digits. We should be
able to parse Natural numbers now. All we have to do, presumably,
is fmap our old function fromDigits, of type [Digit] → Natural, over
our parsing result.
Hmm, this ain’t right. What about a bigger number, like 12345?
What? Why are we only getting 123 as a Natural result, and "45" as
leftovers? Well, the function is called parseThreeDigits, isn’t it? So
this behavior is exactly what we asked for. We parse three digits at a
time, not less, not more, and we make a Natural number out of
them.
Unit :: Unit
unit :: Monoidal f => Unit
This typeclass will have two methods. One shall correspond to the
Void type, the identity element for the sum of types, conveying the
idea of zero. And the other shall correspond to the Either type-
constructor, the associative binary operation for this monoid.
Either :: a -> b -> Either a b
Either takes two types a and b, and returns yet another type Either a
b. So, mimicking the correspondence between the tuple type-
constructor (,) and pair, we’ll need a function where the a and b
given as input, as well as the Either a b returned as output, are all
wrapped in some Functor f.
We’ll call the function alt, suggesting that Parser a and Parser b are
different alternatives. This or that. The correspondence between
Either and alt, regarding their shape at least, is unquestionable.
But alt is not enough. Our monoid needs an identity element too.
In the case of Either, it is Void. For us, it’ll be Void too, but much like
unit wrapped Unit in some Functor f, we’ll wrap Void in an f too.
Let’s call it void, in lowercase.
And since we like a good pun, and moreover we are obliged to rebel
against the misnamed Monoidal, we’ll name our typeclass Monoidalt
and chuckle for a while. We’ll learn, eventually, that neither
Monoidal nor Monoidalt are the names these things go by, so any
discussions about their naming would be rather moot and
temporal. Let’s have fun.
class Functor f => Monoidalt f where
alt :: f a -> f b -> f (Either a b)
void :: f Void
alt void a ≈ a
alt a void ≈ a
These laws are exactly the same as the Monoidal ones, except we have
replaced all mentions of pair with alt, and unit with void.
First we’ll try to parse "friend", which is exactly the minimum input
our leftmost parser expect "friend" needs to succeed. alt, seeing as
expect "friend" succeeds, proceeds to wrap its Unit output in a Left
constructor that fits the expected Either Unit Bool type.
The error arises when trying to parse the 'o' in position 1, not
before. Yet, despite the leading 'f' having already been consumed
by then, alt, seeing how the leftmost Parser failed, will provide the
entire input String "foe" to its rightmost Parser parseBool, which
will successfully turn that 'f' into False. Not just "oe".
There’s not much going on here. All that’s happening is that while
pair pa pb only executes pb when pa succeeds, alt pa pb only
executes pb when pa fails, respectively wrapping as and bs in Left
and Right. That’s all.
And just like in pair, the fact that alt doesn’t touch pb unless truly
necessary means that any laziness in pb is fully preserved. We can
observe this by composing undefined, our trojan horse, to the right
of a successful Parser.
Had alt evaluated that undefined, our program would have crashed.
Alas, that only happens when the leftmost Parser fails.
What about void, of type Parser Void? This Parser clearly must fail,
for there’s no way we can produce a value of type Void as output.
But failing is easy, we just return a Left with a dummy error
message, and report zero Chars of consumed input.
void :: Parser Void
void = Parser (const (Left (0, "void")))
pairing with void obviously fails, since this corresponds to the idea
of multiplying by zero, which results in zero.
some is a function that, given some f a where the f has both Monoidal
and Monoidalt instances, it will return a list with at least one a in it.
We can try some parseDigit on its own to see how this works.
optional
:: (Monoidal f, Monoidalt f)
=> f a -> f (Maybe a)
optional fa =
fmap (either Just (const Nothing))
(alt fa unit)
> sequence []
Right []
> sequence [Left "red"]
Left "red"
> sequence [Left "red", Left "blue"]
Left "red"
> sequence [Left "red", undefined, Right 4]
Left "red"
> sequence [Right 3, Left "red"]
Left "red"
> sequence [Right 2]
Right [2]
> sequence [Right 4, Right 1, Right 8]
Right [4, 1, 8]
What about functions next? Let’s see what happens if we pick (→)
Natural to be our Monoidal functor, and assume we’ll be using
sequence on lists containing values of type Natural → Bool. Let’s try
with the function named even first, of type Natural → Bool,
returning True if the given number is even or False otherwise.
> even 4
True
> even 3
False
> :type sequence [even]
sequence [even] :: Natural -> [Bool]
> sequence [even] 4
[True]
> sequence [even] 3
[False]
Fascinating. Like pair and (&&&), sequence still fans-out, but it does
so on a larger scale.
unit :: [Unit]
unit = [Unit]
What about pair? The type says it takes a list of as and a list of bs,
and it turns them into a single list of as and bs. So, presumably, it
just pairs the individual elements of these lists in order.
Alright, this seems fine. Let’s try a list with one element next.
What? That’s not right. Where did the 7 go? The identity law for
monoidal functors has been violated. Did we pick the wrong unit?
No, actually, we picked the wrong pair. The function we
implemented is not pair, but a function called zip.
zip :: [a] -> [b] -> [(a, b)]
zip (a : as) (b : bs) = (a, b) : zip as bs
zip _ _ = []
zip is useful when you have two lists of the same length, and you
want to pair up the individual elements of those lists in the same
order they appear on those lists.
However, when one of the given lists is shorter than the other one,
all the “extra” elements from the longer lists are simply dropped
from the result.
What failed when we tested for the identity law is that we were
trying to zip a list of length one, unit, with a longer list, and this
caused all the elements beyond the first one to be discarded.
Can we pair lists, then? Yes, yes we can, but the answer is
something else. We need to implement the cartesian product of
these lists, where each element on one list is paired exactly once with
each element on the other list:
> pair [1, 2, 3] [True, False]
[(1, True), (1, False),
(2, True), (2, False),
(3, True), (3, False)]
You see, all of 1, 2 and 3 are paired with both True and False. And,
symmetrically, all of True and False are paired with 1, 2, and 3. If we
were to pair with unit, the result would be isomorphic to the given
list.
And pairing with the empty list, obviously leads to an empty result.
If we recall, lists are Monoids too, and when we have a list of Monoids,
we can use mconcat, of type Monoid m ⇒ [m] → m, to mappend all of those
ms together. So if we pick [x] to be our m, for some arbitrary x, then
mconcat adopts the type [[x]] -> [x] and simply concatenates lists
together, seeing how list concatenation is how mappend combines
values of type [x].
> mconcat []
[]
> mconcat [[]]
[]
> mconcat [[], []]
[]
> mconcat [[], [1, 2], [3]]
[1, 2, 3]
> mconcat [[1, 2], [], [3, 4], [5]]
[1, 2, 3, 4, 5]
So, yes, lists are Monoidal functors too, and pairing them gives us the
cartesian product of the given lists. I leave it up to you to prove,
using equational reasoning, that the Monoidal functor laws hold for
our chosen unit and pair. Here’s the full definition of the Monoidal
instance for lists.
Well, seeing how pair gives us the cartesian product of two lists, and
how sequence is essentially pair but for zero or more lists rather than
two, sequence will presumably give us the cartesian product of all
the input lists.
> sequence []
[]
> sequence [[]]
[]
> sequence [[], []]
[]
> sequence [[1, 2], [3, 4, 5]]
[[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]
> sequence [[1, 2], []]
[]
> sequence [[1, 2], [], [3, 4, 5]]
[]
> sequence [[1, 2], [3], [4, 5, 6]]
[[1, 3, 4], [1, 3, 5], [1, 3, 6],
[2, 3, 4], [2, 3, 5], [2, 3, 6]]
The length of [1, 2] is two, the length of [3, 4, 5] is three, thus the
length of the list returned by sequence [[1, 2], [3, 4, 5]] is three
times two, six. Following this reasoning, we can see how including
an empty list [], whose length is zero, among the input to sequence,
would lead to an empty list as result. Multiplying by zero results in
zero.
Imagine we had a product type named Foo with four fields in it.
Could we use pair to, say, construct a Parser for Foo? Of course.
As always, let’s start by solving the smaller problems first. Let’s start
by implementing Parsers for each Season individually.
parseWinter shares its type with parseSeason from before, but rather
than worring about all four possible Seasons, it worries only about
Winter. How? It expects "winter" to lead the input String being
parsed, and if it does, then this Parser's boring Unit output will be
replaced with Winter by simply fmapping const Winter over it.
And we can do the same for the rest of the Season values.
Monoidalt doesn’t exist. Nor alt, nor void. All we have is Alternative
and its methods. We’ll never write a Monoidalt instance for Parser,
say, because the Monoidalt typeclass doesn’t exist. But we will write
an Alternative instance for Parser.
Of course, once we have empty and <|>, if for some reason we want
void and alt back, we could define them on terms of empty and <|>.
void is easy, seeing as it’s just a less polymorphic version of empty.
We don’t need to do anything.
And alt is not much more complex than this, really. Essentially, we
need to do something similar to what we did when parsing Seasons
using <|>, but for Either rather than for Season.
If you recall, one of our intuitions for foldr is that all occurrences
of (:) in the list are replaced by the given binary operation, and [] is
replaced by the given initial accumulator. In our case, we replace
them with (<|>) and empty.
Replacing those (:) and [] for <|> and empty, as asum would, takes us
back to where we started:
> or []
False
> asum []
Nothing
> or [False, False]
False
> asum [Nothing, Nothing]
Nothing
> or [False, True]
True
> asum [Nothing, Just 4]
Just 4
Alright. What about Monoidal, unit and pair? Can we get rid of
them, too? Is that something we want to do? Why are we asking
these questions?