├── .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 | ![代码逐步演示和解读](./images/feature-quokkaJs.gif) 29 | 30 | - 练习易上手,把测试合到一起,实时反馈 31 | 32 | ![把测试合到一起,实时反馈](./images/feature-exercise.gif) 33 | 34 | - 幻灯片划重点,方便复习,[marp插件](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode) 35 | 36 | ![幻灯片划重点,方便复习](./images/feature-marp.gif) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg left fit](images/function-sets.gif) 63 | 64 | ### 从 x 到 y 的函数关系 65 | 66 | $$f(x)=y$$ 67 | 68 | --- 69 | 70 | ![bg left fit](images/relation-not-function.gif) 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 | ![bg right fit](images/fn_graph.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/cover.png) 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 | ![cat_theory](images/cat_theory.png) 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 | ![category composition1](images/cat_comp1.png) 141 | ![category composition2](images/cat_comp2.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/cover.png) 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 | ![bg right fit](images/jar-functor.jpg) 9 | 10 | 第 8 章 11 | 12 | 《函数式编程指南》 13 | 14 | # Functor 15 | 16 | --- 17 | 18 | ## 万能的容器 19 | 20 | ![bg fit right:30%](images/jar.jpg) 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 | ![cat](images/cat.png) 44 | 45 | --- 46 | 47 | ## 用例 48 | 49 | - 发送失败信号 Maybe(null) 50 | - map 遇到 null 会跳过计算 51 | 52 | --- 53 | 54 | ## 释放容器里的值 55 | 56 | - 任何函数都要有一个结束 57 | - Maybe里的不确定的值是魔鬼吗? 58 | - 如果函数没有完成使命,可能是其他代码分支造成的 59 | 60 | --- 61 | 62 | ![bg fit right:30%](images/cat.png) 63 | 64 | > 代码应该像薛定谔的猫一样,同时处于两种状态,并且保持这种状态直到最后一个函数为止。哪怕代码有很多逻辑的分支,也能保证一个直线的顺序流 65 | 66 | --- 67 | 68 | ### Maybe 总结 69 | 70 | - Maybe 能够有效提高函数的安全性 71 | - Maybe 的“真正”实现会把它分为两种类型:
一种是非空值,另一种是空值 72 | - 遵守 map 的 parametricity 特性:
null 和 undefined 都能被 map 调用 73 | 74 | --- 75 | 76 | ## 纯的错误处理 77 | 78 | ![bg fit right:30%](images/fists.jpg) 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 | ![dominoes](images/dominoes.jpg) 112 | 113 | --- 114 | 115 | ### 压栈 116 | 117 | ![bg fit right:30%](images/dominoes.jpg) 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 | ![width:600px](images/catmap.png) 166 | 167 | --- 168 | 169 | ### Functor 映射 170 | 171 | ![width:600px](images/functormap.png) 172 | 173 | --- 174 | 175 | ### 殊路同归 176 | 177 | ![width:600px](images/functormapmaybe.png) 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 | ![bg right fit](images/cover.png) 8 | 9 | 第 1 章 10 | 11 | 《函数式编程指南》 12 | 13 | # 我们在
做什么 14 | 15 | --- 16 | 17 | ![bg right fit](images/cover.png) 18 | 19 | 第 2 章 20 | 21 | 《函数式编程指南》 22 | 23 | # 一等公民
的函数 24 | 25 | --- 26 | 27 | ![bg right fit](images/cover.png) 28 | 29 | 第 3 章 30 | 31 | 《函数式编程指南》 32 | 33 | # 纯函数
的好处 34 | 35 | --- 36 | 37 | ![bg right fit](images/cover.png) 38 | 39 | 第 4 章 40 | 41 | 《函数式编程指南》 42 | 43 | # 柯里化
(curry) 44 | 45 | --- 46 | 47 | ![bg right fit](images/cover.png) 48 | 49 | 第 5 章 50 | 51 | 《函数式编程指南》 52 | 53 | # 函数组合
(compose) 54 | 55 | --- 56 | 57 | ![bg right fit](images/cover.png) 58 | 59 | 第 6 章 60 | 61 | 《函数式编程指南》 62 | 63 | # 示例应用 64 | 65 | --- 66 | 67 | ![bg right fit](images/cover.png) 68 | 69 | 第 7 章 70 | 71 | 《函数式编程指南》 72 | 73 | # Hindley-Milner
类型签名 74 | 75 | --- 76 | 77 | ![bg right fit](images/jar-functor.jpg) 78 | 79 | 第 8 章 80 | 81 | 《函数式编程指南》 82 | 83 | # Functor 84 | 85 | --- 86 | 87 | ![bg right fit](images/cover.png) 88 | 89 | 第 9 章 90 | 91 | 《函数式编程指南》 92 | 93 | # Monad 94 | 95 | --- 96 | 97 | ![bg right fit](images/cover.png) 98 | 99 | 第 10 章 100 | 101 | 《函数式编程指南》 102 | 103 | # Applicative
Functor 104 | 105 | --- 106 | 107 | ![bg right fit](images/cover.png) 108 | 109 | 第 11 章 110 | 111 | 《函数式编程指南》 112 | 113 | # 再转换一次
就很自然 114 | 115 | --- 116 | 117 | ![bg right fit](images/cover.png) 118 | 119 | 第 12 章 120 | 121 | 《函数式编程指南》 122 | 123 | # 遍历 124 | 125 | --- 126 | 127 | ![bg right fit](images/cover.png) 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 | --------------------------------------------------------------------------------