├── .editorconfig
├── .gitignore
├── .markdownlint.json
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── ch00.md
├── ch01.md
├── ch01
├── c1_flock.js
└── c2_flock.js
├── ch02.md
├── ch02
└── c1_greeting.js
├── ch03.md
├── ch03
├── c1_pure.js
├── c2_impure_checkAge.js
├── c3_pure_checkAge.js
├── c4_immutable.js
├── c5_math.js
├── c6_cacheable.js
├── c7_portable.js
└── c8_reasonable.js
├── ch04.md
├── ch04
├── c1_add.js
├── c2_curry.js
├── exercise_a.js
├── exercise_b.js
└── exercise_c.js
├── ch05.md
├── ch05
├── c1_compose.js
├── c2_last.js
├── c3_variadic.js
├── c4_snakeCase.js
├── c5_initials.js
├── c6_debug.js
├── c7_trace.js
├── exercise_a.js
├── exercise_b.js
└── exercise_c.js
├── ch06.md
├── ch06
├── index.html
├── main.js
├── main_step2.js
├── main_step3.js
└── main_step4.js
├── ch07.md
├── ch07
├── c1.js
└── c2.js
├── ch08.md
├── ch08
├── c10_task.js
├── c11_task.js
├── c12_id.js
├── c13_route.js
├── c14_nested.js
├── c1_container.js
├── c2_functor.js
├── c3_maybe.js
├── c4_maybe.js
├── c5_either.js
├── c6_either_getAge.js
├── c7_either.js
├── c8_io.html
├── c8_io.js
├── c9_task.js
├── exercise_a.js
├── exercise_b.js
├── exercise_c.js
├── exercise_d.js
└── metamorphosis.txt
├── covers.md
├── images
├── canopener.jpg
├── cat.png
├── cat_comp1.png
├── cat_comp2.png
├── cat_theory.png
├── catmap.png
├── cats_ss.png
├── chain.jpg
├── console_ss.png
├── cover.png
├── dominoes.jpg
├── feature-exercise.gif
├── feature-marp.gif
├── feature-quokkaJs.gif
├── fists.jpg
├── fn_graph.png
├── function-sets.gif
├── functormap.png
├── functormapmaybe.png
├── id_to_maybe.png
├── jar-functor.jpg
├── jar.jpg
├── monad_associativity.png
├── natural_transformation.png
├── onion.png
├── relation-not-function.gif
├── ship_in_a_bottle.jpg
└── triangle_identity.png
├── package.json
├── solutions
├── ch04
│ ├── solution_a.js
│ ├── solution_b.js
│ └── solution_c.js
├── ch05
│ ├── solution_a.js
│ ├── solution_b.js
│ └── solution_c.js
└── ch08
│ ├── solution_a.js
│ ├── solution_b.js
│ ├── solution_c.js
│ └── solution_d.js
├── support
├── README.md
├── index.js
└── package.json
└── tarslab.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.md]
10 | indent_size = 2
11 | trim_trailing_whitespace = false
12 |
13 | [*.js]
14 | indent_size = 2
15 | indent_style = space
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD004": false,
3 | "MD033": false,
4 | "MD041": false
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | support/index.js
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["wallabyjs.quokka-vscode", "marp-team.marp-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "markdown.marp.enableHtml": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | * All text in this book is under:
2 | Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
3 | http://creativecommons.org/licenses/by-sa/4.0/
4 |
5 | * All artwork randomly stolen from google image search so the license doesn't apply. Please let me know if any artwork is yours and I'll give credit or remove.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > Video tutorial series on *[mostly-adequate-guide](https://github.com/MostlyAdequate/mostly-adequate-guide)*。
2 |
3 | [函数式编程指南](https://github.com/MostlyAdequate/mostly-adequate-guide)的中文讲解系列视频,视频里面提到的代码和课件都在该项目里。
4 |
5 | 视频系列中对白的主要参考[函数式编程指北](https://github.com/llh911001/mostly-adequate-guide-chinese)。
6 |
7 | B站视频链接:
8 |
9 | - [第1章: 我们在做什么?](https://www.bilibili.com/video/BV11j411v7ft/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
10 | - [第2章: 一等公民的函数](https://www.bilibili.com/video/BV1fB4y1o74C/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
11 | - [第3章: 纯函数的好处](https://www.bilibili.com/video/BV1QM41197sz/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
12 | - [第4章: 柯里化 curry](https://www.bilibili.com/video/BV1v84y1972g/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
13 | - [第5章: 函数组合 compose](https://www.bilibili.com/video/BV1Ng4y1R76K/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
14 | - [第6章: 示例应用](https://www.bilibili.com/video/BV1Yb4y1M75n/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
15 | - [第7章: Hindley-Milner类型签名](https://www.bilibili.com/video/BV1HN411j7Nt/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
16 | - [第8章: Functor 函子 Maybe、Either、IO、Task](https://www.bilibili.com/video/BV1wC4y1679Q/?share_source=copy_web&vd_source=580ef492c06a7e7fa3902ed3134fd80a)
17 |
18 | # 介绍
19 |
20 | 这是我这么多年学习编程技术收获最大的一本书。我把函数式编程思维应用到自己的工作项目中,代码质量有了质的飞跃,以前难以重构的代码重新获得生命,重新迭代起来,学习的回报巨大。但是在我学习那么多技术书中,它的学习过程最为艰辛。在学习过程中,对代码一知半解,不知道如何跑起来,不知道其中逻辑如何。由于函数式编程思维的方式和常见的命令式编程很不一样,这个困难更加突出。学习编程毕竟离不开敲代码,动手做练习,但是仓库里的练习题,检查是否正确,需要跨越多个文件去检查,很耗时麻烦。
21 |
22 | 所以,我制作这个系列的视频教程,期望能够让大家减少学习中的一些不必要的麻烦。
23 |
24 | # 更高效的学习体验
25 |
26 | - 代码逐步演示和解读,[quokka.js插件](https://marketplace.visualstudio.com/items?itemName=WallabyJs.quokka-vscode)
27 |
28 | 
29 |
30 | - 练习易上手,把测试合到一起,实时反馈
31 |
32 | 
33 |
34 | - 幻灯片划重点,方便复习,[marp插件](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode)
35 |
36 | 
37 |
--------------------------------------------------------------------------------
/ch00.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | style: @import 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css'
5 | paginate: true
6 | header: '函数式编程指南 第1章:我们在做什么'
7 | ---
8 |
9 | ## Github上最火的
函数式编程教程
10 |
11 | * Mostly Adequate Guide to Functional Programming,
12 | ⭐ 22.7k, Professor Franklin Frisby
13 | [MostlyAdequate/mostly-adequate-guide](https://github.com/MostlyAdequate/mostly-adequate-guide)
14 |
15 | ---
16 |
17 | ## 被翻译成众多语言
18 |
19 | * 中文版⭐ 2.2k,Linghao Li,函数式编程指北
20 | [llh911001/mostly-adequate-guide-chinese](https://github.com/llh911001/mostly-adequate-guide-chinese)
21 |
22 | ---
23 |
24 | ## 学习成果很美妙
但学习过程很艰辛
25 |
26 | * 代码没有亲手运行,逻辑一知半解
27 |
28 | * 做练习题时,检查是否正确很麻烦
29 |
30 | ---
31 |
32 | ## 本视频系列教程的目的
33 |
34 | * 减少不必要的麻烦
35 | * 更高效的学习体验
36 |
37 | ---
38 |
39 | ## 更高效的学习体验
40 |
41 | * 代码逐步演示和解读,`quokka.js`插件
42 | * 练习易上手,把测试合到一起,实时反馈
43 | * 幻灯片划重点,方便复习,`marp`插件
44 |
45 | ---
46 |
47 | 函数式编程指南视频讲解
48 |
49 | [ tarslab/mostly-adequate-guide-video-zh](https://github.com/tarslab/mostly-adequate-guide-video-zh)
50 |
51 | ---
52 |
53 | 
54 |
55 | 《函数式编程指南》
56 |
57 | # 关于本书
58 |
59 | * 主题是函数范式
(functional paradigm)
60 | * 用JavaScript来讲
61 |
62 | ---
63 |
64 | ## js是学习函数式编程的最好方式
65 |
66 | 为什么?
67 |
68 | ---
69 |
70 | ### 1. 你很有可能在日常工作中使用它
71 |
72 | - 有机会在实际的编程过程中学以致用
73 |
74 | ---
75 |
76 | ### 2. 你不必从头学起就能开始编写程序
77 |
78 | - Js容易入门
79 | - 是一门混合范式的语言
80 |
81 | ---
82 |
83 | ### 3. js有能力书写高级的函数式代码
84 |
85 | * 借助一到两个类库,模拟全部函数式特性
86 | * 面向对象范式在 js 里非常笨拙
87 |
88 | ---
89 |
90 | ### 函数范式是普适的
91 |
92 | * 所有的接口都是数学的
93 |
--------------------------------------------------------------------------------
/ch01.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第1章:我们在做什么'
6 | ---
7 |
8 | 
9 |
10 | 第 1 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 我们在
做什么
15 |
16 | ---
17 |
18 | ## 介绍
19 |
20 | Franklin Frisby 教授
21 |
22 | 教函数式编程的知识
23 |
24 | ---
25 |
26 | ### 希望你已经
27 |
28 | - 熟悉 JavaScript 语言
29 | - 有一些面向对象编程的经验
30 | - 会修复一些代码bug
31 |
32 | ---
33 |
34 | > 不需要有任何函数式编程相关的知识
35 |
36 | ---
37 |
38 | ### 本章的目的
39 |
40 | 对函数式编程的目标有初步的认识
41 |
42 | ---
43 |
44 | ### 常见的编程原则
45 |
46 | * 不要重复自己 DRY
47 | * 高内聚低耦合 loose coupling high cohesion
48 | * 你不会用到它的 YAGNI (ya ain't gonna need it)
49 | * 单一责任 single responsibility
50 |
51 | ---
52 |
53 | 常见的编程原则同样适用于函数式编程
54 |
55 | ---
56 |
57 | ### 后面还会讲到的数学知识
58 |
59 | * 范畴学(category theory)
60 | * 集合论(set theory)
61 | * lambda 运算
62 |
63 | ---
64 |
65 | ### 理论和实践结合
66 |
67 | > 我们希望去践行每一部分都能完美接合的理论,希望能以一种通用的、可组合的组件来表示我们的特定问题,然后利用这些组件的特性来解决这些问题。
68 |
69 | ---
70 |
71 | ### 强约束、数学性的“框架”
72 |
73 | > 对比命令式编程,函数式编程会有更多的约束, 但回报也更多。
74 |
--------------------------------------------------------------------------------
/ch01/c1_flock.js:
--------------------------------------------------------------------------------
1 | class Flock {
2 | constructor(n) {
3 | this.seagulls = n;
4 | }
5 |
6 | conjoin(other) {
7 | this.seagulls += other.seagulls;
8 | return this;
9 | }
10 |
11 | breed(other) {
12 | this.seagulls = this.seagulls * other.seagulls;
13 | return this;
14 | }
15 | }
16 |
17 | const flockA = new Flock(4);
18 | const flockB = new Flock(2);
19 | const flockC = new Flock(0);
20 |
21 | const result = flockA
22 | .conjoin(flockC)
23 | .breed(flockB)
24 | .conjoin(flockA.breed(flockB)).seagulls;
25 |
26 | console.log(result);
27 |
--------------------------------------------------------------------------------
/ch01/c2_flock.js:
--------------------------------------------------------------------------------
1 | const add = (x, y) => x + y;
2 | const multiply = (x, y) => x * y;
3 |
4 | const flockA = 4;
5 | const flockB = 2;
6 | const flockC = 0;
7 |
8 | const result = add(
9 | multiply(flockB, add(flockA, flockC)),
10 | multiply(flockA, flockB)
11 | );
12 |
13 | const x = 1, y = 2, z = 3;
14 | // 结合律(associative) x + (y + z) == (x + y) + z
15 | add(add(x, y), z) === add(x, add(y, z));
16 |
17 | // 交换律(commutative) x + y == y + x
18 | add(x, y) === add(y, x);
19 |
20 | // 同一律(identity) x + 0 == x
21 | add(x, 0) === x;
22 |
23 | // 分配律(distributive) x * (y + z) == x * y + x * z
24 | multiply(x, add(y, z)) === add(multiply(x, y), multiply(x, z));
25 |
26 | // 应用同一律,去掉多余的加法操作 add(flockA, flockC) == flockA
27 | add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB)) === add(multiply(flockB, flockA), multiply(flockA, flockB));
28 |
29 | // 再应用分配律
30 | add(multiply(flockB, flockA), multiply(flockA, flockB)) === multiply(flockB, add(flockA, flockA));
31 |
32 | console.log(result);
33 | console.log(multiply(flockB, add(flockA, flockA)));
34 |
--------------------------------------------------------------------------------
/ch02.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第2章:一等公民的函数'
6 | ---
7 |
8 | 
9 |
10 | 第 2 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 一等公民
的函数
15 |
16 | ---
17 |
18 | ## 快速概览
19 |
20 | "一等公民"函数和其他数据类型一样
21 |
22 | * 存在数组里
23 | * 当作参数传递
24 | * 赋值给变量...
25 |
26 | ---
27 |
28 | 这是 JavaScript 语言的基础概念
29 | 但是...
30 | 很多人对这个概念的集体无视
31 |
32 | ---
33 |
34 | ### 例子
35 |
36 | ```js
37 | // 太傻了
38 | const getServerStuff = (callback) => ajaxCall((json) => callback(json));
39 |
40 | // 这才像样
41 | const getServerStuff = ajaxCall;
42 | ```
43 |
44 | ---
45 |
46 | ```js
47 | // 这行
48 | ajaxCall(json => callback(json));
49 |
50 | // 等价于这行
51 | ajaxCall(callback);
52 | ```
53 |
54 | 基于上面,可以重构下
55 |
56 | ```js
57 | // 那么,重构前
58 | const getServerStuff = (callback) => ajaxCall(callback);
59 |
60 | // ...重构后
61 | const getServerStuff = ajaxCall; // 看,没有括号哦
62 | ```
63 |
64 | ---
65 |
66 | ```js
67 | const BlogController = {
68 | index(posts) { return Views.index(posts); },
69 | show(post) { return Views.show(post); },
70 | create(attrs) { return Db.create(attrs); },
71 | update(post, attrs) { return Db.update(post, attrs); },
72 | destroy(post) { return Db.destroy(post); },
73 | };
74 |
75 | // 重构后
76 | const BlogController = {
77 | index: Views.index,
78 | show: Views.show,
79 | create: Db.create,
80 | update: Db.update,
81 | destroy: Db.destroy,
82 | }
83 | ```
84 |
85 | ---
86 |
87 | ## 为何钟爱一等公民?
88 |
89 | ---
90 |
91 | ```js
92 | httpGet('/post/2', (json) => renderPost(json));
93 |
94 | // 如果renderPost新增个err参数,那么所有的httpGet都需要跟着改
95 | httpGet('/post/2', (json, err) => renderPost(json, err));
96 | ```
97 |
98 | 一等公民函数,则少了改动的麻烦
99 |
100 | ```js
101 | // renderPost参数变化不会影响到httpGet
102 | httpGet('/post/2', renderPost);
103 | ```
104 |
105 | ---
106 |
107 | ### 一等公民函数使用更通用的命名
108 |
109 | ```js
110 | // 只针对当前的博客项目
111 | const validArticles = (articles) =>
112 | articles.filter((article) => article !== null && article !== undefined);
113 |
114 | // 对未来的项目更友好
115 | const compact = (xs) => xs.filter((x) => x !== null && x !== undefined);
116 | ```
117 |
118 | ---
119 |
120 | ### 小心Js的this
121 |
122 | ```js
123 | const fs = require('fs');
124 |
125 | // 太可怕了
126 | fs.readFile('freaky_friday.txt', Db.save);
127 |
128 | // 好一点点
129 | fs.readFile('freaky_friday.txt', Db.save.bind(Db));
130 | ```
131 |
132 | ---
133 |
134 | ## 总结
135 |
136 | > 一等公民的函数,直接赋值给函数
不要加一层多余的包裹
137 |
138 | * 改动更少,参数的变动不会影响到函数的调用
139 | * 使用更通用的命名,对未来的项目更友好
140 |
--------------------------------------------------------------------------------
/ch02/c1_greeting.js:
--------------------------------------------------------------------------------
1 | const hi = (name) => `Hi ${name}`;
2 | const greeting = hi;
3 |
4 | console.log(hi);
5 | console.log(hi('jonas'));
6 |
7 | console.log(greeting);
8 | console.log(greeting('times'));
9 |
--------------------------------------------------------------------------------
/ch03.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第3章:纯函数的好处'
6 | ---
7 |
8 | 
9 |
10 | 第 3 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 纯函数
的好处
15 |
16 | ---
17 |
18 | ## 再次强调“纯”
19 |
20 | ### 纯函数的概念
21 |
22 | > 纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
23 |
24 | ---
25 |
26 | ## 副作用可能包括...
27 |
28 | 副作用是什么?
29 |
30 | > 副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
31 |
32 | ---
33 |
34 | ### 副作用可能包含,但不限于
35 |
36 | * 更改文件系统
37 | * 往数据库插入记录
38 | * 发送一个 http 请求
39 | * 修改数据
40 | * 打印/log
41 | * 获取用户输入
42 | * 查询 DOM
43 | * 访问系统状态
44 |
45 | ---
46 |
47 | ### 副作用让函数变得不纯
48 |
49 | * 纯函数, 相同输入得到相同输出
50 | * 跟外部事物打交道, 无法保证这一点
51 |
52 | ---
53 |
54 | ## 高中数学
55 |
56 | 函数是什么?
57 |
58 | > 函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。
59 |
60 | ---
61 |
62 | 
63 |
64 | ### 从 x 到 y 的函数关系
65 |
66 | $$f(x)=y$$
67 |
68 | ---
69 |
70 | 
71 |
72 | ### 不是函数关系
73 |
74 | * 输入值5指向了多个输出
75 |
76 | ---
77 |
78 | ### 函数可以描述为集合映射
79 |
80 | `(输入, 输出)对:[(1,2), (3,6), (5,10)]`
81 |
82 | | 输入 | 输出 |
83 | | --- | --- |
84 | | 1 | 2 |
85 | | 2 | 4 |
86 | | 3 | 6 |
87 | | 4 | 8 |
88 | | 5 | 10 |
89 |
90 | ---
91 |
92 | ### 函数曲线图
93 |
94 | 
95 |
96 | ---
97 |
98 | * 映射,另外一种思考函数的方式.
99 | * 函数有多个参数呢?后面的第4章柯里化会讲到
100 | * 纯函数就是数学上的函数,而且是整个函数式编程的理论基础
101 |
102 | ---
103 |
104 | ## 追求“纯”的理由
105 |
106 | ---
107 |
108 | ### 1. 可缓存性(Cacheable)
109 |
110 | 纯函数总能够根据输入来做缓存
111 |
112 | ---
113 |
114 | ### 2. 可移植性/自文档化(Portable / Self-Documenting)
115 |
116 | * 纯函数的依赖很明确,更易于观察和理解
117 | * 通过强迫“注入”依赖,或者把它们当作参数传递,应用也更加灵活
118 | * 你上一次把某个类方法拷贝到新的应用中是什么时候?
119 |
120 | ---
121 |
122 | > 面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林
123 |
124 | ---
125 |
126 | ### 3. 可测试性(Testable)
127 |
128 | * 只需简单地给函数一个输入,然后断言输出就好了
129 | * 函数式编程的社区在开创一些新的测试工具
130 |
131 | ---
132 |
133 | ### 4. 合理性(Reasonable)
134 |
135 | > 如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
136 |
137 | * 引用透明性(referential transparency)
138 | * 等式推导带来分析代码的能力, 有利于重构
139 |
140 | ---
141 |
142 | ### 5. 并行代码(Parallel code)
143 |
144 | * 可以并行运行任意纯函数
145 | * 纯函数不会因副作用而进入竞争态(race condition)
146 |
147 | ---
148 |
149 | ## 总结
150 |
151 | 1) 可缓存性(Cacheable)
152 | 2) 可移植性/自文档化(Portable / Self-Documenting)
153 | 3) 可测试性(Testable)
154 | 4) 合理性(Reasonable)
155 | 5) 并行代码(Parallel code)
156 |
--------------------------------------------------------------------------------
/ch03/c1_pure.js:
--------------------------------------------------------------------------------
1 | const xs = [1, 2, 3, 4, 5];
2 |
3 | // slice是纯的
4 | console.log(xs.slice(0, 3));
5 |
6 | console.log(xs.slice(0, 3));
7 |
8 | console.log(xs.slice(0, 3));
9 |
10 | // splice是不纯的
11 | console.log(xs.splice(0, 3));
12 |
13 | console.log(xs.splice(0, 3));
14 |
15 | console.log(xs.splice(0, 3));
16 |
--------------------------------------------------------------------------------
/ch03/c2_impure_checkAge.js:
--------------------------------------------------------------------------------
1 | // 不纯的
2 | let minimum = 21;
3 | const checkAge = (age) => age >= minimum;
4 |
5 | console.log(checkAge(24));
6 |
7 | minimum = 27;
8 | console.log(checkAge(24));
9 |
10 | minimum = 18;
11 | console.log(checkAge(24));
12 |
--------------------------------------------------------------------------------
/ch03/c3_pure_checkAge.js:
--------------------------------------------------------------------------------
1 | // 纯的
2 | const checkAge = (age) => {
3 | const minimum = 21;
4 | return age >= minimum;
5 | };
6 |
7 | console.log(checkAge(24));
8 |
9 | minimum = 27;
10 | console.log(checkAge(24));
11 |
12 | minimum = 18;
13 | console.log(checkAge(24));
14 |
--------------------------------------------------------------------------------
/ch03/c4_immutable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const immutableState = Object.freeze({ minimum: 21 });
4 | // 纯的
5 | const checkAge = (age) => age >= immutableState.minimum;
6 |
7 | console.log(checkAge(24));
8 |
--------------------------------------------------------------------------------
/ch03/c5_math.js:
--------------------------------------------------------------------------------
1 | const toLowerCase = {
2 | A: 'a',
3 | B: 'b',
4 | C: 'c',
5 | D: 'd',
6 | E: 'e',
7 | F: 'f'
8 | };
9 |
10 | console.log(toLowerCase['C']);
11 |
12 | const isPrime = {
13 | 1: false,
14 | 2: true,
15 | 3: true,
16 | 4: false,
17 | 5: true,
18 | 6: false
19 | };
20 |
21 | console.log(isPrime[3]);
22 |
--------------------------------------------------------------------------------
/ch03/c6_cacheable.js:
--------------------------------------------------------------------------------
1 | const memoize = (f) => {
2 | const cache = {};
3 |
4 | return (...args) => {
5 | const argStr = JSON.stringify(args);
6 | if (cache[argStr] === undefined) {
7 | console.log(`calculate input ${args}`);
8 | cache[argStr] = f(...args);
9 | } else {
10 | console.log(`returns cache for input ${args}`);
11 | }
12 | return cache[argStr];
13 | };
14 | };
15 |
16 | const squareNumber = memoize((x) => x * x);
17 |
18 | console.log(squareNumber(4));
19 |
20 | console.log(squareNumber(4));
21 |
22 | console.log(squareNumber(5));
23 |
24 | console.log(squareNumber(5));
25 |
26 | // http请求,纯函数, 延迟执行
27 | const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params));
28 |
--------------------------------------------------------------------------------
/ch03/c7_portable.js:
--------------------------------------------------------------------------------
1 | // impure
2 | const signUpImpure = (attrs) => {
3 | const user = saveUser(attrs);
4 | welcomeUser(user);
5 | };
6 |
7 | // pure
8 | const signUp = (Db, Email, attrs) => () => {
9 | const user = saveUser(Db, attrs);
10 | welcomeUser(Email, user);
11 | };
12 |
--------------------------------------------------------------------------------
/ch03/c8_reasonable.js:
--------------------------------------------------------------------------------
1 | const { Map } = require('immutable');
2 |
3 | // Aliases: p = player, a = attacker, t = target
4 | const jobe = Map({ name: 'Jobe', hp: 20, team: 'red' });
5 | const michael = Map({ name: 'Michael', hp: 20, team: 'green' });
6 |
7 | const decrementHP = (p) => p.set('hp', p.get('hp') - 1);
8 | const isSameTeam = (p1, p2) => p1.get('team') === p2.get('team');
9 |
10 | const punch = (a, t) => t.set('hp', t.get('hp') - 1);
11 |
12 | console.log(punch(jobe, michael));
13 |
--------------------------------------------------------------------------------
/ch04.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第4章:柯里化(curry)'
6 | ---
7 |
8 | 
9 |
10 | 第 4 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 柯里化
(curry)
15 |
16 | ---
17 |
18 | ## 不可或缺的 curry
19 |
20 | > 柯里的概念:传递一部分的参数来调用函数,它返回一个接受剩余参数的函数
21 |
22 | ---
23 |
24 | ## 不仅仅是双关语 / 咖喱
25 |
26 | * curry 用处非常广泛
27 | * 只需传给函数一些参数,得到一个新函数
28 |
29 | ---
30 |
31 | > map把参数是单个元素的函数包裹,
转换成参数为数组的函数
32 |
33 | ```js
34 | const getChildren = (x) => x.childNodes;
35 | ```
36 |
37 | map包裹:
38 |
39 | ```js
40 | const allTheChildren = map(getChildren);
41 | ```
42 |
43 | ---
44 |
45 | > 只传给函数一部分参数,也叫做局部调用(partial application)
46 |
47 | ---
48 |
49 | ```js
50 | import * as _ from 'lodash';
51 |
52 | const allTheChildren = (elements) => _.map(elements, getChildren);
53 | ```
54 |
55 | vs
56 |
57 | ```js
58 | // curry化
59 | const allTheChildren = map(getChildren);
60 | ```
61 |
62 | 对比下,下面的局部调用减少样板代码(boilerplate code)
63 |
64 | ---
65 |
66 | ## 总结
67 |
68 | * curry是函数式编程必备工具
69 | * 通过简单地传递几个参数,就能动态创建实用的新函数
70 | * 保留了数学的函数定义,尽管参数不止一个
71 |
--------------------------------------------------------------------------------
/ch04/c1_add.js:
--------------------------------------------------------------------------------
1 | const add = (x) => (y) => x + y;
2 | const increment = add(1);
3 | const addTen = add(10);
4 |
5 | console.log(increment(2));
6 | console.log(addTen(2));
7 |
--------------------------------------------------------------------------------
/ch04/c2_curry.js:
--------------------------------------------------------------------------------
1 | import { curry } from '@mostly-adequate/support';
2 |
3 | const match = curry((what, s) => s.match(what));
4 | const replace = curry((what, replacement, s) => s.replace(what, replacement));
5 | const filter = curry((f, xs) => xs.filter(f));
6 | const map = curry((f, xs) => xs.map(f));
7 |
8 | console.log(match(/r/g, 'hello world'));
9 |
10 | const hasLetterR = match(/r/g);
11 | console.log(hasLetterR('hello world'));
12 | console.log(hasLetterR('just j and s and t etc'));
13 |
14 | console.log(filter(hasLetterR, ['rock and roll', 'smooth jazz']));
15 |
16 | const removeStringsWithoutRs = filter(hasLetterR);
17 | console.log(removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']));
18 |
19 | const noVowels = replace(/[aeiou]/gi);
20 | const censored = noVowels('*');
21 | console.log(censored('Chocolate Rain'));
22 |
--------------------------------------------------------------------------------
/ch04/exercise_a.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 A
3 | * 通过局部调用(partial apply)移除所有参数
4 | */
5 | import { split } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // words :: String -> [String]
8 | const words = (str) => split(' ', str);
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert.arrayEqual(
15 | words('Jingle bells Batman smells'),
16 | ['Jingle', 'bells', 'Batman', 'smells'],
17 | 'The function gives incorrect results.'
18 | );
19 |
20 | assert(split.partially, 'The answer is incorrect; hint: split is currified!');
21 |
--------------------------------------------------------------------------------
/ch04/exercise_b.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 B
3 | * 通过局部调用(partial apply)移除所有参数
4 | */
5 | import { filter, match } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // filterQs :: [String] -> [String]
8 | const filterQs = (xs) => filter((x) => x.match(/q/i), xs);
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert.arrayEqual(
15 | filterQs(['quick', 'camels', 'quarry', 'over', 'quails']),
16 | ['quick', 'quarry', 'quails'],
17 | 'The function gives incorrect results.'
18 | );
19 |
20 | assert(
21 | filter.partially,
22 | 'The answer is incorrect; hint: look at the arguments for `filter`.'
23 | );
24 |
25 | assert(
26 | match.partially,
27 | 'The answer is incorrect; hint: look at the arguments for `match`.'
28 | );
29 |
--------------------------------------------------------------------------------
/ch04/exercise_c.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 C
3 | * 给定以下函数:
4 | *
5 | * const keepHighest = (x, y) => (x >= y ? x : y);
6 | *
7 | * 使用帮助函数 `keepHighest` 重构 `max`, 重构后的`max`不再引用任何参数。
8 | */
9 | import { keepHighest, reduce } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // max :: [Number] -> Number
12 | const max = (xs) => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | assert(
19 | max([323, 523, 554, 123, 5234]) === 5234,
20 | 'The function gives incorrect results.'
21 | );
22 |
23 | assert(
24 | reduce.partially,
25 | 'The answer is incorrect; hint: look at the arguments for `reduce`!'
26 | );
27 |
28 | assert(
29 | keepHighest.calledBy && keepHighest.calledBy.name === '$reduceIterator',
30 | "The answer is incorrect; hint: look closely to `reduce's` iterator and `keepHighest`!"
31 | );
32 |
--------------------------------------------------------------------------------
/ch05.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第5章:函数组合(compose)'
6 | ---
7 |
8 | 
9 |
10 | 第 5 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 函数组合
(compose)
15 |
16 | ---
17 |
18 | ## 函数饲养
19 |
20 | ### 组合的定义
21 |
22 | ```js
23 | const compose = (...fns) => (...args) =>
24 | fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
25 | ```
26 |
27 | ```js
28 | // 简化版,只考虑输入是两个函数的情况
29 | const compose2 = (f, g) => (x) => f(g(x));
30 | ```
31 |
32 | ---
33 |
34 | ### 组合像是在饲养函数
35 |
36 | > 你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数
37 |
38 | ---
39 |
40 | ### 组合的执行顺序
41 |
42 | * 组合中函数的执行顺序从右到左
43 | * 组合的概念直接来自于数学课本
44 |
45 | ---
46 |
47 | ## 组合符合结合律
48 |
49 | ```js
50 | compose(f, compose(g, h)) === compose(compose(f, g), h);
51 | ```
52 |
53 | 如果想把字符串变为大写,可以这么写:
54 |
55 | ```js
56 | compose(toUpperCase, compose(head, reverse));
57 | // 或者
58 | compose(compose(toUpperCase, head), reverse);
59 | ```
60 |
61 | ---
62 |
63 | * 结合律的好处,任何一个函数分组都可以被拆开来,
然后再以它们自己的组合方式打包在一起
64 | * 如何组合,最佳实践是让组合可复用
65 |
66 | ---
67 |
68 | ## Pointfree
69 |
70 | > 函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化以及组合协作起来有助于实现pointfree模式。
71 |
72 | ---
73 |
74 | ### Pointfree 模式
75 |
76 | * 能够减少不必要的命名
77 | * 石蕊试纸试验,能检测函数是接受输入返回输出的小函数
78 | * 是一把双刃剑,有时候也能混淆视听。
可以使用它的时候就使用,不能使用的时候就用普通函数。
79 |
80 | ---
81 |
82 | ## Debug
83 |
84 | ```js
85 | const trace = curry((tag, x) => {
86 | console.log(tag, x);
87 | return x;
88 | });
89 | ```
90 |
91 | * trace 函数允许我们在某个特定的点观察数据以便 debug
92 |
93 | ---
94 |
95 | ## 范畴学
96 |
97 | * 对象(object)
98 | * 态射(morphism)
99 | * 变化式(transformation)
100 |
101 | 这些概念跟编程的联系非常紧密
102 |
103 | ---
104 |
105 | 一些相同的概念分别在不同理论下的形式:
106 | 
107 |
108 | ---
109 |
110 | ### 一个范畴的构成
111 |
112 | * 对象的集合
113 | * 态射的集合
114 | * 态射的组合
115 | * 特殊态射identity
116 |
117 | ---
118 |
119 | ### 对象的集合
120 |
121 | * 把数据类型视作所有可能的值的一个集合
122 | * 举例,`Boolean` 是 `[true, false]` 的集合,`Number` 是所有实数的一个集合
123 | * 可以用集合论(set theory)处理类型
124 |
125 | ---
126 |
127 | ### 态射的集合
128 |
129 | 态射是标准的纯函数
130 |
131 | ---
132 |
133 | ### 态射的组合
134 |
135 | - 态射的组合就是本章讲的组合Compose
136 | - 结合律是在范畴学中对任何组合都适用的一个特性
137 |
138 | ---
139 |
140 | 
141 | 
142 |
143 | ```js
144 | const g = (x) => x.length;
145 | const f = (x) => x === 4;
146 | const isFourLetterWord = compose(f, g);
147 | ```
148 |
149 | ---
150 |
151 | ### 特殊态射identity
152 |
153 | ```js
154 | const id = (x) => x;
155 | ```
156 |
157 | 这个特性对所有的一元函数(unary function) f 都成立:
158 |
159 | ```js
160 | // identity
161 | compose(id, f) === compose(f, id) === f;
162 | // true
163 | ```
164 |
165 | ---
166 |
167 | ## 总结
168 |
169 | * 组合像一系列管道那样把不同的函数联系在一起,数据在其中流动
170 | * 组合是高于其他所有原则的设计原则
171 | * 范畴学将用在指导应用架构、副作用建模和保证正确性
172 |
--------------------------------------------------------------------------------
/ch05/c1_compose.js:
--------------------------------------------------------------------------------
1 | import { compose } from '@mostly-adequate/support';
2 |
3 | // 组合例子
4 | const toUpperCase = (x) => x.toUpperCase();
5 | const exclaim = (x) => `${x}!`;
6 | const shout = compose(exclaim, toUpperCase);
7 | console.log(shout('send in the clowns'));
8 |
9 | // 如果不用comose组合,直接调用
10 | const shout2 = (x) => exclaim(toUpperCase(x));
11 | console.log(shout2('send in the clowns'));
12 |
--------------------------------------------------------------------------------
/ch05/c2_last.js:
--------------------------------------------------------------------------------
1 | import { compose, reduce } from '@mostly-adequate/support';
2 |
3 | const head = (x) => x[0];
4 | const reverse = reduce((acc, x) => [x, ...acc], []);
5 | const last = compose(head, reverse);
6 |
7 | console.log(last(['jumpkick', 'roundhouse', 'uppercut']));
8 |
--------------------------------------------------------------------------------
/ch05/c3_variadic.js:
--------------------------------------------------------------------------------
1 | import { compose, reduce } from '@mostly-adequate/support';
2 |
3 | // 第1个例子
4 | const toUpperCase = (x) => x.toUpperCase();
5 | const exclaim = (x) => `${x}!`;
6 | const shout = compose(exclaim, toUpperCase);
7 |
8 | // 第2个例子
9 | const head = (x) => x[0];
10 | const reverse = reduce((acc, x) => [x, ...acc], []);
11 |
12 | // 前面的例子中我们必须要写两个组合才行,但既然组合是符合结合律的,我们就可以只写一个,
13 | // 而且想传给它多少个函数就传给它多少个,然后让它自己决定如何分组。
14 | const arg = ['jumpkick', 'roundhouse', 'uppercut'];
15 | const lastUpper = compose(toUpperCase, head, reverse);
16 | console.log(lastUpper(arg));
17 |
18 | const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
19 | console.log(loudLastUpper(arg));
20 |
21 | // loudLastUpper的第二种写法
22 | const last = compose(head, reverse);
23 | const loudLastUpper1 = compose(exclaim, toUpperCase, last);
24 | console.log(loudLastUpper1(arg));
25 |
26 | // loudLastUpper的第三种写法
27 | const angry = compose(exclaim, toUpperCase);
28 | const loudLastUpper2 = compose(angry, last);
29 | console.log(loudLastUpper2(arg));
30 |
31 | // 更多写法...
32 |
--------------------------------------------------------------------------------
/ch05/c4_snakeCase.js:
--------------------------------------------------------------------------------
1 | import { compose, replace, toLowerCase } from '@mostly-adequate/support';
2 |
3 | // 非 pointfree,因为提到了数据:word
4 | // const snakeCase = (word) => word.toLowerCase().replace(/\s+/ig, '_');
5 |
6 | // pointfree
7 | const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
8 |
9 | console.log(snakeCase('Hello World'));
10 |
--------------------------------------------------------------------------------
/ch05/c5_initials.js:
--------------------------------------------------------------------------------
1 | import { compose, head, intercalate, map, split, toUpperCase } from '@mostly-adequate/support';
2 |
3 | // 非 pointfree,因为提到了数据:name
4 | // const initials = (name) => name.split(' ').map(compose(toUpperCase, head)).join('. ');
5 |
6 | // pointfree
7 | const initials = compose(
8 | intercalate('. '),
9 | map(compose(toUpperCase, head)),
10 | split(' ')
11 | );
12 |
13 | console.log(initials('hunter stockton thompson'));
14 |
--------------------------------------------------------------------------------
/ch05/c6_debug.js:
--------------------------------------------------------------------------------
1 | import { compose, map, reverse, toUpperCase } from '@mostly-adequate/support';
2 |
3 | const exclaim = (x) => `${x}!`;
4 | const angry = compose(exclaim, toUpperCase);
5 |
6 | // 错误做法:我们传给了 angry 一个数组,根本不知道最后传给 map 的是什么东西。
7 | // const latin = compose(map, angry, reverse);
8 |
9 | // 正确做法:每个函数都接受一个实际参数。
10 | const latin = compose(map(angry), reverse);
11 |
12 | console.log(latin(['frog', 'eyes']));
13 |
--------------------------------------------------------------------------------
/ch05/c7_trace.js:
--------------------------------------------------------------------------------
1 | import { compose, curry, intercalate, map, replace, split, toLowerCase } from '@mostly-adequate/support';
2 |
3 | const trace = curry((tag, x) => {
4 | console.log(tag, x);
5 | return x;
6 | });
7 |
8 | const dasherize = compose(
9 | intercalate('-'),
10 | map(toLowerCase),
11 | trace('after split'),
12 | split(' '),
13 | replace(/s{2,}/gi, ' ')
14 | );
15 |
16 | console.log(dasherize('The world is a vampire'));
17 |
--------------------------------------------------------------------------------
/ch05/exercise_a.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 A
3 | 我们有这样结构的Car对象:
4 | {
5 | name: 'Aston Martin One-77',
6 | horsepower: 750,
7 | dollar_value: 1850000,
8 | in_stock: true,
9 | }
10 | 使用 `compose()` 重写下面这个函数`isLastInStock`
11 | */
12 | import { compose, last, prop } from '@tarslab/mostly-adequate-exercises';
13 |
14 | // isLastInStock :: [Car] -> Boolean
15 | const isLastInStock = (cars) => {
16 | const lastCar = last(cars);
17 | return prop('in_stock', lastCar);
18 | };
19 |
20 | // ------------------------------------
21 | // tests
22 | // ------------------------------------
23 |
24 | import { cars } from '@tarslab/mostly-adequate-exercises';
25 |
26 | const fixture01 = cars.slice(0, 3);
27 | const fixture02 = cars.slice(3);
28 |
29 | try {
30 | assert(isLastInStock(fixture01), 'The function gives incorrect results.');
31 |
32 | assert(!isLastInStock(fixture02), 'The function gives incorrect results.');
33 | } catch (err) {
34 | const callees = isLastInStock.callees || [];
35 |
36 | if (callees[0] === 'prop' && callees[1] === 'last') {
37 | throw new Error(
38 | 'The answer is incorrect; hint: functions are composed from right to left!'
39 | );
40 | }
41 |
42 | throw err;
43 | }
44 |
45 | assert.arrayEqual(
46 | isLastInStock.callees || [],
47 | ['last', 'prop'],
48 | 'The answer is incorrect; hint: prop is currified!'
49 | );
50 |
--------------------------------------------------------------------------------
/ch05/exercise_b.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 B
3 | 给定下面的函数:
4 |
5 | const average = xs => reduce(add, 0, xs) / xs.length;
6 |
7 | 使用帮助函数 `average` 重构 `averageDollarValue` 使之成为一个组合
8 | */
9 | import { average, compose, map, prop } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // averageDollarValue :: [Car] -> Int
12 | const averageDollarValue = (cars) => {
13 | const dollarValues = map((c) => c.dollar_value, cars);
14 | return average(dollarValues);
15 | };
16 |
17 | // ------------------------------------
18 | // tests
19 | // ------------------------------------
20 |
21 | import { cars } from '@tarslab/mostly-adequate-exercises';
22 |
23 | try {
24 | assert(
25 | averageDollarValue(cars) === 790700,
26 | 'The function gives incorrect results.'
27 | );
28 | } catch (err) {
29 | const callees = averageDollarValue.callees || [];
30 |
31 | if (callees[0] === 'average' && callees[1] === 'map') {
32 | throw new Error(
33 | 'The answer is incorrect; hint: functions are composed from right to left!'
34 | );
35 | }
36 |
37 | throw err;
38 | }
39 |
40 | assert.arrayEqual(
41 | averageDollarValue.callees || [],
42 | ['map', 'average'],
43 | 'The answer is incorrect; hint: map is currified!'
44 | );
45 |
46 | assert(
47 | prop.partially,
48 | "The answer is almost correct; hint: you can use prop to access objects' properties!"
49 | );
50 |
--------------------------------------------------------------------------------
/ch05/exercise_c.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 C
3 | 重构 `fastestCar` 使之成为 pointfree 风格,使用 `compose()` 和其他函数。
4 | */
5 | import { concat, compose, last, sortBy } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // fastestCar :: [Car] -> String
8 | const fastestCar = (cars) => {
9 | const sorted = sortBy((car) => car.horsepower, cars);
10 | const fastest = last(sorted);
11 | return concat(fastest.name, ' is the fastest');
12 | };
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | import { cars } from '@tarslab/mostly-adequate-exercises';
19 |
20 | try {
21 | assert(
22 | fastestCar(cars) === 'Aston Martin One-77 is the fastest',
23 | 'The function gives incorrect results.'
24 | );
25 | } catch (err) {
26 | const callees = fastestCar.callees || [];
27 |
28 | if (callees.length > 0 && callees[0] !== 'sortBy') {
29 | throw new Error(
30 | 'The answer is incorrect; hint: functions are composed from right to left!'
31 | );
32 | }
33 |
34 | throw err;
35 | }
36 |
37 | const callees = fastestCar.callees || [];
38 |
39 | assert.arrayEqual(
40 | callees.slice(0, 3),
41 | ['sortBy', 'last', 'prop'],
42 | 'The answer is incorrect; hint: Hindley-Milner signatures help a lot to reason about composition!'
43 | );
44 |
--------------------------------------------------------------------------------
/ch06.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第6章:示例应用'
6 | ---
7 |
8 | 
9 |
10 | 第 6 章
11 |
12 | 《函数式编程指南》
13 |
14 | # 示例应用
15 |
16 | ---
17 |
18 | ## 声明式代码
19 |
20 | * 不同于命令式的一步一步指令,声明式写的是表达式
21 | * 以SQL为例,没有“先做这个,再做那个”的指令
只有取什么数据的表达式,引擎自己决定如何取数据
22 |
23 | ---
24 |
25 | ### 命令式 vs 声明式
26 |
27 | ```js
28 | // 命令式
29 | const makes = [];
30 | for (let i = 0; i < cars.length; i += 1) {
31 | makes.push(cars[i].make);
32 | }
33 |
34 | // 声明式
35 | const makes = cars.map(car => car.make);
36 | ```
37 |
38 | ---
39 |
40 | * map函数如何具体实现,有很大的自由
41 | * 是what(做什么),不是how(怎么做)
42 | * "使用命令式循环速度要快很多?" [JIT编译器的视频链接](https://www.youtube.com/watch?v=65-RbBwZQdU)
43 |
44 | ---
45 |
46 | ### 声明式不指定执行顺序
47 |
48 | ```js
49 | // 命令式
50 | const authenticate = (form) => {
51 | const user = toUser(form);
52 | return logIn(user);
53 | };
54 |
55 | // 声明式
56 | const authenticate = compose(logIn, toUser);
57 | ```
58 |
59 | ---
60 |
61 | ### 声明式的好处
62 |
63 | * 为底层的代码变更留下空间,使得顶层的代码成为了一种高级规范(high level specification)
64 | * 不指定执行顺序,所以适用于并行运算
65 |
66 | ---
67 |
68 | ## 一个函数式的 Flickr
69 |
70 | - 目标:以声明式的、可组合的方式创建示例应用程序
71 | - 需求:浏览器 widget,从 Flickr 获取图片,在页面上展示
72 |
73 | ---
74 |
75 | ## 有原则的重构
76 |
77 | ```js
78 | // map的结合律
79 | compose(map(f), map(g)) === map(compose(f, g));
80 | ```
81 |
82 | ---
83 |
84 | ## 总结
85 |
86 | * 小而不失真实的应用中运用函数式编程
87 | * 用函数式“数学框架”来推导和重构代码
88 |
--------------------------------------------------------------------------------
/ch06/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flickr App
6 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ch06/main.js:
--------------------------------------------------------------------------------
1 | const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
2 | const ramda = CDN('ramda/0.21.0/ramda.min');
3 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min');
4 |
5 | requirejs.config({ paths: { ramda, jquery } });
6 | require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
7 | // 1. 根据搜索关键字构造 url
8 | // 2. 往 url 发送 api 请求
9 | // 3. 把返回的 json 转为图片地址
10 | // 4. 把图片地址放到 html
11 | // -- Utils ----------------------------------------------------------
12 | const Impure = {
13 | trace: curry((tag, x) => { console.log(tag, x); return x; }),
14 | getJSON: curry((callback, url) => $.getJSON(url, callback)),
15 | setHtml: curry((sel, html) => $(sel).html(html)),
16 | }
17 |
18 | // -- Pure -----------------------------------------------------------
19 | const host = 'api.flickr.com';
20 | const path = '/services/feeds/photos_public.gne';
21 | const query = (t) => `?tags=${t}&format=json&jsoncallback=?`;
22 | const url = (t) => `https://${host}${path}${query(t)}`;
23 |
24 | const img = (src) => $('
', { src });
25 | const mediaUrl = compose(prop('m'), prop('media'));
26 | const mediaToImg = compose(img, mediaUrl);
27 | const images = compose(map(mediaToImg), prop('items'));
28 |
29 | // -- Impure ---------------------------------------------------------
30 | const render = compose(Impure.setHtml('#js-main'), images);
31 | const app = compose(Impure.getJSON(render), url);
32 |
33 | app('cats');
34 | });
35 |
--------------------------------------------------------------------------------
/ch06/main_step2.js:
--------------------------------------------------------------------------------
1 | const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
2 | const ramda = CDN('ramda/0.21.0/ramda.min');
3 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min');
4 |
5 | requirejs.config({ paths: { ramda, jquery } });
6 | require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
7 | // 1. 根据搜索关键字构造 url
8 | // 2. 往 url 发送 api 请求
9 | // 3. 把返回的 json 转为图片地址
10 | // 4. 把图片地址放到 html
11 | const Impure = {
12 | trace: curry((tag, x) => { console.log(tag, x); return x; }),
13 | getJSON: curry((callback, url) => $.getJSON(url, callback)),
14 | setHtml: curry((sel, html) => $(sel).html(html)),
15 | }
16 |
17 | const host = 'api.flickr.com';
18 | const path = '/services/feeds/photos_public.gne';
19 | const query = (t) => `?tags=${t}&format=json&jsoncallback=?`;
20 | const url = (t) => `https://${host}${path}${query(t)}`;
21 |
22 | const app = compose(Impure.getJSON(Impure.trace('response')), url);
23 | app('cats');
24 | });
25 |
--------------------------------------------------------------------------------
/ch06/main_step3.js:
--------------------------------------------------------------------------------
1 | const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
2 | const ramda = CDN('ramda/0.21.0/ramda.min');
3 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min');
4 |
5 | requirejs.config({ paths: { ramda, jquery } });
6 | require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
7 | // 1. 根据搜索关键字构造 url
8 | // 2. 往 url 发送 api 请求
9 | // 3. 把返回的 json 转为图片地址
10 | // 4. 把图片地址放到 html
11 | const Impure = {
12 | trace: curry((tag, x) => { console.log(tag, x); return x; }),
13 | getJSON: curry((callback, url) => $.getJSON(url, callback)),
14 | setHtml: curry((sel, html) => $(sel).html(html)),
15 | }
16 |
17 | const host = 'api.flickr.com';
18 | const path = '/services/feeds/photos_public.gne';
19 | const query = (t) => `?tags=${t}&format=json&jsoncallback=?`;
20 | const url = (t) => `https://${host}${path}${query(t)}`;
21 |
22 | const mediaUrl = compose(prop('m'), prop('media'));
23 | const mediaUrls = compose(map(mediaUrl), prop('items'));
24 | const render = compose(Impure.setHtml('#js-main'), mediaUrls);
25 |
26 | const app = compose(Impure.getJSON(render), url);
27 | app('cats');
28 | });
29 |
--------------------------------------------------------------------------------
/ch06/main_step4.js:
--------------------------------------------------------------------------------
1 | const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
2 | const ramda = CDN('ramda/0.21.0/ramda.min');
3 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min');
4 |
5 | requirejs.config({ paths: { ramda, jquery } });
6 | require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
7 | // 1. 根据搜索关键字构造 url
8 | // 2. 往 url 发送 api 请求
9 | // 3. 把返回的 json 转为图片地址
10 | // 4. 把图片地址放到 html
11 | const Impure = {
12 | trace: curry((tag, x) => { console.log(tag, x); return x; }),
13 | getJSON: curry((callback, url) => $.getJSON(url, callback)),
14 | setHtml: curry((sel, html) => $(sel).html(html)),
15 | }
16 |
17 | const host = 'api.flickr.com';
18 | const path = '/services/feeds/photos_public.gne';
19 | const query = (t) => `?tags=${t}&format=json&jsoncallback=?`;
20 | const url = (t) => `https://${host}${path}${query(t)}`;
21 |
22 | const img = (src) => $('
', { src });
23 | const mediaUrl = compose(prop('m'), prop('media'));
24 | const mediaUrls = compose(map(mediaUrl), prop('items'));
25 | const images = compose(map(img), mediaUrls);
26 |
27 | const render = compose(Impure.setHtml('#js-main'), images);
28 |
29 | const app = compose(Impure.getJSON(render), url);
30 | app('cats');
31 | });
32 |
--------------------------------------------------------------------------------
/ch07.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第7章:Hindley-Milner类型签名'
6 | ---
7 |
8 | 
9 |
10 | 第 7 章
11 |
12 | 《函数式编程指南》
13 |
14 | # Hindley-Milner
类型签名
15 |
16 | ---
17 |
18 | ## 初识类型
19 |
20 | * 类型(type)是高效沟通的元语言
21 | * Hindley-Milner 类型系统
22 |
23 | ---
24 |
25 | ### 类型签名
26 |
27 | * 暴露函数的行为和目的
28 | * 衍生出"免费定理"概念
29 | * 可以很精确,也可以保持通用、抽象
30 | * 编译时检测,证明是最好的文档
31 |
32 | ---
33 |
34 | ### Js的类型签名
35 |
36 | * 类型检查工具 [Flow](http://flowtype.org/),[Typescript](http://www.typescriptlang.org/)
37 | * 选择所有函数式语言都遵循的 Hindley-Milner
38 |
39 | ---
40 |
41 | ## 神秘的传奇故事
42 |
43 | - Hindley-Milner 身影无处不在
44 |
45 | ---
46 |
47 | ### 类型命名原则
48 |
49 | - 相同的变量名,其类型也一定相同
50 |
51 | ```js
52 | // id :: a -> a
53 | const id = (x) => x;
54 |
55 | // map :: (a -> b) -> [a] -> [b]
56 | const map = curry((f, xs) => xs.map(f));
57 | ```
58 |
59 | ---
60 |
61 | ## 缩小可能性范围
62 |
63 | ### [Parametricity](https://en.wikipedia.org/wiki/Parametricity)
64 |
65 | - 函数对每一个可能的类型的操作都必须保持统一
66 |
67 | ---
68 |
69 | ### 猜猜函数的功能
70 |
71 | ```js
72 | // head :: [a] -> a
73 | ```
74 |
75 | 1. 返回数组的第一个?
76 | 2. 最后一个?
77 | 3. 随机一个?
78 |
79 | ---
80 |
81 | ```js
82 | // reverse :: [a] -> [a]
83 | ```
84 |
85 | 1. 排序?不能,缺失大小关系信息
86 | 2. 重新排列?有可能
87 | 3. 删除或重复某个元素?有可能
88 |
89 | - **类型a的多态性大幅缩小可能的范围**
90 |
91 | ---
92 |
93 | ### 签名搜索引擎
94 |
95 | - 可以用 [Hoogle](https://www.haskell.org/hoogle) 去搜索函数
96 |
97 | ---
98 |
99 | ## 免费定理
100 |
101 | 从 [Wadler 论文](http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf) 中随机选择出的
102 |
103 | - 定理1
104 |
105 | ```js
106 | // head :: [a] -> a
107 | compose(f, head) === compose(head, map(f));
108 | ```
109 |
110 | ---
111 |
112 | - 定理2:
113 |
114 | ```js
115 | // filter :: (a -> Bool) -> [a] -> [a]
116 | compose(map(f), filter(compose(p, f))) === compose(filter(p), map(f));
117 | ```
118 |
119 | ---
120 |
121 | ### 免费定理是普适的
122 |
123 | - 可以应用到任何多态性类型签名
124 |
125 | ---
126 |
127 | ## 类型约束
128 |
129 | ```js
130 | // sort :: Ord a => [a] -> [a]
131 | ```
132 |
133 | - a 必须实现 Ord 接口
134 | - 这种接口声明叫做类型约束(type constraints)
135 |
136 | ---
137 |
138 | 例子2:
139 |
140 | ```js
141 | // assertEqual :: (Eq a, Show a) => a -> a -> Assertion
142 | ```
143 |
144 | - a 有两个约束 Eq 和 Show
145 |
146 | ---
147 |
148 | ## 总结
149 |
150 | - Hindley-Milner 类型签名在函数式编程中无处不在
151 |
--------------------------------------------------------------------------------
/ch07/c1.js:
--------------------------------------------------------------------------------
1 | import { curry, head, toLowerCase, toUpperCase } from '@mostly-adequate/support';
2 |
3 | const tail = (xs) => xs.slice(1);
4 |
5 | // capitalize :: String -> String
6 | const capitalize = (s) => toUpperCase(head(s)) + toLowerCase(tail(s));
7 | console.log(capitalize('smurf'));
8 |
9 | // strLength :: String -> Number
10 | const strLength = (s) => s.length;
11 | console.log(strLength('hello'));
12 |
13 | // join :: String -> [String] -> String
14 | const join = curry((what, xs) => xs.join(what));
15 | console.log(join('-', ['mostly', 'adequate', 'guide']));
16 |
17 | // match :: Regex -> (String -> [String])
18 | const match = curry((reg, s) => s.match(reg));
19 | console.log(match(/l/g, 'hello world'));
20 |
21 | // onHoliday :: String -> [String]
22 | const onHoliday = match(/holiday/ig);
23 | console.log(onHoliday('Summer Holiday'));
24 |
25 | // replace :: Regex -> String -> String -> String
26 | const replace = curry((reg, sub, s) => s.replace(reg, sub));
27 | console.log(replace(/o/g, 'O', 'hello world'));
28 |
--------------------------------------------------------------------------------
/ch07/c2.js:
--------------------------------------------------------------------------------
1 | import { curry } from '@mostly-adequate/support';
2 |
3 | // head :: [a] -> a
4 | const head = (xs) => xs[0];
5 |
6 | // filter :: (a -> Bool) -> [a] -> [a]
7 | const filter = curry((f, xs) => xs.filter(f));
8 |
9 | // reduce :: ((b, a) -> b) -> b -> [a] -> b
10 | const reduce = curry((f, x, xs) => xs.reduce(f, x));
11 |
--------------------------------------------------------------------------------
/ch08.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | header: '函数式编程指南 第8章:Functor'
6 | ---
7 |
8 | 
9 |
10 | 第 8 章
11 |
12 | 《函数式编程指南》
13 |
14 | # Functor
15 |
16 | ---
17 |
18 | ## 万能的容器
19 |
20 | 
21 |
22 | * 容器 Container 只有一个属性
23 | * $value 不能局限类型
24 | * 数据一旦存放进去,就会一直在那里
25 |
26 | ---
27 |
28 | ## 第一个 Functor
29 |
30 | > functor 是实现了 map 函数
并遵守一些特定规则的容器类型
31 |
32 | ---
33 |
34 | ### 为什么要用map?
35 |
36 | - Q:让容器自己去运用函数的好处?
37 | - A:抽象,对于函数运用的抽象
38 |
39 | ---
40 |
41 | ## 薛定谔的 Maybe
42 |
43 | 
44 |
45 | ---
46 |
47 | ## 用例
48 |
49 | - 发送失败信号 Maybe(null)
50 | - map 遇到 null 会跳过计算
51 |
52 | ---
53 |
54 | ## 释放容器里的值
55 |
56 | - 任何函数都要有一个结束
57 | - Maybe里的不确定的值是魔鬼吗?
58 | - 如果函数没有完成使命,可能是其他代码分支造成的
59 |
60 | ---
61 |
62 | 
63 |
64 | > 代码应该像薛定谔的猫一样,同时处于两种状态,并且保持这种状态直到最后一个函数为止。哪怕代码有很多逻辑的分支,也能保证一个直线的顺序流
65 |
66 | ---
67 |
68 | ### Maybe 总结
69 |
70 | - Maybe 能够有效提高函数的安全性
71 | - Maybe 的“真正”实现会把它分为两种类型:
一种是非空值,另一种是空值
72 | - 遵守 map 的 parametricity 特性:
null 和 undefined 都能被 map 调用
73 |
74 | ---
75 |
76 | ## 纯的错误处理
77 |
78 | 
79 |
80 | ### Either
81 |
82 | - left 左边:错误情况下的值
83 | - right 右边:正确情况下的值
84 |
85 | ---
86 |
87 | ### Lift
88 |
89 | > 一个函数在调用的时候,如果被 map 包裹了,
那么它从一个非 functor 函数转换为一个 functor 函数,
我们把这个过程叫做 *Lift*
90 |
91 | ---
92 |
93 | ### 必要时再 Lift
94 |
95 | - 普通函数更适合操作普通的数据类型而不是容器类型
96 | - 必要的时候再通过 Lift 变为合适的容器去操作容器类型
97 | - 复用性更好,能够随需求而变,兼容任意functor
98 |
99 | ---
100 |
101 | ### Either 不只是个包含错误的容器
102 |
103 | - 表示逻辑或
104 | - 范畴学 coproduct
105 | - sum type (不相交并集)
106 |
107 | ---
108 |
109 | ## 多米诺骨牌的 IO
110 |
111 | 
112 |
113 | ---
114 |
115 | ### 压栈
116 |
117 | 
118 |
119 | > 传给 map 的函数并没有运行,我们只是把它们压到一个“运行栈”的最末端而已,一个函数紧挨着另一个函数,就像小心摆放的多米诺骨牌
120 |
121 | ---
122 |
123 | ### 谁放出野兽
124 |
125 | - 把责任推到调用者
126 |
127 | ---
128 |
129 | ## 异步 Task
130 |
131 | - 回调地狱
132 | - 使用 [Data.Task](https://github.com/folktale/data.task) 演示
133 |
134 | ---
135 |
136 | ### Task 机制
137 |
138 | - 类似IO,运行 Task 需要绿灯信号
139 | - 对于异步场景,IO 被 Task 取代
140 | - Task 的 map 操作,像 Todo 任务清单
141 | - Task 的 异步调用,用 fork 方法
142 |
143 | ---
144 |
145 | ### 各种 Functor 都有用武之地
146 |
147 | ---
148 |
149 | ## 范畴学理论
150 |
151 | 同一律和结合律
152 |
153 | ```js
154 | // identity
155 | map(id) === id;
156 |
157 | // composition
158 | compose(map(f), map(g)) === map(compose(f, g));
159 | ```
160 |
161 | ---
162 |
163 | ### Functor 映射到新范畴
164 |
165 | 
166 |
167 | ---
168 |
169 | ### Functor 映射
170 |
171 | 
172 |
173 | ---
174 |
175 | ### 殊路同归
176 |
177 | 
178 |
179 | ---
180 |
181 | ## 总结
182 |
183 | - Maybe、Either、IO、Task
184 | - Tree、List、Map、Pair
185 | - Event Stream、Observable
186 | - Functor 无处不在
187 |
--------------------------------------------------------------------------------
/ch08/c10_task.js:
--------------------------------------------------------------------------------
1 | // 注意,这里的代码是不完全的,不能运行,用来展示大概逻辑。
2 |
3 | // -- Pure application -------------------------------------------------
4 |
5 | // blogPage :: Posts -> HTML
6 | const blogPage = Handlebars.compile(blogTemplate);
7 |
8 | // renderPage :: Posts -> HTML
9 | const renderPage = compose(blogPage, sortBy(prop('date')));
10 |
11 | // blog :: Params -> Task Error HTML
12 | const blog = compose(map(renderPage), getJSON('/posts'));
13 |
14 | // -- Impure calling code ----------------------------------------------
15 |
16 | blog({}).fork(
17 | (error) => $('#error').html(error.message),
18 | (page) => $('#main').html(page)
19 | );
20 |
21 | $('#spinner').show();
22 |
--------------------------------------------------------------------------------
/ch08/c11_task.js:
--------------------------------------------------------------------------------
1 | // 注意,这里的代码是不完全的,不能运行,用来展示大概逻辑
2 |
3 | // Postgres.connect :: Url -> IO DbConnection
4 | // runQuery :: DbConnection -> ResultSet
5 | // readFile :: String -> Task Error String
6 |
7 | // -- Pure application -------------------------------------------------
8 |
9 | // dbUrl :: Config -> Either Error Url
10 | const dbUrl = ({ uname, pass, host, db }) => {
11 | if (uname && pass && host && db) {
12 | return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
13 | }
14 |
15 | return left(Error('Invalid config!'));
16 | };
17 |
18 | // connectDb :: Config -> Either Error (IO DbConnection)
19 | const connectDb = compose(map(Postgres.connect), dbUrl);
20 |
21 | // getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
22 | const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile);
23 |
24 | // -- Impure calling code ----------------------------------------------
25 |
26 | getConfig('db.json').fork(
27 | logErr("couldn't read file"),
28 | either(console.log, map(runQuery))
29 | );
30 |
--------------------------------------------------------------------------------
/ch08/c12_id.js:
--------------------------------------------------------------------------------
1 | import { append, map, compose } from '@mostly-adequate/support';
2 |
3 | class Container {
4 | constructor(x) {
5 | this.$value = x;
6 | }
7 |
8 | static of(x) {
9 | return new Container(x);
10 | }
11 | }
12 |
13 | Container.prototype.map = function (f) {
14 | return Container.of(f(this.$value));
15 | };
16 |
17 | const id = (x) => x;
18 | const idLaw1 = map(id);
19 | const idLaw2 = id;
20 |
21 | console.log(idLaw1(Container.of(2)));
22 | console.log(idLaw2(Container.of(2)));
23 |
24 | const compLaw1 = compose(map(append(' world')), map(append(' cruel')));
25 | const compLaw2 = map(compose(append(' world'), append(' cruel')));
26 |
27 | console.log(compLaw1(Container.of('Goodbye')));
28 | console.log(compLaw2(Container.of('Goodbye')));
29 |
--------------------------------------------------------------------------------
/ch08/c13_route.js:
--------------------------------------------------------------------------------
1 | import { compose, map, Maybe, reverse } from '@mostly-adequate/support';
2 |
3 | // 先 f 再 F.of
4 | // topRoute :: String -> Maybe String
5 | const topRoute = compose(Maybe.of, reverse);
6 |
7 | // 先 F.of 再 map(f)
8 | // bottomRoute :: String -> Maybe String
9 | const bottomRoute = compose(map(reverse), Maybe.of);
10 |
11 | console.log(topRoute('hi'));
12 | console.log(bottomRoute('hi'));
13 |
--------------------------------------------------------------------------------
/ch08/c14_nested.js:
--------------------------------------------------------------------------------
1 | import { Either, Maybe, Task, append, left, map, toUpperCase } from '@mostly-adequate/support';
2 |
3 | const nested = Task.of([Either.of('pillows'), left('no sleep for you')]);
4 | const t = map(map(map(toUpperCase)), nested);
5 |
6 | t.fork(
7 | (e) => {
8 | console.error(e);
9 | },
10 | (value) => {
11 | console.log(value);
12 | }
13 | );
14 |
15 | class Compose {
16 | constructor(fgx) {
17 | this.getCompose = fgx;
18 | }
19 | static of(fgx) {
20 | return new Compose(fgx);
21 | }
22 | map(fn) {
23 | return new Compose(map(map(fn), this.getCompose));
24 | }
25 | }
26 |
27 | const tmd = Task.of(Maybe.of('Rock over London'));
28 | const ctmd = Compose.of(tmd);
29 | const ctmd2 = map(append(', rock on, Chicago'), ctmd);
30 |
31 | ctmd2.getCompose.fork(
32 | (e) => {
33 | console.error(e);
34 | },
35 | (value) => {
36 | console.log(value);
37 | }
38 | );
39 |
--------------------------------------------------------------------------------
/ch08/c1_container.js:
--------------------------------------------------------------------------------
1 | class Container {
2 | constructor(x) {
3 | this.$value = x;
4 | }
5 |
6 | static of(x) {
7 | return new Container(x);
8 | }
9 | }
10 |
11 | console.log(Container.of(3));
12 | console.log(Container.of('hotdogs'));
13 | console.log(Container.of(Container.of({ name: 'yoda' })));
14 |
--------------------------------------------------------------------------------
/ch08/c2_functor.js:
--------------------------------------------------------------------------------
1 | import { append, prop } from '@mostly-adequate/support';
2 |
3 | class Container {
4 | constructor(x) {
5 | this.$value = x;
6 | }
7 |
8 | static of(x) {
9 | return new Container(x);
10 | }
11 | }
12 |
13 | // (a -> b) -> Container a -> Container b
14 | Container.prototype.map = function (f) {
15 | return Container.of(f(this.$value));
16 | };
17 |
18 | console.log(Container.of(2).map((two) => two + 2));
19 |
20 | console.log(Container.of('flamethrowers').map((s) => s.toUpperCase()));
21 |
22 | console.log(Container.of('bombs').map(append(' away')).map(prop('length')));
23 |
--------------------------------------------------------------------------------
/ch08/c3_maybe.js:
--------------------------------------------------------------------------------
1 | import { add, compose, curry, inspect, match, prop } from '@mostly-adequate/support';
2 |
3 | class Maybe {
4 | static of(x) {
5 | return new Maybe(x);
6 | }
7 |
8 | get isNothing() {
9 | return this.$value === null || this.$value === undefined;
10 | }
11 |
12 | constructor(x) {
13 | this.$value = x;
14 | }
15 |
16 | map(fn) {
17 | return this.isNothing ? this : Maybe.of(fn(this.$value));
18 | }
19 |
20 | inspect() {
21 | return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
22 | }
23 | }
24 |
25 | console.log(Maybe.of('Malkovich Malkovich').map(match(/a/gi)));
26 | console.log(Maybe.of(null).map(match(/a/gi)));
27 | console.log(Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10)));
28 | console.log(Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10)));
29 |
30 | // map :: Functor f => (a -> b) -> f a -> f b
31 | const map = curry((f, anyFunctor) => anyFunctor.map(f));
32 |
33 | // safeHead :: [a] -> Maybe(a)
34 | const safeHead = (xs) => Maybe.of(xs[0]);
35 |
36 | // streetName :: Object -> Maybe String
37 | const streetName = compose(map(prop('street')), safeHead, prop('addresses'));
38 |
39 | console.log(streetName({ addresses: [] }));
40 | console.log(streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] }));
41 |
42 | // withdraw :: Number -> Account -> Maybe(Account)
43 | const withdraw = curry((amount, { balance }) =>
44 | Maybe.of(balance >= amount ? { balance: balance - amount } : null));
45 |
46 | // updateLedger :: Account -> Account
47 | const updateLedger = (account) => account;
48 |
49 | // remainingBalance :: Account -> String
50 | const remainingBalance = ({ balance }) => `Your balance is $${balance}`;
51 |
52 | // finishTransaction :: Account -> String
53 | const finishTransaction = compose(remainingBalance, updateLedger);
54 |
55 | // getTwenty :: Account -> Maybe(String)
56 | const getTwenty = compose(map(finishTransaction), withdraw(20));
57 |
58 | console.log(getTwenty({ balance: 200.00 }));
59 | console.log(getTwenty({ balance: 10.00 }));
60 |
--------------------------------------------------------------------------------
/ch08/c4_maybe.js:
--------------------------------------------------------------------------------
1 | import { compose, curry, Maybe } from '@mostly-adequate/support';
2 |
3 | const withdraw = curry((amount, { balance }) =>
4 | Maybe.of(balance >= amount ? { balance: balance - amount } : null));
5 |
6 | // This function is hypothetical, not implemented here... nor anywhere else.
7 | // updateLedger :: Account -> Account
8 | const updateLedger = (account) => account;
9 |
10 | // remainingBalance :: Account -> String
11 | const remainingBalance = ({ balance }) => `Your balance is $${balance}`;
12 |
13 | // finishTransaction :: Account -> String
14 | const finishTransaction = compose(remainingBalance, updateLedger);
15 |
16 | // maybe :: b -> (a -> b) -> Maybe a -> b
17 | const maybe = curry((v, f, m) => {
18 | if (m.isNothing) {
19 | return v;
20 | }
21 |
22 | return f(m.$value);
23 | });
24 |
25 | // getTwenty :: Account -> String
26 | const getTwenty = compose(
27 | maybe("You're broke!", finishTransaction),
28 | withdraw(20)
29 | );
30 |
31 | console.log(getTwenty({ balance: 200.0 }));
32 | console.log(getTwenty({ balance: 10.0 }));
33 |
--------------------------------------------------------------------------------
/ch08/c5_either.js:
--------------------------------------------------------------------------------
1 | import { inspect, prop } from '@mostly-adequate/support';
2 |
3 | class Either {
4 | static of(x) {
5 | return new Right(x);
6 | }
7 | constructor(x) {
8 | this.$value = x;
9 | }
10 | }
11 |
12 | class Left extends Either {
13 | map(f) {
14 | return this;
15 | }
16 | inspect() {
17 | return `Left(${inspect(this.$value)})`;
18 | }
19 | }
20 |
21 | class Right extends Either {
22 | map(f) {
23 | return Either.of(f(this.$value));
24 | }
25 | inspect() {
26 | return `Right(${inspect(this.$value)})`;
27 | }
28 | }
29 |
30 | const left = (x) => new Left(x);
31 |
32 | console.log(Either.of('rain').map((str) => `b${str}`));
33 | console.log(
34 | left('rain').map((str) => `It's gonna ${str}, better bring your umbrella!`)
35 | );
36 | console.log(Either.of({ host: 'localhost', port: 80 }).map(prop('host')));
37 | console.log(left('rolls eyes...').map(prop('host')));
38 |
--------------------------------------------------------------------------------
/ch08/c6_either_getAge.js:
--------------------------------------------------------------------------------
1 | import { Either, add, compose, concat, curry, left, map, toString } from '@mostly-adequate/support';
2 | import moment from 'moment';
3 |
4 | // getAge :: Date -> User -> Either(String, Number)
5 | const getAge = curry((now, user) => {
6 | const birthDate = moment(user.birthDate, 'YYYY-MM-DD');
7 | return birthDate.isValid()
8 | ? Either.of(now.diff(birthDate, 'years'))
9 | : left('Birth date could not be parsed');
10 | });
11 |
12 | const today = '2024-01-01';
13 | console.log(getAge(moment(today), { birthDate: '2005-12-12' }));
14 | console.log(getAge(moment(today), { birthDate: 'July 4, 2001' }));
15 |
16 | // fortune :: Number -> String
17 | const fortune = compose(
18 | concat('If you survive, you will be '),
19 | toString,
20 | add(1)
21 | );
22 |
23 | // zoltar :: User -> Either(String, _)
24 | const zoltar = compose(map(fortune), getAge(moment(today)));
25 |
26 | console.log(zoltar({ birthDate: '2005-12-12' }));
27 | console.log(zoltar({ birthDate: 'balloons!' }));
28 |
--------------------------------------------------------------------------------
/ch08/c7_either.js:
--------------------------------------------------------------------------------
1 | import { Either, Left, Right, add, compose, concat, curry, left, toString } from '@mostly-adequate/support';
2 | import moment from 'moment';
3 |
4 | // getAge :: Date -> User -> Either(String, Number)
5 | const getAge = curry((now, user) => {
6 | const birthDate = moment(user.birthDate, 'YYYY-MM-DD');
7 | return birthDate.isValid()
8 | ? Either.of(now.diff(birthDate, 'years'))
9 | : left('Birth date could not be parsed');
10 | });
11 |
12 | // fortune :: Number -> String
13 | const fortune = compose(
14 | concat('If you survive, you will be '),
15 | toString,
16 | add(1)
17 | );
18 |
19 | // either :: (a -> c) -> (b -> c) -> Either a b -> c
20 | const either = curry((f, g, e) => {
21 | let result;
22 | switch (e.constructor) {
23 | case Left:
24 | result = f(e.$value);
25 | break;
26 | case Right:
27 | result = g(e.$value);
28 | break;
29 | // No Default
30 | }
31 | return result;
32 | });
33 |
34 | const id = (x) => x;
35 |
36 | const today = '2024-01-01';
37 |
38 | // zoltar :: User -> String
39 | const zoltar = compose(either(id, fortune), getAge(moment(today)));
40 | console.log(zoltar({ birthDate: '2005-12-12' }));
41 | console.log(zoltar({ birthDate: 'balloons!' }));
42 |
--------------------------------------------------------------------------------
/ch08/c8_io.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IO Window
6 |
7 |
8 |
9 | I am some inner html
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ch08/c8_io.js:
--------------------------------------------------------------------------------
1 | const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
2 | const ramda = CDN('ramda/0.21.0/ramda.min');
3 |
4 | requirejs.config({ paths: { ramda } });
5 | require(['ramda'], ({ compose, curry, find, head, inspect, last, map, prop, split }) => {
6 |
7 | // getFromStorage :: String -> (_ -> String)
8 | const getFromStorage = (key) => () => window.localStorage[key];
9 |
10 | class IO {
11 | static of(x) {
12 | return new IO(() => x);
13 | }
14 |
15 | constructor(fn) {
16 | this.unsafePerformIO = fn;
17 | }
18 |
19 | map(fn) {
20 | return new IO(compose(fn, this.unsafePerformIO));
21 | }
22 |
23 | inspect() {
24 | return `IO(${inspect(this.unsafePerformIO)})`;
25 | }
26 | }
27 |
28 | // ioWindow :: IO Window
29 | const ioWindow = new IO(() => window);
30 |
31 | console.log(ioWindow.map((win) => win.innerWidth).unsafePerformIO());
32 | console.log(ioWindow.map(prop('location')).unsafePerformIO());
33 | console.log(ioWindow.map(prop('location')).map(prop('href')).map(split('/')).unsafePerformIO());
34 |
35 | // $ :: String -> IO [DOM]
36 | const $ = (selector) => new IO(() => document.querySelectorAll(selector));
37 |
38 | console.log(
39 | $('#myDiv')
40 | .map(head)
41 | .map((div) => div.innerHTML)
42 | .unsafePerformIO()
43 | );
44 |
45 | class Maybe {
46 | static of(x) {
47 | return new Maybe(x);
48 | }
49 |
50 | get isNothing() {
51 | return this.$value === null || this.$value === undefined;
52 | }
53 |
54 | constructor(x) {
55 | this.$value = x;
56 | }
57 |
58 | map(fn) {
59 | return this.isNothing ? this : Maybe.of(fn(this.$value));
60 | }
61 |
62 | inspect() {
63 | return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
64 | }
65 | }
66 |
67 | const eq = curry((a, b) => a === b);
68 |
69 | // url :: IO String
70 | const url = new IO(() => window.location.href);
71 |
72 | // toPairs :: String -> [[String]]
73 | const toPairs = compose(map(split('=')), split('&'));
74 |
75 | // params :: String -> [[String]]
76 | const params = compose(toPairs, last, split('?'));
77 |
78 | // findParam :: String -> IO Maybe [String]
79 | const findParam = (key) => map(compose(Maybe.of, find(compose(eq(key), head)), params), url);
80 |
81 | // -- Impure calling code ----------------------------------------------
82 |
83 | // http://127.0.0.1:3000/ch08/c8_io.html?searchTerm=wafflehouse
84 | console.log(findParam('searchTerm').unsafePerformIO());
85 | });
86 |
--------------------------------------------------------------------------------
/ch08/c9_task.js:
--------------------------------------------------------------------------------
1 | // -- Node readFile example ------------------------------------------
2 | import { curry, head, prop, split } from '@mostly-adequate/support';
3 | const Task = require('data.task');
4 | const fs = require('fs');
5 |
6 | // readFile :: String -> Task Error String
7 | const readFile = (filename) =>
8 | new Task((reject, result) => {
9 | fs.readFile(filename, 'utf8', (err, data) => (err ? reject(err) : result(data)));
10 | });
11 |
12 | readFile('./ch08/metamorphosis.txt')
13 | .map(split('\n'))
14 | .map(head)
15 | .fork(
16 | (e) => {
17 | console.error(e);
18 | },
19 | (value) => {
20 | console.log(value);
21 | }
22 | );
23 |
24 | // -- jQuery getJSON example -----------------------------------------
25 |
26 | // getJSON :: String -> {} -> Task Error JSON
27 | const getJSON = curry(
28 | (url, params) =>
29 | new Task((reject, result) => {
30 | $.getJSON(url, params, result).fail(reject);
31 | })
32 | );
33 |
34 | getJSON('/video', { id: 10 }).map(prop('title'));
35 | // Task('Family Matters ep 15')
36 |
37 | // -- Default Minimal Context ----------------------------------------
38 |
39 | // We can put normal, non futuristic values inside as well
40 | Task.of(3)
41 | .map((three) => three + 1)
42 | .fork(
43 | (e) => {
44 | console.error('err', e);
45 | },
46 | (value) => {
47 | console.log('value', value);
48 | }
49 | );
50 |
--------------------------------------------------------------------------------
/ch08/exercise_a.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 A
3 | 使用 `add` 和 `map` 创建一个 functor,使得 functor 里的值增加 1
4 | */
5 | import { Identity, add, map } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // incrF :: Functor f => f Int -> f Int
8 | const incrF = undefined;
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert(incrF(Identity.of(2)).$value === 3, 'The function gives incorrect results.');
15 |
16 | assert(add.partially, 'The answer is incorrect; hint: add is currified!');
17 |
18 | assert(map.partially, 'The answer is incorrect; hint: map is currified!');
19 |
--------------------------------------------------------------------------------
/ch08/exercise_b.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 B
3 | 给定下面的 User 对象:
4 |
5 | const user = { id: 2, name: 'Albert', active: true };
6 |
7 | 使用 `safeProp` 和 `head` 找到 user 名字的首字母。
8 | */
9 | import { compose, head, map, safeProp } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // initial :: User -> Maybe String
12 | const initial = undefined;
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | import { albert, Maybe } from '@tarslab/mostly-adequate-exercises';
19 |
20 | if (
21 | !(initial(albert) instanceof Maybe) &&
22 | initial.callees &&
23 | initial.callees[0] === 'safeProp' &&
24 | initial.callees[1] === 'head'
25 | ) {
26 | throw new Error(
27 | 'The function gives incorrect results; hint: look carefully at the signatures of `safeProp` and `head`!'
28 | );
29 | }
30 |
31 | assert(initial(albert).$value === 'A', 'The function gives incorrect results.');
32 |
33 | assert.arrayEqual(
34 | initial.callees || [],
35 | ['safeProp', 'map'],
36 | 'The answer is incorrect; hint: you can compose `safeProp` with `head` in a declarative way'
37 | );
38 |
--------------------------------------------------------------------------------
/ch08/exercise_c.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 C
3 | 给定下面的帮助函数:
4 |
5 | // showWelcome :: User -> String
6 | const showWelcome = compose(concat('Welcome '), prop('name'));
7 |
8 | // checkActive :: User -> Either String User
9 | const checkActive = function checkActive(user) {
10 | return user.active
11 | ? Either.of(user)
12 | : left('Your account is not active');
13 | };
14 |
15 | 写一个函数,使用 `checkActive` 和 `showWelcome` 授予访问权限或返回错误。
16 | */
17 | import { checkActive, compose, map, showWelcome } from '@tarslab/mostly-adequate-exercises';
18 |
19 | // eitherWelcome :: User -> Either String String
20 | const eitherWelcome = undefined;
21 |
22 | // ------------------------------------
23 | // tests
24 | // ------------------------------------
25 |
26 | import { Either, gary, theresa } from '@tarslab/mostly-adequate-exercises';
27 |
28 | if (
29 | !(eitherWelcome(gary) instanceof Either) &&
30 | eitherWelcome.callees &&
31 | eitherWelcome.callees[0] === 'checkActive' &&
32 | eitherWelcome.callees[1] === 'showWelcome'
33 | ) {
34 | throw new Error(
35 | 'The function gives incorrect results; hint: look carefully at the signatures of `checkActive` and `showWelcome`!'
36 | );
37 | }
38 |
39 | assert(eitherWelcome(gary).$value === 'Your account is not active', 'The function gives incorrect results.');
40 |
41 | assert(eitherWelcome(theresa).$value === 'Welcome Theresa', 'The function gives incorrect results.');
42 |
43 | assert.arrayEqual(
44 | eitherWelcome.callees || [],
45 | ['checkActive', 'map'],
46 | 'The answer is incorrect; hint: you can compose `checkActive` with `showWelcome` in a declarative way!'
47 | );
48 |
--------------------------------------------------------------------------------
/ch08/exercise_d.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 D
3 |
4 | 给定下面的帮助函数:
5 |
6 | // validateUser :: (User -> Either String ()) -> User -> Either String User
7 | const validateUser = curry((validate, user) => validate(user).map(_ => user));
8 |
9 | // save :: User -> IO User
10 | const save = user => new IO(() => ({ ...user, saved: true }));
11 |
12 | // showWelcome :: User -> String
13 | const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
14 |
15 | 编写一个 `validateName` 函数,检查用户的名字是否超过3个字符,或者返回一个错误消息。
16 | 然后使用 `either`,`showWelcome` 和 `save` 来编写一个 `register` 函数,当验证通过时注册并欢迎用户。
17 |
18 | 记住,either的两个参数必须返回相同的类型。
19 | */
20 | import { Either, IO, compose, either, left, map, save, showWelcome, validateUser } from '@tarslab/mostly-adequate-exercises';
21 |
22 | // validateName :: User -> Either String ()
23 | const validateName = undefined;
24 |
25 | // register :: User -> IO String
26 | const register = compose(undefined, validateUser(validateName));
27 |
28 | // ------------------------------------
29 | // tests
30 | // ------------------------------------
31 |
32 | import { albert, gary, yi } from '@tarslab/mostly-adequate-exercises';
33 |
34 | const validateGary = validateName(gary);
35 | assert(
36 | validateGary instanceof Either && validateGary.isRight,
37 | 'The function `validateName` gives incorrect results.'
38 | );
39 |
40 | const validateYi = validateName(yi);
41 | assert(
42 | validateYi instanceof Either &&
43 | validateYi.isLeft &&
44 | typeof validateYi.$value === 'string',
45 | 'The function `validateName` gives incorrect results!'
46 | );
47 |
48 | const registerAlbert = register(albert);
49 | assert(
50 | registerAlbert instanceof IO,
51 | "The right outcome to `register` is incorrect; hint: `save` returns an `IO` and you'll need `map` to manipulate the inner value!"
52 | );
53 |
54 | const msgAlbert = registerAlbert.unsafePerformIO();
55 | assert(
56 | typeof msgAlbert === 'string',
57 | 'The right outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!'
58 | );
59 |
60 | const callees = register.callees || [];
61 |
62 | assert(
63 | callees[callees.length - 1] === 'either',
64 | 'The function `register` seems incorrect; hint: you can use `either` to branch an `Either` to different outcomes!'
65 | );
66 |
67 | assert(
68 | msgAlbert === showWelcome(albert),
69 | 'The function `register` returns a correct type, but the inner value is incorrect! Did you use `showWelcome`?'
70 | );
71 |
72 | const registerYi = register(yi);
73 | assert(
74 | registerYi instanceof IO,
75 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO` in every scenarios!'
76 | );
77 |
78 | const msgYi = registerYi.unsafePerformIO();
79 | assert(
80 | typeof msgYi === 'string',
81 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!'
82 | );
83 |
--------------------------------------------------------------------------------
/ch08/metamorphosis.txt:
--------------------------------------------------------------------------------
1 | One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that in bed he had been changed into a monstrous verminous bug.
2 | He lay on his armour-hard back and saw, as he lifted his head up a little, his brown, arched abdomen divided up into rigid bow-like sections.
3 | From this height the blanket, just about ready to slide off completely, could hardly stay in place.
4 | His numerous legs, pitifully thin in comparison to the rest of his circumference, flickered helplessly before his eyes.
5 |
--------------------------------------------------------------------------------
/covers.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | paginate: true
5 | ---
6 |
7 | 
8 |
9 | 第 1 章
10 |
11 | 《函数式编程指南》
12 |
13 | # 我们在
做什么
14 |
15 | ---
16 |
17 | 
18 |
19 | 第 2 章
20 |
21 | 《函数式编程指南》
22 |
23 | # 一等公民
的函数
24 |
25 | ---
26 |
27 | 
28 |
29 | 第 3 章
30 |
31 | 《函数式编程指南》
32 |
33 | # 纯函数
的好处
34 |
35 | ---
36 |
37 | 
38 |
39 | 第 4 章
40 |
41 | 《函数式编程指南》
42 |
43 | # 柯里化
(curry)
44 |
45 | ---
46 |
47 | 
48 |
49 | 第 5 章
50 |
51 | 《函数式编程指南》
52 |
53 | # 函数组合
(compose)
54 |
55 | ---
56 |
57 | 
58 |
59 | 第 6 章
60 |
61 | 《函数式编程指南》
62 |
63 | # 示例应用
64 |
65 | ---
66 |
67 | 
68 |
69 | 第 7 章
70 |
71 | 《函数式编程指南》
72 |
73 | # Hindley-Milner
类型签名
74 |
75 | ---
76 |
77 | 
78 |
79 | 第 8 章
80 |
81 | 《函数式编程指南》
82 |
83 | # Functor
84 |
85 | ---
86 |
87 | 
88 |
89 | 第 9 章
90 |
91 | 《函数式编程指南》
92 |
93 | # Monad
94 |
95 | ---
96 |
97 | 
98 |
99 | 第 10 章
100 |
101 | 《函数式编程指南》
102 |
103 | # Applicative
Functor
104 |
105 | ---
106 |
107 | 
108 |
109 | 第 11 章
110 |
111 | 《函数式编程指南》
112 |
113 | # 再转换一次
就很自然
114 |
115 | ---
116 |
117 | 
118 |
119 | 第 12 章
120 |
121 | 《函数式编程指南》
122 |
123 | # 遍历
124 |
125 | ---
126 |
127 | 
128 |
129 | 第 13 章
130 |
131 | 《函数式编程指南》
132 |
133 | # 集大成者的
Monoid
134 |
--------------------------------------------------------------------------------
/images/canopener.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/canopener.jpg
--------------------------------------------------------------------------------
/images/cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cat.png
--------------------------------------------------------------------------------
/images/cat_comp1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cat_comp1.png
--------------------------------------------------------------------------------
/images/cat_comp2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cat_comp2.png
--------------------------------------------------------------------------------
/images/cat_theory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cat_theory.png
--------------------------------------------------------------------------------
/images/catmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/catmap.png
--------------------------------------------------------------------------------
/images/cats_ss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cats_ss.png
--------------------------------------------------------------------------------
/images/chain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/chain.jpg
--------------------------------------------------------------------------------
/images/console_ss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/console_ss.png
--------------------------------------------------------------------------------
/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/cover.png
--------------------------------------------------------------------------------
/images/dominoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/dominoes.jpg
--------------------------------------------------------------------------------
/images/feature-exercise.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/feature-exercise.gif
--------------------------------------------------------------------------------
/images/feature-marp.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/feature-marp.gif
--------------------------------------------------------------------------------
/images/feature-quokkaJs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/feature-quokkaJs.gif
--------------------------------------------------------------------------------
/images/fists.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/fists.jpg
--------------------------------------------------------------------------------
/images/fn_graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/fn_graph.png
--------------------------------------------------------------------------------
/images/function-sets.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/function-sets.gif
--------------------------------------------------------------------------------
/images/functormap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/functormap.png
--------------------------------------------------------------------------------
/images/functormapmaybe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/functormapmaybe.png
--------------------------------------------------------------------------------
/images/id_to_maybe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/id_to_maybe.png
--------------------------------------------------------------------------------
/images/jar-functor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/jar-functor.jpg
--------------------------------------------------------------------------------
/images/jar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/jar.jpg
--------------------------------------------------------------------------------
/images/monad_associativity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/monad_associativity.png
--------------------------------------------------------------------------------
/images/natural_transformation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/natural_transformation.png
--------------------------------------------------------------------------------
/images/onion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/onion.png
--------------------------------------------------------------------------------
/images/relation-not-function.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/relation-not-function.gif
--------------------------------------------------------------------------------
/images/ship_in_a_bottle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/ship_in_a_bottle.jpg
--------------------------------------------------------------------------------
/images/triangle_identity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TarsLab/mostly-adequate-guide-video-zh/b3731553c67dc6f4b008407a84aaaa752788014e/images/triangle_identity.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mostly-adequate-guide-video-zh",
3 | "version": "1.0.0",
4 | "description": "The Mostly Adequate Guide to Functional Programming",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/TarsLab/mostly-adequate-guide-video-zh"
8 | },
9 | "keywords": [
10 | "javascript",
11 | "guide",
12 | "functional programming"
13 | ],
14 | "author": "C jack",
15 | "license": "CC BY-SA",
16 | "dependencies": {
17 | "@mostly-adequate/support": "^2.0.1",
18 | "@tarslab/mostly-adequate-exercises": "^1.0.0",
19 | "data.task": "^3.1.2",
20 | "immutable": "^4.3.0",
21 | "moment": "^2.29.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/solutions/ch04/solution_a.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 A
3 | * 通过局部调用(partial apply)移除所有参数
4 | */
5 | import { split } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // words :: String -> [String]
8 | const words = split(' ');
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert.arrayEqual(
15 | words('Jingle bells Batman smells'),
16 | ['Jingle', 'bells', 'Batman', 'smells'],
17 | 'The function gives incorrect results.'
18 | );
19 |
20 | assert(split.partially, 'The answer is incorrect; hint: split is currified!');
21 |
--------------------------------------------------------------------------------
/solutions/ch04/solution_b.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 B
3 | * 通过局部调用(partial apply)移除所有参数
4 | */
5 | import { filter, match } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // filterQs :: [String] -> [String]
8 | const filterQs = filter(match(/q/i));
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert.arrayEqual(
15 | filterQs(['quick', 'camels', 'quarry', 'over', 'quails']),
16 | ['quick', 'quarry', 'quails'],
17 | 'The function gives incorrect results.'
18 | );
19 |
20 | assert(
21 | filter.partially,
22 | 'The answer is incorrect; hint: look at the arguments for `filter`.'
23 | );
24 |
25 | assert(
26 | match.partially,
27 | 'The answer is incorrect; hint: look at the arguments for `match`.'
28 | );
29 |
--------------------------------------------------------------------------------
/solutions/ch04/solution_c.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 练习 C
3 | * 给定以下函数:
4 | *
5 | * const keepHighest = (x, y) => (x >= y ? x : y);
6 | *
7 | * 使用帮助函数 `keepHighest` 重构 `max`, 重构后的`max`不再引用任何参数。
8 | */
9 | import { keepHighest, reduce } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // max :: [Number] -> Number
12 | const max = reduce(keepHighest, -Infinity);
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | assert(
19 | max([323, 523, 554, 123, 5234]) === 5234,
20 | 'The function gives incorrect results.'
21 | );
22 |
23 | assert(
24 | reduce.partially,
25 | 'The answer is incorrect; hint: look at the arguments for `reduce`!'
26 | );
27 |
28 | assert(
29 | keepHighest.calledBy && keepHighest.calledBy.name === '$reduceIterator',
30 | "The answer is incorrect; hint: look closely to `reduce's` iterator and `keepHighest`!"
31 | );
32 |
--------------------------------------------------------------------------------
/solutions/ch05/solution_a.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 A
3 | 我们有这样结构的Car对象:
4 | {
5 | name: 'Aston Martin One-77',
6 | horsepower: 750,
7 | dollar_value: 1850000,
8 | in_stock: true,
9 | }
10 | 使用 `compose()` 重写下面这个函数`isLastInStock`
11 | */
12 | import { compose, last, prop } from '@tarslab/mostly-adequate-exercises';
13 |
14 | // isLastInStock :: [Car] -> Boolean
15 | const isLastInStock = compose(prop('in_stock'), last);
16 |
17 | // ------------------------------------
18 | // tests
19 | // ------------------------------------
20 |
21 | import { cars } from '@tarslab/mostly-adequate-exercises';
22 |
23 | const fixture01 = cars.slice(0, 3);
24 | const fixture02 = cars.slice(3);
25 | try {
26 | assert(isLastInStock(fixture01), 'The function gives incorrect results.');
27 |
28 | assert(!isLastInStock(fixture02), 'The function gives incorrect results.');
29 | } catch (err) {
30 | const callees = isLastInStock.callees || [];
31 |
32 | if (callees[0] === 'prop' && callees[1] === 'last') {
33 | throw new Error(
34 | 'The answer is incorrect; hint: functions are composed from right to left!'
35 | );
36 | }
37 |
38 | throw err;
39 | }
40 |
41 | assert.arrayEqual(
42 | isLastInStock.callees || [],
43 | ['last', 'prop'],
44 | 'The answer is incorrect; hint: prop is currified!'
45 | );
46 |
--------------------------------------------------------------------------------
/solutions/ch05/solution_b.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 B
3 | 给定下面的函数:
4 |
5 | const average = xs => reduce(add, 0, xs) / xs.length;
6 |
7 | 使用帮助函数 `average` 重构 `averageDollarValue` 使之成为一个组合
8 | */
9 | import { average, compose, map, prop } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // averageDollarValue :: [Car] -> Int
12 | const averageDollarValue = compose(average, map(prop('dollar_value')));
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | import { cars } from '@tarslab/mostly-adequate-exercises';
19 |
20 | try {
21 | assert(
22 | averageDollarValue(cars) === 790700,
23 | 'The function gives incorrect results.'
24 | );
25 | } catch (err) {
26 | const callees = averageDollarValue.callees || [];
27 |
28 | if (callees[0] === 'average' && callees[1] === 'map') {
29 | throw new Error(
30 | 'The answer is incorrect; hint: functions are composed from right to left!'
31 | );
32 | }
33 |
34 | throw err;
35 | }
36 |
37 | assert.arrayEqual(
38 | averageDollarValue.callees || [],
39 | ['map', 'average'],
40 | 'The answer is incorrect; hint: map is currified!'
41 | );
42 |
43 | assert(
44 | prop.partially,
45 | "The answer is almost correct; hint: you can use prop to access objects' properties!"
46 | );
47 |
--------------------------------------------------------------------------------
/solutions/ch05/solution_c.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 C
3 | 重构 `fastestCar` 使之成为 pointfree 风格,使用 `compose()` 和其他函数。
4 | */
5 | import {
6 | append,
7 | compose,
8 | last,
9 | prop,
10 | sortBy
11 | } from '@tarslab/mostly-adequate-exercises';
12 |
13 | // fastestCar :: [Car] -> String
14 | const fastestCar = compose(
15 | append(' is the fastest'),
16 | prop('name'),
17 | last,
18 | sortBy(prop('horsepower'))
19 | );
20 |
21 | // ------------------------------------
22 | // tests
23 | // ------------------------------------
24 |
25 | import { cars } from '@tarslab/mostly-adequate-exercises';
26 |
27 | try {
28 | assert(
29 | fastestCar(cars) === 'Aston Martin One-77 is the fastest',
30 | 'The function gives incorrect results.'
31 | );
32 | } catch (err) {
33 | const callees = fastestCar.callees || [];
34 |
35 | if (callees.length > 0 && callees[0] !== 'sortBy') {
36 | throw new Error(
37 | 'The answer is incorrect; hint: functions are composed from right to left!'
38 | );
39 | }
40 |
41 | throw err;
42 | }
43 |
44 | const callees = fastestCar.callees || [];
45 |
46 | assert.arrayEqual(
47 | callees.slice(0, 3),
48 | ['sortBy', 'last', 'prop'],
49 | 'The answer is incorrect; hint: Hindley-Milner signatures help a lot to reason about composition!'
50 | );
51 |
--------------------------------------------------------------------------------
/solutions/ch08/solution_a.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 A
3 | 使用 `add` 和 `map` 创建一个 functor,使得 functor 里的值增加 1
4 | */
5 | import { Identity, add, map } from '@tarslab/mostly-adequate-exercises';
6 |
7 | // incrF :: Functor f => f Int -> f Int
8 | const incrF = map(add(1));
9 |
10 | // ------------------------------------
11 | // tests
12 | // ------------------------------------
13 |
14 | assert(incrF(Identity.of(2)).$value === 3, 'The function gives incorrect results.');
15 |
16 | assert(add.partially, 'The answer is incorrect; hint: add is currified!');
17 |
18 | assert(map.partially, 'The answer is incorrect; hint: map is currified!');
19 |
--------------------------------------------------------------------------------
/solutions/ch08/solution_b.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 B
3 | 给定下面的 User 对象:
4 |
5 | const user = { id: 2, name: 'Albert', active: true };
6 |
7 | 使用 `safeProp` 和 `head` 找到 user 名字的首字母。
8 | */
9 | import { compose, head, map, safeProp } from '@tarslab/mostly-adequate-exercises';
10 |
11 | // initial :: User -> Maybe String
12 | const initial = compose(map(head), safeProp('name'));
13 |
14 | // ------------------------------------
15 | // tests
16 | // ------------------------------------
17 |
18 | import { albert, Maybe } from '@tarslab/mostly-adequate-exercises';
19 |
20 | if (
21 | !(initial(albert) instanceof Maybe) &&
22 | initial.callees &&
23 | initial.callees[0] === 'safeProp' &&
24 | initial.callees[1] === 'head'
25 | ) {
26 | throw new Error(
27 | 'The function gives incorrect results; hint: look carefully at the signatures of `safeProp` and `head`!'
28 | );
29 | }
30 |
31 | assert(initial(albert).$value === 'A', 'The function gives incorrect results.');
32 |
33 | assert.arrayEqual(
34 | initial.callees || [],
35 | ['safeProp', 'map'],
36 | 'The answer is incorrect; hint: you can compose `safeProp` with `head` in a declarative way'
37 | );
38 |
--------------------------------------------------------------------------------
/solutions/ch08/solution_c.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 C
3 | 给定下面的帮助函数:
4 |
5 | // showWelcome :: User -> String
6 | const showWelcome = compose(concat('Welcome '), prop('name'));
7 |
8 | // checkActive :: User -> Either String User
9 | const checkActive = function checkActive(user) {
10 | return user.active
11 | ? Either.of(user)
12 | : left('Your account is not active');
13 | };
14 |
15 | 写一个函数,使用 `checkActive` 和 `showWelcome` 授予访问权限或返回错误。
16 | */
17 | import { checkActive, compose, map, showWelcome } from '@tarslab/mostly-adequate-exercises';
18 |
19 | // eitherWelcome :: User -> Either String String
20 | const eitherWelcome = compose(map(showWelcome), checkActive);
21 |
22 | // ------------------------------------
23 | // tests
24 | // ------------------------------------
25 |
26 | import { Either, gary, theresa } from '@tarslab/mostly-adequate-exercises';
27 |
28 | if (
29 | !(eitherWelcome(gary) instanceof Either) &&
30 | eitherWelcome.callees &&
31 | eitherWelcome.callees[0] === 'checkActive' &&
32 | eitherWelcome.callees[1] === 'showWelcome'
33 | ) {
34 | throw new Error(
35 | 'The function gives incorrect results; hint: look carefully at the signatures of `checkActive` and `showWelcome`!'
36 | );
37 | }
38 |
39 | assert(eitherWelcome(gary).$value === 'Your account is not active', 'The function gives incorrect results.');
40 |
41 | assert(eitherWelcome(theresa).$value === 'Welcome Theresa', 'The function gives incorrect results.');
42 |
43 | assert.arrayEqual(
44 | eitherWelcome.callees || [],
45 | ['checkActive', 'map'],
46 | 'The answer is incorrect; hint: you can compose `checkActive` with `showWelcome` in a declarative way!'
47 | );
48 |
--------------------------------------------------------------------------------
/solutions/ch08/solution_d.js:
--------------------------------------------------------------------------------
1 | /*
2 | 练习 D
3 |
4 | 给定下面的帮助函数:
5 |
6 | // validateUser :: (User -> Either String ()) -> User -> Either String User
7 | const validateUser = curry((validate, user) => validate(user).map(_ => user));
8 |
9 | // save :: User -> IO User
10 | const save = user => new IO(() => ({ ...user, saved: true }));
11 |
12 | // showWelcome :: User -> String
13 | const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
14 |
15 | 编写一个 `validateName` 函数,检查用户的名字是否超过3个字符,或者返回一个错误消息。
16 | 然后使用 `either`,`showWelcome` 和 `save` 来编写一个 `register` 函数,当验证通过时注册并欢迎用户。
17 |
18 | 记住,either的两个参数必须返回相同的类型。
19 | */
20 | import { Either, IO, compose, either, left, map, save, showWelcome, validateUser } from '@tarslab/mostly-adequate-exercises';
21 |
22 | // validateName :: User -> Either String ()
23 | const validateName = ({ name }) =>
24 | name.length > 3 ? Either.of(null) : left('Your name need to be > 3');
25 |
26 | const saveAndWelcome = compose(map(showWelcome), save);
27 |
28 | // register :: User -> IO String
29 | const register = compose(
30 | either(IO.of, saveAndWelcome),
31 | validateUser(validateName)
32 | );
33 |
34 | // ------------------------------------
35 | // tests
36 | // ------------------------------------
37 |
38 | import { albert, gary, yi } from '@tarslab/mostly-adequate-exercises';
39 |
40 | const validateGary = validateName(gary);
41 | assert(
42 | validateGary instanceof Either && validateGary.isRight,
43 | 'The function `validateName` gives incorrect results.'
44 | );
45 |
46 | const validateYi = validateName(yi);
47 | assert(
48 | validateYi instanceof Either &&
49 | validateYi.isLeft &&
50 | typeof validateYi.$value === 'string',
51 | 'The function `validateName` gives incorrect results!'
52 | );
53 |
54 | const registerAlbert = register(albert);
55 | assert(
56 | registerAlbert instanceof IO,
57 | "The right outcome to `register` is incorrect; hint: `save` returns an `IO` and you'll need `map` to manipulate the inner value!"
58 | );
59 |
60 | const msgAlbert = registerAlbert.unsafePerformIO();
61 | assert(
62 | typeof msgAlbert === 'string',
63 | 'The right outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!'
64 | );
65 |
66 | const callees = register.callees || [];
67 |
68 | assert(
69 | callees[callees.length - 1] === 'either',
70 | 'The function `register` seems incorrect; hint: you can use `either` to branch an `Either` to different outcomes!'
71 | );
72 |
73 | assert(
74 | msgAlbert === showWelcome(albert),
75 | 'The function `register` returns a correct type, but the inner value is incorrect! Did you use `showWelcome`?'
76 | );
77 |
78 | const registerYi = register(yi);
79 | assert(
80 | registerYi instanceof IO,
81 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO` in every scenarios!'
82 | );
83 |
84 | const msgYi = registerYi.unsafePerformIO();
85 | assert(
86 | typeof msgYi === 'string',
87 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!'
88 | );
89 |
--------------------------------------------------------------------------------
/support/README.md:
--------------------------------------------------------------------------------
1 | # Mostly Adequate Guide to Functional Programming - Exercises Support
2 |
3 | ## Overview
4 |
5 | This package contains all functions and data-structure referenced in the
6 | appendixes of the [Mostly Adequate Guide to Functional Programming](https://github.com/MostlyAdequate/mostly-adequate-guide).
7 |
8 | These functions have an educational purpose and aren't intended to be used in
9 | any production environment. They are however, a good learning material for anyone
10 | interested in functional programming.
11 |
12 | ## How to install
13 |
14 | The package is available on `npm` and can be installed via the following incantation:
15 |
16 | ```bash
17 | npm install @tarslab/mostly-adequate-exercises
18 | ```
19 |
20 | ## How to use
21 |
22 | There's no particular structure to the module, everything is flat and exported
23 | from the root (the curious reader may have a quick glance at the `index.js` to
24 | get convinced about this).
25 |
26 | Also, all top-level functions are curried so you don't have to worry about calling
27 | `curry` on any of them.
28 |
29 | For example:
30 |
31 | ```js
32 | const { Maybe, liftA2, append, concat, reverse } = require('@tarslab/mostly-adequate-exercises');
33 |
34 | const a = Maybe.of("yltsoM").map(reverse);
35 | const b = Maybe.of("Adequate").map(concat(" "));
36 |
37 | liftA2(append)(b)(a);
38 | // Just("Mostly Adequate")
39 | ```
40 |
41 | ## 中文说明
42 |
43 | 这个项目来自[MostlyAdequate/mostly-adequate-guide](https://github.com/MostlyAdequate/mostly-adequate-guide)的部分源码。
44 |
45 | 拷贝原项目的`exercises/support.js`, 重新命名为`index.js`。相对于`@mostly-adequate/support`这个包,增加了练习题的测试辅助和一些测试用例,在做练习题时候,运行代码可以得到更友好的提示。
46 |
--------------------------------------------------------------------------------
/support/index.js:
--------------------------------------------------------------------------------
1 | // NOTE We keep named function here to leverage this in the `compose` function,
2 | // and later on in the validations scripts.
3 |
4 | /* eslint-disable prefer-arrow-callback */
5 |
6 |
7 | /* ---------- Internals ---------- */
8 |
9 | function namedAs(value, fn) {
10 | Object.defineProperty(fn, 'name', { value });
11 | return fn;
12 | }
13 |
14 |
15 | // NOTE This file is loaded by gitbook's exercises plugin. When it does, there's an
16 | // `assert` function available in the global scope.
17 |
18 | /* eslint-disable no-undef, global-require */
19 | if (typeof assert !== 'function' && typeof require === 'function') {
20 | global.assert = require('assert');
21 | }
22 |
23 | assert.arrayEqual = function assertArrayEqual(actual, expected, message = 'arrayEqual') {
24 | if (actual.length !== expected.length) {
25 | throw new Error(message);
26 | }
27 |
28 | for (let i = 0; i < expected.length; i += 1) {
29 | if (expected[i] !== actual[i]) {
30 | throw new Error(message);
31 | }
32 | }
33 | };
34 | /* eslint-enable no-undef, global-require */
35 |
36 |
37 | function inspect(x) {
38 | if (x && typeof x.inspect === 'function') {
39 | return x.inspect();
40 | }
41 |
42 | function inspectFn(f) {
43 | return f.name ? f.name : f.toString();
44 | }
45 |
46 | function inspectTerm(t) {
47 | switch (typeof t) {
48 | case 'string':
49 | return `'${t}'`;
50 | case 'object': {
51 | const ts = Object.keys(t).map(k => [k, inspect(t[k])]);
52 | return `{${ts.map(kv => kv.join(': ')).join(', ')}}`;
53 | }
54 | default:
55 | return String(t);
56 | }
57 | }
58 |
59 | function inspectArgs(args) {
60 | return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args);
61 | }
62 |
63 | return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x);
64 | }
65 |
66 |
67 | /* eslint-disable no-param-reassign */
68 | function withSpyOn(prop, obj, fn) {
69 | const orig = obj[prop];
70 | let called = false;
71 | obj[prop] = function spy(...args) {
72 | called = true;
73 | return orig.call(this, ...args);
74 | };
75 | fn();
76 | obj[prop] = orig;
77 | return called;
78 | }
79 | /* eslint-enable no-param-reassign */
80 |
81 |
82 | const typeMismatch = (src, got, fn) => `Type Mismatch in function '${fn}'
83 |
84 | ${fn} :: ${got}
85 |
86 | instead of
87 |
88 | ${fn} :: ${src}`;
89 |
90 |
91 | const capitalize = s => `${s[0].toUpperCase()}${s.substring(1)}`;
92 |
93 |
94 | const ordinal = (i) => {
95 | switch (i) {
96 | case 1:
97 | return '1st';
98 | case 2:
99 | return '2nd';
100 | case 3:
101 | return '3rd';
102 | default:
103 | return `${i}th`; // NOTE won't get any much bigger ...
104 | }
105 | };
106 |
107 | const getType = (x) => {
108 | if (x === null) {
109 | return 'Null';
110 | }
111 |
112 | if (typeof x === 'undefined') {
113 | return '()';
114 | }
115 |
116 | if (Array.isArray(x)) {
117 | return `[${x[0] ? getType(x[0]) : '?'}]`;
118 | }
119 |
120 | if (typeof x.getType === 'function') {
121 | return x.getType();
122 | }
123 |
124 | if (x.constructor && x.constructor.name) {
125 | return x.constructor.name;
126 | }
127 |
128 | return capitalize(typeof x);
129 | };
130 |
131 |
132 | /* ---------- Essential FP Functions ---------- */
133 |
134 | // NOTE A slightly pumped up version of `curry` which also keeps track of
135 | // whether a function was called partially or with all its arguments at once.
136 | // This is useful to provide insights during validation of exercises.
137 | function curry(fn) {
138 | assert(
139 | typeof fn === 'function',
140 | typeMismatch('function -> ?', [getType(fn), '?'].join(' -> '), 'curry'),
141 | );
142 |
143 | const arity = fn.length;
144 |
145 | return namedAs(fn.name, function $curry(...args) {
146 | $curry.partially = this && this.partially;
147 |
148 | if (args.length < arity) {
149 | return namedAs(fn.name, $curry.bind({ partially: true }, ...args));
150 | }
151 |
152 | return fn.call(this || { partially: false }, ...args);
153 | });
154 | }
155 |
156 |
157 | // NOTE A slightly pumped up version of `compose` which also keeps track of the chain
158 | // of callees. In the end, a function created with `compose` holds a `callees` variable
159 | // with the list of all the callees' names.
160 | // This is useful to provide insights during validation of exercises
161 | function compose(...fns) {
162 | const n = fns.length;
163 |
164 | return function $compose(...args) {
165 | $compose.callees = [];
166 |
167 | let $args = args;
168 |
169 | for (let i = n - 1; i >= 0; i -= 1) {
170 | const fn = fns[i];
171 |
172 | assert(
173 | typeof fn === 'function',
174 | `Invalid Composition: ${ordinal(n - i)} element in a composition isn't a function`,
175 | );
176 |
177 | $compose.callees.push(fn.name);
178 | $args = [fn.call(null, ...$args)];
179 | }
180 |
181 | return $args[0];
182 | };
183 | }
184 |
185 |
186 | /* ---------- Algebraic Data Structures ---------- */
187 |
188 | class Either {
189 | static of(x) {
190 | return new Right(x); // eslint-disable-line no-use-before-define
191 | }
192 |
193 | constructor(x) {
194 | this.$value = x;
195 | }
196 | }
197 |
198 |
199 | class Left extends Either {
200 | get isLeft() { // eslint-disable-line class-methods-use-this
201 | return true;
202 | }
203 |
204 | get isRight() { // eslint-disable-line class-methods-use-this
205 | return false;
206 | }
207 |
208 | ap() {
209 | return this;
210 | }
211 |
212 | chain() {
213 | return this;
214 | }
215 |
216 | inspect() {
217 | return `Left(${inspect(this.$value)})`;
218 | }
219 |
220 | getType() {
221 | return `(Either ${getType(this.$value)} ?)`;
222 | }
223 |
224 | join() {
225 | return this;
226 | }
227 |
228 | map() {
229 | return this;
230 | }
231 |
232 | sequence(of) {
233 | return of(this);
234 | }
235 |
236 | traverse(of, fn) {
237 | return of(this);
238 | }
239 | }
240 |
241 |
242 | class Right extends Either {
243 | get isLeft() { // eslint-disable-line class-methods-use-this
244 | return false;
245 | }
246 |
247 | get isRight() { // eslint-disable-line class-methods-use-this
248 | return true;
249 | }
250 |
251 | ap(f) {
252 | return f.map(this.$value);
253 | }
254 |
255 | chain(fn) {
256 | return fn(this.$value);
257 | }
258 |
259 | inspect() {
260 | return `Right(${inspect(this.$value)})`;
261 | }
262 |
263 | getType() {
264 | return `(Either ? ${getType(this.$value)})`;
265 | }
266 |
267 | join() {
268 | return this.$value;
269 | }
270 |
271 | map(fn) {
272 | return Either.of(fn(this.$value));
273 | }
274 |
275 | sequence(of) {
276 | return this.traverse(of, x => x);
277 | }
278 |
279 | traverse(of, fn) {
280 | fn(this.$value).map(Either.of);
281 | }
282 | }
283 |
284 |
285 | class Identity {
286 | static of(x) {
287 | return new Identity(x);
288 | }
289 |
290 | constructor(x) {
291 | this.$value = x;
292 | }
293 |
294 | ap(f) {
295 | return f.map(this.$value);
296 | }
297 |
298 | chain(fn) {
299 | return this.map(fn).join();
300 | }
301 |
302 | inspect() {
303 | return `Identity(${inspect(this.$value)})`;
304 | }
305 |
306 | getType() {
307 | return `(Identity ${getType(this.$value)})`;
308 | }
309 |
310 | join() {
311 | return this.$value;
312 | }
313 |
314 | map(fn) {
315 | return Identity.of(fn(this.$value));
316 | }
317 |
318 | sequence(of) {
319 | return this.traverse(of, x => x);
320 | }
321 |
322 | traverse(of, fn) {
323 | return fn(this.$value).map(Identity.of);
324 | }
325 | }
326 |
327 | class IO {
328 | static of(x) {
329 | return new IO(() => x);
330 | }
331 |
332 | constructor(io) {
333 | assert(
334 | typeof io === 'function',
335 | 'invalid `io` operation given to IO constructor. Use `IO.of` if you want to lift a value in a default minimal IO context.',
336 | );
337 |
338 | this.unsafePerformIO = io;
339 | }
340 |
341 | ap(f) {
342 | return this.chain(fn => f.map(fn));
343 | }
344 |
345 | chain(fn) {
346 | return this.map(fn).join();
347 | }
348 |
349 | inspect() {
350 | return `IO(${inspect(this.unsafePerformIO())})`;
351 | }
352 |
353 | getType() {
354 | return `(IO ${getType(this.unsafePerformIO())})`;
355 | }
356 |
357 |
358 | join() {
359 | return this.unsafePerformIO();
360 | }
361 |
362 | map(fn) {
363 | return new IO(compose(fn, this.unsafePerformIO));
364 | }
365 | }
366 |
367 |
368 | class Map {
369 | constructor(x) {
370 | assert(
371 | typeof x === 'object' && x !== null,
372 | 'tried to create `Map` with non object-like',
373 | );
374 |
375 | this.$value = x;
376 | }
377 |
378 | inspect() {
379 | return `Map(${inspect(this.$value)})`;
380 | }
381 |
382 | getType() {
383 | const sample = this.$value[Object.keys(this.$value)[0]];
384 |
385 | return `(Map String ${sample ? getType(sample) : '?'})`;
386 | }
387 |
388 | insert(k, v) {
389 | const singleton = {};
390 | singleton[k] = v;
391 | return new Map(Object.assign({}, this.$value, singleton));
392 | }
393 |
394 | reduce(fn, zero) {
395 | return this.reduceWithKeys((acc, _, k) => fn(acc, k), zero);
396 | }
397 |
398 | reduceWithKeys(fn, zero) {
399 | return Object.keys(this.$value)
400 | .reduce((acc, k) => fn(acc, this.$value[k], k), zero);
401 | }
402 |
403 | map(fn) {
404 | return new Map(this.reduceWithKeys((obj, v, k) => {
405 | obj[k] = fn(v); // eslint-disable-line no-param-reassign
406 | return obj;
407 | }, {}));
408 | }
409 |
410 | sequence(of) {
411 | return this.traverse(of, x => x);
412 | }
413 |
414 | traverse(of, fn) {
415 | return this.reduceWithKeys(
416 | (f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f),
417 | of(new Map({})),
418 | );
419 | }
420 | }
421 |
422 |
423 | class List {
424 | static of(x) {
425 | return new List([x]);
426 | }
427 |
428 | constructor(xs) {
429 | assert(
430 | Array.isArray(xs),
431 | 'tried to create `List` from non-array',
432 | );
433 |
434 | this.$value = xs;
435 | }
436 |
437 | concat(x) {
438 | return new List(this.$value.concat(x));
439 | }
440 |
441 | inspect() {
442 | return `List(${inspect(this.$value)})`;
443 | }
444 |
445 | getType() {
446 | const sample = this.$value[0];
447 |
448 | return `(List ${sample ? getType(sample) : '?'})`;
449 | }
450 |
451 | map(fn) {
452 | return new List(this.$value.map(fn));
453 | }
454 |
455 | sequence(of) {
456 | return this.traverse(of, x => x);
457 | }
458 |
459 | traverse(of, fn) {
460 | return this.$value.reduce(
461 | (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
462 | of(new List([])),
463 | );
464 | }
465 | }
466 |
467 |
468 | class Maybe {
469 | static of(x) {
470 | return new Maybe(x);
471 | }
472 |
473 | get isNothing() {
474 | return this.$value === null || this.$value === undefined;
475 | }
476 |
477 | get isJust() {
478 | return !this.isNothing;
479 | }
480 |
481 | constructor(x) {
482 | this.$value = x;
483 | }
484 |
485 | ap(f) {
486 | return this.isNothing ? this : f.map(this.$value);
487 | }
488 |
489 | chain(fn) {
490 | return this.map(fn).join();
491 | }
492 |
493 | inspect() {
494 | return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
495 | }
496 |
497 | getType() {
498 | return `(Maybe ${this.isJust ? getType(this.$value) : '?'})`;
499 | }
500 |
501 | join() {
502 | return this.isNothing ? this : this.$value;
503 | }
504 |
505 | map(fn) {
506 | return this.isNothing ? this : Maybe.of(fn(this.$value));
507 | }
508 |
509 | sequence(of) {
510 | return this.traverse(of, x => x);
511 | }
512 |
513 | traverse(of, fn) {
514 | return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of);
515 | }
516 | }
517 |
518 |
519 | class Task {
520 | constructor(fork) {
521 | assert(
522 | typeof fork === 'function',
523 | 'invalid `fork` operation given to Task constructor. Use `Task.of` if you want to lift a value in a default minimal Task context.',
524 | );
525 |
526 | this.fork = fork;
527 | }
528 |
529 | static of(x) {
530 | return new Task((_, resolve) => resolve(x));
531 | }
532 |
533 | static rejected(x) {
534 | return new Task((reject, _) => reject(x));
535 | }
536 |
537 | ap(f) {
538 | return this.chain(fn => f.map(fn));
539 | }
540 |
541 | chain(fn) {
542 | return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve)));
543 | }
544 |
545 | inspect() { // eslint-disable-line class-methods-use-this
546 | return 'Task(?)';
547 | }
548 |
549 | getType() { // eslint-disable-line class-methods-use-this
550 | return '(Task ? ?)';
551 | }
552 |
553 | join() {
554 | return this.chain(x => x);
555 | }
556 |
557 | map(fn) {
558 | return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn)));
559 | }
560 | }
561 |
562 | // In nodejs the existance of a class method named `inspect` will trigger a deprecation warning
563 | // when passing an instance to `console.log`:
564 | // `(node:3845) [DEP0079] DeprecationWarning: Custom inspection function on Objects via .inspect() is deprecated`
565 | // The solution is to alias the existing inspect method with the special inspect symbol exported by node
566 | if (typeof module !== 'undefined' && typeof this !== 'undefined' && this.module !== module) {
567 | const customInspect = require('util').inspect.custom;
568 | const assignCustomInspect = it => it.prototype[customInspect] = it.prototype.inspect;
569 | [Left, Right, Identity, IO, Map, List, Maybe, Task].forEach(assignCustomInspect);
570 | }
571 |
572 | const identity = function identity(x) { return x; };
573 |
574 | const either = curry(function either(f, g, e) {
575 | if (e.isLeft) {
576 | return f(e.$value);
577 | }
578 |
579 | return g(e.$value);
580 | });
581 |
582 | const left = function left(x) { return new Left(x); };
583 |
584 | const maybe = curry(function maybe(v, f, m) {
585 | if (m.isNothing) {
586 | return v;
587 | }
588 |
589 | return f(m.$value);
590 | });
591 |
592 | const nothing = Maybe.of(null);
593 |
594 | const reject = function reject(x) { return Task.rejected(x); };
595 |
596 | const chain = curry(function chain(fn, m) {
597 | assert(
598 | typeof fn === 'function' && typeof m.chain === 'function',
599 | typeMismatch('Monad m => (a -> m b) -> m a -> m a', [getType(fn), getType(m), 'm a'].join(' -> '), 'chain'),
600 | );
601 |
602 | return m.chain(fn);
603 | });
604 |
605 | const join = function join(m) {
606 | assert(
607 | typeof m.chain === 'function',
608 | typeMismatch('Monad m => m (m a) -> m a', [getType(m), 'm a'].join(' -> '), 'join'),
609 | );
610 |
611 | return m.join();
612 | };
613 |
614 | const map = curry(function map(fn, f) {
615 | assert(
616 | typeof fn === 'function' && typeof f.map === 'function',
617 | typeMismatch('Functor f => (a -> b) -> f a -> f b', [getType(fn), getType(f), 'f b'].join(' -> '), 'map'),
618 | );
619 |
620 | return f.map(fn);
621 | });
622 |
623 | const sequence = curry(function sequence(of, x) {
624 | assert(
625 | typeof of === 'function' && typeof x.sequence === 'function',
626 | typeMismatch('(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'),
627 | );
628 |
629 | return x.sequence(of);
630 | });
631 |
632 | const traverse = curry(function traverse(of, fn, x) {
633 | assert(
634 | typeof of === 'function' && typeof fn === 'function' && typeof x.traverse === 'function',
635 | typeMismatch(
636 | '(Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b)',
637 | [getType(of), getType(fn), getType(x), 'f (t b)'].join(' -> '),
638 | 'traverse',
639 | ),
640 | );
641 |
642 | return x.traverse(of, fn);
643 | });
644 |
645 | const unsafePerformIO = function unsafePerformIO(io) {
646 | assert(
647 | io instanceof IO,
648 | typeMismatch('IO a', getType(io), 'unsafePerformIO'),
649 | );
650 |
651 | return io.unsafePerformIO();
652 | };
653 |
654 | const liftA2 = curry(function liftA2(fn, a1, a2) {
655 | assert(
656 | typeof fn === 'function'
657 | && typeof a1.map === 'function'
658 | && typeof a2.ap === 'function',
659 | typeMismatch('Applicative f => (a -> b -> c) -> f a -> f b -> f c', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
660 | );
661 |
662 | return a1.map(fn).ap(a2);
663 | });
664 |
665 | const liftA3 = curry(function liftA3(fn, a1, a2, a3) {
666 | assert(
667 | typeof fn === 'function'
668 | && typeof a1.map === 'function'
669 | && typeof a2.ap === 'function'
670 | && typeof a3.ap === 'function',
671 | typeMismatch('Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
672 | );
673 |
674 | return a1.map(fn).ap(a2).ap(a3);
675 | });
676 |
677 | const always = curry(function always(a, b) { return a; });
678 |
679 |
680 | /* ---------- Pointfree Classic Utilities ---------- */
681 |
682 | const append = curry(function append(a, b) {
683 | assert(
684 | typeof a === 'string' && typeof b === 'string',
685 | typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
686 | );
687 |
688 | return b.concat(a);
689 | });
690 |
691 | const add = curry(function add(a, b) {
692 | assert(
693 | typeof a === 'number' && typeof b === 'number',
694 | typeMismatch('Number -> Number -> Number', [getType(a), getType(b), 'Number'].join(' -> '), 'add'),
695 | );
696 |
697 | return a + b;
698 | });
699 |
700 | const concat = curry(function concat(a, b) {
701 | assert(
702 | typeof a === 'string' && typeof b === 'string',
703 | typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
704 | );
705 |
706 | return a.concat(b);
707 | });
708 |
709 | const eq = curry(function eq(a, b) {
710 | assert(
711 | getType(a) === getType(b),
712 | typeMismatch('a -> a -> Boolean', [getType(a), getType(b), 'Boolean'].join(' -> '), eq),
713 | );
714 |
715 | return a === b;
716 | });
717 |
718 | const filter = curry(function filter(fn, xs) {
719 | assert(
720 | typeof fn === 'function' && Array.isArray(xs),
721 | typeMismatch('(a -> Boolean) -> [a] -> [a]', [getType(fn), getType(xs), getType(xs)].join(' -> '), 'filter'),
722 | );
723 |
724 | return xs.filter(fn);
725 | });
726 |
727 | const flip = curry(function flip(fn, a, b) {
728 | assert(
729 | typeof fn === 'function',
730 | typeMismatch('(a -> b) -> (b -> a)', [getType(fn), '(b -> a)'].join(' -> '), 'flip'),
731 | );
732 |
733 | return fn(b, a);
734 | });
735 |
736 | const forEach = curry(function forEach(fn, xs) {
737 | assert(
738 | typeof fn === 'function' && Array.isArray(xs),
739 | typeMismatch('(a -> ()) -> [a] -> ()', [getType(fn), getType(xs), '()'].join(' -> '), 'forEach'),
740 | );
741 |
742 | xs.forEach(fn);
743 | });
744 |
745 | const intercalate = curry(function intercalate(str, xs) {
746 | assert(
747 | typeof str === 'string' && Array.isArray(xs) && (xs.length === 0 || typeof xs[0] === 'string'),
748 | typeMismatch('String -> [String] -> String', [getType(str), getType(xs), 'String'].join(' -> '), 'intercalate'),
749 | );
750 |
751 | return xs.join(str);
752 | });
753 |
754 | const head = function head(xs) {
755 | assert(
756 | Array.isArray(xs) || typeof xs === 'string',
757 | typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'head'),
758 | );
759 |
760 | return xs[0];
761 | };
762 |
763 | const last = function last(xs) {
764 | assert(
765 | Array.isArray(xs) || typeof xs === 'string',
766 | typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'last'),
767 | );
768 |
769 | return xs[xs.length - 1];
770 | };
771 |
772 | const match = curry(function match(re, str) {
773 | assert(
774 | re instanceof RegExp && typeof str === 'string',
775 | typeMismatch('RegExp -> String -> Boolean', [getType(re), getType(str), 'Boolean'].join(' -> '), 'match'),
776 | );
777 |
778 | return re.test(str);
779 | });
780 |
781 | const prop = curry(function prop(p, obj) {
782 | assert(
783 | typeof p === 'string' && typeof obj === 'object' && obj !== null,
784 | typeMismatch('String -> Object -> a', [getType(p), getType(obj), 'a'].join(' -> '), 'prop'),
785 | );
786 |
787 | return obj[p];
788 | });
789 |
790 | const reduce = curry(function reduce(fn, zero, xs) {
791 | assert(
792 | typeof fn === 'function' && Array.isArray(xs),
793 | typeMismatch('(b -> a -> b) -> b -> [a] -> b', [getType(fn), getType(zero), getType(xs), 'b'].join(' -> '), 'reduce'),
794 | );
795 |
796 | return xs.reduce(
797 | function $reduceIterator($acc, $x) { return fn($acc, $x); },
798 | zero,
799 | );
800 | });
801 |
802 | const safeHead = namedAs('safeHead', compose(Maybe.of, head));
803 |
804 | const safeProp = curry(function safeProp(p, obj) { return Maybe.of(prop(p, obj)); });
805 |
806 | const sortBy = curry(function sortBy(fn, xs) {
807 | assert(
808 | typeof fn === 'function' && Array.isArray(xs),
809 | typeMismatch('Ord b => (a -> b) -> [a] -> [a]', [getType(fn), getType(xs), '[a]'].join(' -> '), 'sortBy'),
810 | );
811 |
812 | return xs.sort((a, b) => {
813 | if (fn(a) === fn(b)) {
814 | return 0;
815 | }
816 |
817 | return fn(a) > fn(b) ? 1 : -1;
818 | });
819 | });
820 |
821 | const split = curry(function split(s, str) {
822 | assert(
823 | typeof s === 'string' && typeof str === 'string',
824 | typeMismatch('String -> String -> [String]', [getType(s), getType(str), '[String]'].join(' -> '), 'split'),
825 | );
826 |
827 | return str.split(s);
828 | });
829 |
830 | const take = curry(function take(n, xs) {
831 | assert(
832 | typeof n === 'number' && (Array.isArray(xs) || typeof xs === 'string'),
833 | typeMismatch('Number -> [a] -> [a]', [getType(n), getType(xs), getType(xs)].join(' -> '), 'take'),
834 | );
835 |
836 | return xs.slice(0, n);
837 | });
838 |
839 | const toLowerCase = function toLowerCase(s) {
840 | assert(
841 | typeof s === 'string',
842 | typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
843 | );
844 |
845 | return s.toLowerCase();
846 | };
847 |
848 | const toUpperCase = function toUpperCase(s) {
849 | assert(
850 | typeof s === 'string',
851 | typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
852 | );
853 |
854 | return s.toUpperCase();
855 | };
856 |
857 |
858 | /* ---------- Chapter 4 ---------- */
859 |
860 | const keepHighest = function keepHighest(x, y) {
861 | try {
862 | keepHighest.calledBy = keepHighest.caller;
863 | } catch (err) {
864 | // NOTE node.js runs in strict mode and prohibit the usage of '.caller'
865 | // There's a ugly hack to retrieve the caller from stack trace.
866 | const [, caller] = /at (\S+)/.exec(err.stack.split('\n')[2]);
867 |
868 | keepHighest.calledBy = namedAs(caller, () => {});
869 | }
870 |
871 | return x >= y ? x : y;
872 | };
873 |
874 |
875 | /* ---------- Chapter 5 ---------- */
876 |
877 | const cars = [{
878 | name: 'Ferrari FF',
879 | horsepower: 660,
880 | dollar_value: 700000,
881 | in_stock: true,
882 | }, {
883 | name: 'Spyker C12 Zagato',
884 | horsepower: 650,
885 | dollar_value: 648000,
886 | in_stock: false,
887 | }, {
888 | name: 'Jaguar XKR-S',
889 | horsepower: 550,
890 | dollar_value: 132000,
891 | in_stock: true,
892 | }, {
893 | name: 'Audi R8',
894 | horsepower: 525,
895 | dollar_value: 114200,
896 | in_stock: false,
897 | }, {
898 | name: 'Aston Martin One-77',
899 | horsepower: 750,
900 | dollar_value: 1850000,
901 | in_stock: true,
902 | }, {
903 | name: 'Pagani Huayra',
904 | horsepower: 700,
905 | dollar_value: 1300000,
906 | in_stock: false,
907 | }];
908 |
909 | const average = function average(xs) {
910 | return xs.reduce(add, 0) / xs.length;
911 | };
912 |
913 |
914 | /* ---------- Chapter 8 ---------- */
915 |
916 | const albert = {
917 | id: 1,
918 | active: true,
919 | name: 'Albert',
920 | address: {
921 | street: {
922 | number: 22,
923 | name: 'Walnut St',
924 | },
925 | },
926 | };
927 |
928 | const gary = {
929 | id: 2,
930 | active: false,
931 | name: 'Gary',
932 | address: {
933 | street: {
934 | number: 14,
935 | },
936 | },
937 | };
938 |
939 | const theresa = {
940 | id: 3,
941 | active: true,
942 | name: 'Theresa',
943 | };
944 |
945 | const yi = { id: 4, name: 'Yi', active: true };
946 |
947 | const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
948 |
949 | const checkActive = function checkActive(user) {
950 | return user.active
951 | ? Either.of(user)
952 | : left('Your account is not active');
953 | };
954 |
955 | const save = function save(user) {
956 | return new IO(() => Object.assign({}, user, { saved: true }));
957 | };
958 |
959 | const validateUser = curry(function validateUser(validate, user) {
960 | return validate(user).map(_ => user); // eslint-disable-line no-unused-vars
961 | });
962 |
963 |
964 | /* ---------- Chapter 9 ---------- */
965 |
966 | const getFile = IO.of('/home/mostly-adequate/ch09.md');
967 |
968 | const pureLog = function pureLog(str) { return new IO(() => { console.log(str); return str; }); };
969 |
970 | const addToMailingList = function addToMailingList(email) { return IO.of([email]); };
971 |
972 | const emailBlast = function emailBlast(list) { return IO.of(list.join(',')); };
973 |
974 | const validateEmail = function validateEmail(x) {
975 | return /\S+@\S+\.\S+/.test(x)
976 | ? Either.of(x)
977 | : left('invalid email');
978 | };
979 |
980 |
981 | /* ---------- Chapter 10 ---------- */
982 |
983 | const localStorage = { player1: albert, player2: theresa };
984 |
985 | const game = curry(function game(p1, p2) { return `${p1.name} vs ${p2.name}`; });
986 |
987 | const getFromCache = function getFromCache(x) { return new IO(() => localStorage[x]); };
988 |
989 |
990 | /* ---------- Chapter 11 ---------- */
991 |
992 | const findUserById = function findUserById(id) {
993 | switch (id) {
994 | case 1:
995 | return Task.of(Either.of(albert));
996 |
997 | case 2:
998 | return Task.of(Either.of(gary));
999 |
1000 | case 3:
1001 | return Task.of(Either.of(theresa));
1002 |
1003 | default:
1004 | return Task.of(left('not found'));
1005 | }
1006 | };
1007 |
1008 | const eitherToTask = namedAs('eitherToTask', either(Task.rejected, Task.of));
1009 |
1010 |
1011 | /* ---------- Chapter 12 ---------- */
1012 |
1013 | const httpGet = function httpGet(route) { return Task.of(`json for ${route}`); };
1014 |
1015 | const routes = new Map({
1016 | '/': '/',
1017 | '/about': '/about',
1018 | });
1019 |
1020 | const validate = function validate(player) {
1021 | return player.name
1022 | ? Either.of(player)
1023 | : left('must have name');
1024 | };
1025 |
1026 | const readdir = function readdir(dir) {
1027 | return Task.of(['file1', 'file2', 'file3']);
1028 | };
1029 |
1030 | const readfile = curry(function readfile(encoding, file) {
1031 | return Task.of(`content of ${file} (${encoding})`);
1032 | });
1033 |
1034 |
1035 | /* ---------- Exports ---------- */
1036 |
1037 | if (typeof module === 'object') {
1038 | module.exports = {
1039 | // Utils
1040 | withSpyOn,
1041 |
1042 | // Essential FP helpers
1043 | always,
1044 | compose,
1045 | curry,
1046 | either,
1047 | identity,
1048 | inspect,
1049 | left,
1050 | liftA2,
1051 | liftA3,
1052 | maybe,
1053 | nothing,
1054 | reject,
1055 |
1056 | // Algebraic Data Structures
1057 | Either,
1058 | IO,
1059 | Identity,
1060 | Left,
1061 | List,
1062 | Map,
1063 | Maybe,
1064 | Right,
1065 | Task,
1066 |
1067 | // Currified version of 'standard' functions
1068 | append,
1069 | add,
1070 | chain,
1071 | concat,
1072 | eq,
1073 | filter,
1074 | flip,
1075 | forEach,
1076 | head,
1077 | intercalate,
1078 | join,
1079 | last,
1080 | map,
1081 | match,
1082 | prop,
1083 | reduce,
1084 | safeHead,
1085 | safeProp,
1086 | sequence,
1087 | sortBy,
1088 | split,
1089 | take,
1090 | toLowerCase,
1091 | toUpperCase,
1092 | traverse,
1093 | unsafePerformIO,
1094 |
1095 | // Chapter 04
1096 | keepHighest,
1097 |
1098 | // Chapter 05
1099 | cars,
1100 | average,
1101 |
1102 | // Chapter 08
1103 | albert,
1104 | gary,
1105 | theresa,
1106 | yi,
1107 | showWelcome,
1108 | checkActive,
1109 | save,
1110 | validateUser,
1111 |
1112 | // Chapter 09
1113 | getFile,
1114 | pureLog,
1115 | addToMailingList,
1116 | emailBlast,
1117 | validateEmail,
1118 |
1119 | // Chapter 10
1120 | localStorage,
1121 | getFromCache,
1122 | game,
1123 |
1124 | // Chapter 11
1125 | findUserById,
1126 | eitherToTask,
1127 |
1128 | // Chapter 12
1129 | httpGet,
1130 | routes,
1131 | validate,
1132 | readdir,
1133 | readfile,
1134 | };
1135 | }
1136 |
--------------------------------------------------------------------------------
/support/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tarslab/mostly-adequate-exercises",
3 | "version": "1.0.0",
4 | "description": "Support functions and data-structures from the Mostly Adequate Guide to Functional Programming, only fro exercises.",
5 | "license": "MIT",
6 | "main": "index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide"
10 | },
11 | "author": "@mostly-adequate",
12 | "bugs": {
13 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide/issues"
14 | },
15 | "homepage": "https://github.com/MostlyAdequate/mostly-adequate-guide",
16 | "keywords": [
17 | "functional programming",
18 | "mostly adequate",
19 | "guide",
20 | "fp",
21 | "exercises"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/tarslab.md:
--------------------------------------------------------------------------------
1 | ---
2 | marp: true
3 | theme: uncover
4 | style: |
5 | .columns {margin: 0 auto; width: 30%; }
6 | .column {
7 | flex: 1;
8 | margin: 0 0.25rem;
9 | }
10 | @import 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css'
11 | ---
12 |
13 | 函数式编程指南视频讲解
14 |
15 | [ tarslab/mostly-adequate-guide-video-zh](https://github.com/tarslab/mostly-adequate-guide-video-zh)
16 |
17 | 源码穿越
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------