函数式编程小思考4 笔记

JS函数式编程指南(https:/llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch2.html)

现在开始第三遍的阅读

说实话, 我只是在思考函数的时候, 遇到一些疑问,
在我试着整理这些疑问的时, 打开了关于函数式编程相关的东西,

我没有想到会消耗这么长时间,(以为就是读一两篇博客的概念)
或者说, 耗费的时间, 远远超过了,当初的时间预算.
当然其中最大的原因应该在于我的低效率学习方式.

到昨天为止, 我已经读了两遍,
能够把文中的大代码全都读明白了(就是能够读懂,并非是熟悉,或者深入掌握)
犹豫半天, 我是先放在这里, 还是说, 要继续掌握一下.
很明显, 如果在这里停止, 我对函数的理解确实应该潜移默化的增加了理解,
当从另一角度来讲, 不亚于浅尝即止.
而网上有一些说法是,函数式编程在实际的工作当中, 可能也不会用多少.
更多的应该是面向对象的编程.

其实这几天读这个函数式编程指南, 很费劲, 特别是到了最后3章,
代码突然就有点看不懂.
我想还是试着去掌握一下, 不过, 这回, 我们不要把全部时间都投入在这里,
我们把 这个当成一个 长期的任务, 每天抽出一些时间回顾一遍的形式, 可能更好一点.

而且说实话, 这个编程指南提供的是一种,工具, 嗯, 是一种全套解决代码的工具,
有点理解为什么叫编程范式了.
不过实际上, 他没有完全解决我最初的疑问.
我最根本的疑问是, 我始终感觉, 无法完全掌握函数的用法.
形式很简单, 但变化太多样, 效果也太多样.
具体的表现的问题是, 多层函数的效果, 与用法.

但回头回答这个问题之前, 我们先读第三遍, 函数式编程指南.

问题一, 作者是怎么能够快速看出 等价的?

            var hi = function(name) {
                return "Hi " + name;
            };

            var greeting = function(name) {
                return hi(name);
            };

            var greeting1 = hi;

            hi = function(name, age) {
                return "Hi " + name + 'age is ' + age;
            };

            / 太傻了
            var getServerStuff = function(callback) {
                return ajaxCall(function(json) {
                    return callback(json);
                });
            };

            / 这才像样
            var getServerStuff = ajaxCall;
            
            
            / 因为, 功能, 输入的参数, 和返回的数据, 都严格一致
            / 所以首先要看到, 功能是什么, 进入的参数类型,路径是什么
            / 返回的数据类型, 路径是什么
            
            
            / 这里有值得思考的问题
            / 确实函数多层嵌套, 会消耗人更多的脑力, 看不太懂.
            / 或者我似乎好像认为, 多层函数嵌套本身, 就一定是难的.
            / 或者说, 我似乎, 从没有进行过一种训练,
            / 这种训练应该是, 如何能识别一个多层函数结构
            / 第一种比,第二种难的第一个地方是, 变量更多,
            / 总共有 4个变量, getServerStuff,callback,ajaxCall,json
            / 并且总共出现6次
            / 我需要记住4个变量, 并且要找到参数的来路, 返回值的去处, 
            / 还要思考,执行顺序.
            
            / 可作者是怎么看出来的? 怎么快速看出来的?
            / 这里肯定存在一种方法.
            
            / 好吧, 这里是我们第一个遗留的问题.
            / 也许, 作者很熟悉结构的'语义'

/ 换句话讲, 作者能够比我能够更快的看出, 一个函数到底干了什么?
/ 所以语义化理解代码, 是第一步?

功能和函数的区别在这里

函数只是两种数值之间的关系:输入和输出。


可以通过延迟执行的方式把不纯的函数转换为纯函数:

var pureHttpCall = memoize(function(url, params){
  return function() { return $.getJSON(url, params); }
});

所谓的延迟执行,就是返回一个函数.
也就是, 一个函数的执行返回的值, 弄成函数,
函数充当这个'值',
这里的意义在于, 他把一个执行, 弄成了一个值, 一个变量.


纯函数的优点之一, 是依赖关系透明,

不依赖环境变量, 不改变环境变量的意思是,
所有函数内部需要的外部变量,
都必须通过,参数入口传进来.(这算是依赖注入?)
这样有两个衍生的好处,
1,可移植性, 因为, 接口是确定的,就是参数入口, 我们想要更改的时候,
只需要在把数据按照要求的格式改一下, 依然放在那个入口参数的位置即可
我们能很清楚的看到发生了什么.
如果不是通过入口函数而来, 而是来自环境变量, 那么这个替换过程, 就很不透明,
回过头来的时候, 已经不知道这个数据从哪里来了.
2, 可测试性, 因为对环境变量没有依赖, 只对入口参数有依赖,
所以,我们只需要把需要的参数好好传进去就可以进行测试,
而不需要费力把整个运行环境都大概模拟出来

curry 柯里化

            var memoize = function(f) {
                var cache = {};

                return function() {
                    var arg_str = JSON.stringify(arguments);
                    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
                    return cache[arg_str];
                };
            };

            function sub_curry(fn /*, variable number of args */ ) {
                var args = [].slice.call(arguments, 1);
                return function() {
                    return fn.apply(this, args.concat(toArray(arguments)));
                };
            }

            function curry1(fn, length) {
                // capture fn's # of parameters
                length = length || fn.length;
                return function() {
                    if(arguments.length < length) {
                        // not all arguments have been specified. Curry once more.
                        var combined = [fn].concat(toArray(arguments));
                        return length - arguments.length > 0 ?
                            curry(sub_curry.apply(this, combined), length - arguments.length) :
                            sub_curry.call(this, combined);
                    } else {
                        // all arguments have been specified, actually call function
                        return fn.apply(this, arguments);
                    }
                };
            }
            // 应用lodash里的curry
            let curry = _.curry;

柯里化干了什么?
用我自己的理解来讲就是
他的第一层意思是, 功能延迟执行
第二层意思是, 参数相分离, 在时间上, 空间上相分离?

只传给函数一部分参数通常也叫做局部调用(partial application), 展示了一种预加载的能力

几个curry化函数, 后面的练习题及示例都用得到.

            var match = curry(function(what, str) {
                return str.match(what);
            });

            var replace = curry(function(what, replacement, str) {
                return str.replace(what, replacement);
            });

            var filter = curry(function(f, ary) {
                return ary.filter(f);
            });

            var map = curry(function(f, ary) {
                return ary.map(f);
            });
            var split = curry(function(what, str) {
                return split(what, str)
            })
            var reduce = curry(function(f, init, arr) {
                return arr.reduce(f, init)
            })
            var slice = curry(function(start, end, arr) {
                return arr.slice(start, end);
            })

            var toLowerCase = function(str) {
                return str.toLowerCase();
            }

            var join = curry(function(what, arr) {
                return arr.join(what)
            })
            
            var concat = curry(function (what,str) {
                return str.concat(what)
            })

            var id = function(x) {
                return x;
            };
            var trace = _.curry(function(tag, x) {
                console.log(tag, x);
                return x;
            });

上面的过程, 我们完成了一种转换.
比如 arr.slice(start,end), 这里有功能, 有 参数,
通过包裹一层函数, 并柯里化, 让这些功能和参数,分割了开来.
进行了所谓的预加载,
并且, 可以根据需要随意调整参数的顺序,
这个顺序就是, 我获取数据的顺序.

练习题

// 练习 1
            //==============
            // 通过局部调用(partial apply)移除所有参数

            var words = function(str) {
                return split(' ', str);
            };

            // 没听懂什么意思,
            // 局部调用, 应该是让我用 curry
            // 移除所有参数, 这里参数只有 str 和 ' ', 移除参数是什么意思?
            // 是像下面这样嘛? // 但这样有什么意义? 只有一个参数时, curry的意义是什么?
            var words = curry(function(str) {
                return split(' ', str);
            })
            // 或者是这样?
            var split = curry(function(what, str) {
                return split(what, str)
            })
            var words = split(' ');

            // 练习 1a
            //==============
            // 使用 `map` 创建一个新的 `words` 函数,使之能够操作字符串数组

            var sentences = undefined;

            var sentences = function(strs) {
                return map(words, strs);
            }
            // 这样用, 似乎不太好,

            // 练习 2
            //==============
            // 通过局部调用(partial apply)移除所有参数

            var filterQs = function(xs) {
                return filter(function(x) {
                    return match(/q/i, x);
                }, xs);
            };

            var filterQs = filter(match(/q/i));
            // 这里的filter,match 都是curry过的函数, 返回值为 函数.
            // 这个过程却是挺神奇的, 因为我们确实消除了所有参数! 没有设置形参!

            // 练习 3
            //==============
            // 使用帮助函数 `_keepHighest` 重构 `max` 使之成为 curry 函数

            // 无须改动:
            var _keepHighest = function(x, y) {
                return x >= y ? x : y;
            };

            // 重构这段代码:
            var max = function(xs) {
                return reduce(function(acc, x) {
                    return _keepHighest(acc, x);
                }, -Infinity, xs);
            };
            // 解答

            var max = reduce(_keepHighest, -Infinity);
            // 确实很神奇,, 用了curry函数 之后, 我们确实可以消除形参, 代码看起来确实很简洁
            // 当然前提是, 要了解这些curry函数, 起码要知道需要几个参数, 都代表什么含义.

            // 彩蛋 1:
            // ============
            // 包裹数组的 `slice` 函数使之成为 curry 函数
            // //[1,2,3].slice(0, 2)
            var slice = undefined;

            var slice = curry(function(start, end, arr) {
                return arr.slice(start, end);
            })

            // 彩蛋 2:
            // ============
            // 借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。
            var take = undefined;
            
            var take = slice(0);

上面的练习题, 主要练习的就是, 通过一些已经进行过柯里化的函数,
进行参数调用的方式,
消除了形参.
消除形参的方式, 叫什么point free?
很有用处, 一来可以简化代码, 没有太多 function() {} 嵌套的格式
二来, 根据前面的章节所说, 如果设置形参, 则后续要更改的时候, 这些包裹的中间层函数都要更改.


pointfree

pointfree 模式指的是,永远不必说出你的数据
我的理解是, 永远不用设置你的形参,( 函数存在于变量名, 函数存在于返回值.)

一等公民的函数

指的应该是, 用变量的方式 表示函数的意思?

利用 curry,我们能够做到让每个函数都先接收数据,

看下面这个例子

// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

看到组合和柯里配合的惊人之处了嘛?
首先, 无论是 curry 还是组合, 做到的第一件事情都是延迟执行.参数待定.
在这里, 因为replace 是个柯里化的函数, 所以可以先进行一次数据的加载,
等到sankeCase 执行的时候, 在获取最后一个数据. 这在时间上, 数据的加载是 分开的.
不止如此,
compose的意义在于, 规定了函数执行的顺序?
又或者是, 最后一个数据的来源?
突然发现, compose的缺陷就是, 传输数据的通道只能有一个, 也就是每个函数的最后一个参数.
也就是说, 在上compose这趟列车的所有函数, 都必须要把除了最后一个参数之外的其他参数都绑定掉.
比如,一个柯里化的函数, 可以分隔三个参数,
但为了上compose这趟列车, 他必须要把除了最后一个参数之外的其他参数都要绑定掉.
但这个问题似乎也能解决.

我们稍微改一下
            var snakeCase = function (what,when,where) {
                return compose(replace(what, '_'), toLowerCase(when),areyouok(where));
            }
假设上面的是哪个函数, replace, toLowerCase,areyouok 三个都是柯里化函数
每个函数都有需要待定的参数.
那我们就可以给这个compose 包裹上一个函数, 使其可以变成待定.
也就是说包裹一层函数最大的用处之一,就是延迟执行, 并让参数可以变得待定.
配合curry, (也就是多层函数嵌套,)则可以完成参数绑定时机的分离.
            var snakeCase = curry(function (what,when,where) {
                return compose(replace(what, '_'), toLowerCase(when),areyouok(where));
            })

观察一下就会发现, 通过每次执行状态(compose()的执行也是一种执行),时,
我们都可以通过这种方式,使得延迟执行, 以及参数分离.
或者反过来讲,
如果我们想要通过函数定义包裹的方式实现参数分离, 延迟执行,
就必须要把执行形态包裹进去, 或者说,必须存在执行形态,
这样我们才能匹配?
(虽然我自己都不知道自己在说什么, 但我觉得还挺重要的)

而且实际上上面是存在两次延迟, compose执行返回的函数,本身也是有延迟效果的.
反过来讲, 我们可以通过上面的方式,
始终可以把先确定的功能和函数绑定, 把为确定的功能和数据,进行延迟.

突然发现, 上面的柯里化函数, 也是有缺陷的, 这个缺陷在于, 参数的顺序问题.
即, 柯里化函数的时候, 形参的顺序, 变得非常重要,
从某种角度来讲, 我们定义形参顺序的时候, 必须要知道, 数据绑定的顺序.
需要依次进行绑定, 不能乱了顺序.
但这是有问题的.
因为完全存在一种需求是, 我们不知道会先出现绑定哪个数据.

我用例子来讲一下上面的需求

            var add = curry(function (x,y,z) {
                return x * x + y * 2 + z - 1
            })
            add(y)(z)(x);
在这个例子中, 传参顺序的规定, 使得不能达到我的预期.

我想了半天,确实很难解决这个问题, 简单来讲, 
我传入的值, 需要让柯里化的函数能够识别哪个值对应是哪个参数
像上面的情况, 默认识别方式就是,根据传参的顺序.
假设我们想要通过别的方式识别, 就必须要有一个额外的信息来标记自己是谁,

比如说,
add({value:y,index : 1})({value:z,index : 2})({value:x,index : 0})
当然这要对封装的curry函数进行改动。
暂且不说如何改动,光是这种调用形式, 是否稍显臃肿,且不便。
应该是非常不方便的,
或者, 我们传值时, 不能携带多余信息, 但调用时,是否可以添加额外信息?
比如这样调用
add.index(1)(y).index(2)(z).index(0)(x)
也就是, curry返回的函数中弄一个index接口,
不调用这个接口时, 按正常柯里化函数, 顺序的方式进行,
如果调用了这个接口, 则按照这个顺序,进行柯里化?

感觉不是不可以啊...感觉有戏啊.
明天可以试着写一下?

突然又想到一个问题,

假设已知 add函数和curry,能够得到 curry(add)
            var add = function (x,y,z) {
                return x * x + y * 2 + z - 1
            }

如果已知curry(add) , 能否逆推得出add 函数?

这两个问题,都要求深入了解curry, 明天再干吧,,或者先略过去

trace 用来和 compose 配合, 查看错误在哪里, 很有用, 如果用compose, 就最好知道trace

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});

var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined

组合的另一个优点,(特别是相比柯里化),
组合能够大大增强,代码的可读性, 因为语义化非常的好.
所以比较容易阅读.

练习题

require('../../support');
            var _ = require('ramda');
            var accounting = require('accounting');

            // 示例数据
            var CARS = [{
                    name: "Ferrari FF",
                    horsepower: 660,
                    dollar_value: 700000,
                    in_stock: true
                },
                {
                    name: "Spyker C12 Zagato",
                    horsepower: 650,
                    dollar_value: 648000,
                    in_stock: false
                },
                {
                    name: "Jaguar XKR-S",
                    horsepower: 550,
                    dollar_value: 132000,
                    in_stock: false
                },
                {
                    name: "Audi R8",
                    horsepower: 525,
                    dollar_value: 114200,
                    in_stock: false
                },
                {
                    name: "Aston Martin One-77",
                    horsepower: 750,
                    dollar_value: 1850000,
                    in_stock: true
                },
                {
                    name: "Pagani Huayra",
                    horsepower: 700,
                    dollar_value: 1300000,
                    in_stock: false
                }
            ];

            // 练习 1:
            // ============
            // 使用 _.compose() 重写下面这个函数。提示:_.prop() 是 curry 函数
            var isLastInStock = function(cars) {
                var last_car = _.last(cars);
                return _.prop('in_stock', last_car);
            };

            var isLastInStock = compose(_.prop('in_stock'), _.last)

            // 练习 2:
            // ============
            // 使用 _.compose()、_.prop() 和 _.head() 获取第一个 car 的 name
            var nameOfFirstCar = undefined;
            var nameOfFirstCar = compose(_.prop('name'), _.head)

            // 练习 3:
            // ============
            // 使用帮助函数 _average 重构 averageDollarValue 使之成为一个组合
            var _average = function(xs) {
                return reduce(add, 0, xs) / xs.length;
            }; // <- 无须改动

            var averageDollarValue = function(cars) {
                var dollar_values = map(function(c) {
                    return c.dollar_value;
                }, cars);
                return _average(dollar_values);
            };

            var averageDollarValue = compose(_average, map(function(c) {
                return c.dollar_value;
            }))

            // 练习 4:
            // ============
            // 使用 compose 写一个 sanitizeNames() 函数,返回一个下划线连接的小写字符串:
            //例如:sanitizeNames(["Hello World"]) //=> ["hello_world"]。

            var _underscore = replace(/\W+/g, '_'); //<-- 无须改动,并在 sanitizeNames 中使用它

            var sanitizeNames = undefined;
            var sanitizeNames = map(compose(toLowerCase, replace));
            var sanitizeNames = compose(map(toLowerCase), map(replace));

            // 彩蛋 1:
            // ============
            // 使用 compose 重构 availablePrices

            var availablePrices = function(cars) {
                var available_cars = _.filter(_.prop('in_stock'), cars);
                return available_cars.map(function(x) {
                    return accounting.formatMoney(x.dollar_value);
                }).join(', ');
            };

            
            var availablePrices = compose(join(', '), map(compose(accounting.formatMoney, _.prop('dollar_value'))), _.filter(_.prop('in_stock')))

            // 彩蛋 2:
            // ============
            // 重构使之成为 pointfree 函数。提示:可以使用 _.flip()

            var fastestCar = function(cars) {
                var sorted = _.sortBy(function(car) {
                    return car.horsepower
                }, cars);
                var fastest = _.last(sorted);
                return fastest.name + ' is the fastest';
            };
            
            var fastestCar = compose(concat(' is the fastest'),_.prop('name'),_.last,_.sortBy(_.prop('horsepower'))) 
            

做完这组练习题,
确实能够感受到curry和compose的魅力.
有了curry和compose之后, 似乎, 真的可以把所有代码弄成函数调用的形式.
只不过, 要充分了解, 每个函数接收的参数类型, 返回类型
很有魅力.

自由定理

// head :: [a] -> a
compose(f, head) == compose(head, map(f));

// filter :: (a -> Bool) -> [a] -> [a]
compose(map(f), filter(compose(p, f))) == compose(filter(p), map(f));

容器

lift: 一个函数在调用的时候,如果被 map 包裹了,那么它就会从一个非 functor 函数转换为一个 functor 函数。我们把这个过程叫做 lift。

Container

            // 第一种容器
            var Container = function(x) {
                this.__value = x;
            }

            Container.of = function(x) {
                return new Container(x);
            };

            // (a -> b) -> Container a -> Container b
            Container.prototype.map = function(f) {
                return Container.of(f(this.__value))
            }

            Container.of("bombs").map(concat(' away')).map(_.prop('length'))
            //=> Container(10)

            //观察这句代码, 他与 compose 有点类似,
            // compose是从右向左, 而map 是 从左向右
            // compose 是把 return 出来的值, 传递给下一个函数的 参数入口
            // 而 map 是return 一个 容器, 用调用的方式, 把参数传进去.
            // 当然两者有很明显的不同
            // compose 返回的是函数, compose的执行本身 实际上是延迟执行, 预留出一个参数入口
            // 而在上面这种情况, 使用map的时候, 实际上参数值已经确定, 并且功能已经执行,
            // 不存在延迟执行.

            // 这种链式调用方式, 作者似乎称之为 点记法(dot notation syntax)

Maybe

            // 第二种容器
            var Maybe = function(x) {
                this.__value = x;
            }

            Maybe.of = function(x) {
                return new Maybe(x);
            }

            Maybe.prototype.isNothing = function() {
                return(this.__value === null || this.__value === undefined);
            }

            Maybe.prototype.map = function(f) {
                return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
            }

            var maybe = curry(function(x, f, m) { // 出错时返回自定义的信息, 不执行 f, 
                return m.isNothing() ? x : f(m.__value);
            });

Either

            // 第三种容器 Either
            var Left = function(x) {
                this.__value = x;
            }

            Left.of = function(x) {
                return new Left(x);
            }
            // 这又是一种奇特的 map, Left容器遇到多少个map, 多少个 f, 返回的都是同样的值,
            // 让所有f都失效.
            // 这根 Maybe.of(null) 稍微不同.虽然Maybe.of(null) 的 map 也会让所有的map,f都失效.
            Left.prototype.map = function(f) {
                return this;
            }

            var Right = function(x) {
                this.__value = x;
            }

            Right.of = function(x) {
                return new Right(x);
            }

            Right.prototype.map = function(f) {
                return Right.of(f(this.__value));
            }

            //  getAge :: Date -> User -> Either(String, Number)
            var getAge = curry(function(now, user) {
                var birthdate = moment(user.birthdate, 'YYYY-MM-DD');
                if(!birthdate.isValid()) return Left.of("Birth date could not be parsed");
                return Right.of(now.diff(birthdate, 'years'));
            });
            //  either :: (a -> c) -> (b -> c) -> Either a b -> c
                        // either的作用是这样的, 
                        //任何map中的f对Left都没有作用,
                        // either就是希望, 遇到Left情况时, 能够让f产生作用的接口.
            var either = curry(function(f, g, e) {
                switch(e.constructor) {
                    case Left:
                        return f(e.__value);
                    case Right:
                        return g(e.__value);
                }
            });

            //  zoltar :: User -> _
            var zoltar = compose(console.log, either(id, fortune), getAge(moment()));

IO

            var IO = function(f) {
                this.__value = f;
            }

            IO.of = function(x) {
                return new IO(function() {
                    return x;
                });
            }

            IO.prototype.map = function(f) {
                return new IO(_.compose(f, this.__value));
            }
            var IO = function(f) {
                this.unsafePerformIO = f;
            }

            IO.prototype.map = function(f) {
                return new IO(_.compose(f, this.unsafePerformIO));
            }
/ 这种方式,就把所有功能都推迟到最后再执行,
/ map 执行的结果不是f 的执行, 而是 compose

Task

Promise

这一部分没有详细剖析,

Compose

Compose 这个容器是专门用来存放 容器的容器.
值是一个容器
Compose.map 返回的也是双层的容器.

var Compose = function(f_g_x){
  this.getCompose = f_g_x;
}

Compose.prototype.map = function(f){
  return new Compose(map(map(f), this.getCompose));
}
/你会发现, 这个f, 穿越了两个容器.
感觉除非是很熟悉的人, 谁会这么用呢?

练习题

            require('../../support');
            var Task = require('data.task');
            var _ = require('ramda');

            // 练习 1
            // ==========
            // 使用 _.add(x,y) 和 _.map(f,x) 创建一个能让 functor 里的值增加的函数

            var ex1 = undefined

            var ex1 = function(x) {
                compose(_.map(_.add(x)))
            }

            //练习 2
            // ==========
            // 使用 _.head 获取列表的第一个元素
            var xs = Identity.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do']);

            var ex2 = undefined
            var ex2 = xs.map(_.head);

            // 练习 3
            // ==========
            // 使用 safeProp 和 _.head 找到 user 的名字的首字母
            var safeProp = _.curry(function(x, o) {
                return Maybe.of(o[x]);
            });

            var user = {
                id: 2,
                name: "Albert"
            };

            var ex3 = undefined

            var fisrtChart = safeProp("name", user).map(_.head);

            // 练习 4
            // ==========
            // 使用 Maybe 重写 ex4,不要有 if 语句

            var ex4 = function(n) {
                if(n) {
                    return parseInt(n);
                }
            };

            var ex4 = undefined
            var ex4 = function(n) {
                return Maybe.of(n).map(parseInt)
            }
            /我是一边写, 一边觉得忽然发觉很神奇.. 真的把if,else 给干掉了.

            // 练习 5
            // ==========
            // 写一个函数,先 getPost 获取一篇文章,然后 toUpperCase 让这片文章标题变为大写

            // getPost :: Int -> Future({id: Int, title: String})
            // 因为不了解Task, 我们暂且当成Promise来对待
            var getPost = function(i) {
                return new Task(function(rej, res) {
                    setTimeout(function() {
                        res({
                            id: i,
                            title: 'Love them futures'
                        })
                    }, 300)
                });
            }

            var ex5 = undefined
            var ex5 = getPost(i).fork(function(data) {
                return _.prop('title', data).toUpperCase() /
                    或者这样 ?
                    return compose(toUpperCase, _.prop('title'))(data);
            })

            // 练习 6
            // ==========
            // 写一个函数,使用 checkActive() 和 showWelcome() 分别允许访问或返回错误

            var showWelcome = _.compose(_.add("Welcome "), _.prop('name'))
            var checkActive = function(user) {
                return user.active ? Right.of(user) : Left.of('Your account is not active')
            }

            var ex6 = undefined

            var ex6 = compose(map(showWelcome), checkActive);

            // 练习 7
            // ==========
            // 写一个验证函数,检查参数是否 length > 3。如果是就返回 Right(x),否则就返回
            // Left("You need > 3")

            var ex7 = function(x) {
                return undefined // <--- write me. (don't be pointfree)
                return x > 3 ? Right(x) : Left("You need > 3")
            }

            // 练习 8
            // ==========
            // 使用练习 7 的 ex7 和 Either 构造一个 functor,如果一个 user 合法就保存它,否则
            // 返回错误消息。别忘了 either 的两个参数必须返回同一类型的数据。

            var save = function(x) {
                return new IO(function() {
                    console.log("SAVED USER!");
                    return x + '-saved';
                });
            }
            var ex8 = undefined
            
            var ex8 = function(x) {
                return undefined // <--- write me. (don't be pointfree)
                return x > 3 ? save(x) : IO.of('You need > 3')
            }
            // 还真是不知道对不对
            

一个 functor,只要它定义个了一个 join 方法和一个 of 方法,并遵守一些定律,那么它就是一个 monad。

Maybe.prototype.join = function() {
  return this.isNothing() ? Maybe.of(null) : this.__value;
}

文中说这个join的用途在于, 相同类型容器,多层嵌套时, 可以用来解除一层嵌套.
但从语义上来讲, 或者原本的意义上来讲,
join() 应该是取值的意思,
而 of() 应该是存值的意思.

monad

            // monad
            Maybe.prototype.join = function() {
                return this.isNothing() ? Maybe.of(null) : this.__value;
            }

            var join = function(m) {
                return m.join();
            }

            // 对其他类型容器都是一样的
            IO.prototype.join = function() {
                return this.unsafePerformIO();
            }

            //  chain :: Monad m => (a -> m b) -> m a -> m b
            var chain = curry(function(f, m) {
                return m.map(f).join(); // 或者 compose(join, map(f))(m)
            });

下面这个用例, 不是很懂, 但显得略微的屌, 还好之前接触过跟Task 类似 的 Promise,
显得没那么陌生

// getJSON :: String -> {} -> Task(Error, JSON)
var getJSON = curry(function(url, params) {
return new Task(function(reject, result) {
$.getJSON(url, params, result).fail(reject);
});
});

/ 应用chain
getJSON('/authenticate', {username: 'stale', password: 'crackers'})
  .chain(function(user) {
    return getJSON('/friends', {user_id: user.id});// 再次返回一个 Task, 如果不是chain, 而是map, 最后会嵌套
});
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]);

例子2

            var $ = function(selector) {
                return new IO(function() {
                    return document.querySelectorAll(sel ector);
                });
            }
稍微改一下
            var querySelector = function(selector) {
                return new IO(function() {
                    return document.querySelectorAll(sel ector);
                });
            }

querySelector("input.username").chain(function(uname) {
  return querySelector("input.email").chain(function(email) {
    return IO.of(
      "Welcome " + uname.value + " " + "prepare for spam at " + email.value
    );
  });
});
// IO("Welcome Olivia prepare for spam at olivia@tremorcontrol.net");

/ 注意, IO里的chain 调用的 map/join , 而 map 里用的是compose

我勉强看懂了这一段代码干了什么,
但感觉神乎其神!
这相当于compose的一种神乎其神的用法.
实际上你发现, function(uname) {return querySelector("input.email")} 单指这一句代码时,
你会发现,压根uname 和 里面的代码没鸟关系! 全都给了最后一个函数.
这个东西, 很神奇. 对于我说不上来, 感到很自责.
我试着用compose来弄一下,

            / 现在我们试着把容器去掉
            
            var querySelector = function(selector) {
                    return function () {
                        return document.querySelectorAll(selector);
                    }
            }
            compose(function (uname) {
                return compose(function (email) {
                    return function () {return "Welcome " + uname + " " + "prepare for spam at " + email}
                },querySelector("input.email"))
            },querySelector("input.username"))

真是不好理解.
严格来讲, 这不符合我认为的纯函数的概念.
因为里层函数调用 uname 的时候, 实际上是跨越了一个参数入口,
也就是uname没有通过 参数入口传进来.
不过这种方式, 解决了一个问题,
那就是突破了compose 只能一次传递一个数据的限制.

或者想要突破一次只能传一个参数, 可以用 curry

            var some = curry(function (uname,email) {return "Welcome " + uname + " " + "prepare for spam at " + email})
            compose(compose(some,querySelector("input.username")),querySelector("input.email"))

这样是否也能达到同样的效果?

例子3

            Maybe.of(3).chain(function(three) {
                return Maybe.of(2).map(add(three));
            });
            // Maybe(5);
            
                        /相当于
            Maybe.of(3).map(function(three) {
                return Maybe.of(2).map(add(three));
            }).join();
            

                        /改成一般函数, 应该是相当于
            Maybe.of(3).map(add(2)})

比较一下, 比起一般函数, 有什么优点嘛? 
首先这个2 , 被容器包住了. 从某种角度来讲, 我们希望所有的值,都被存在容器当中
也就是说, 我们希望所有的函数返回值的时候, 都包裹在容器当中.

换句话说, 之前我们把值放进容器中, 是用 Maybe.of() 或者 Maybe.map(f)的.
并且map(f)中的f, 则是接收一个非容器类型, 返回的是一个值.
而在这里, 我们希望 所有f返回的都是一个容器类型, 也就是把值全都放在容器当中

而,join,或者 chain, 就是用来处理这种情况的.

---
还是要问一句,这么做究竟有什么好处?

作者举了一个对比的例子

应用monad
// readFile :: Filename -> Either String (Future Error String)
// httpPost :: String -> Future Error JSON

//  upload :: String -> Either String (Future Error JSON)
var upload = compose(map(chain(httpPost('/uploads'))), readFile);

如果想让 url 变成待定参数可以这样
var upload =function (url) {
    return  compose(map(chain(httpPost('/uploads'))), readFile);
}

不应用monad, 纯指令式 完成上述功能
//  upload :: String -> (String -> a) -> Void
var upload = function(filename, callback) {
  if(!filename) {
    throw "You need a filename!";
  } else {
    readFile(filename, function(err, contents) {
      if(err) throw err;
      httpPost(contents, function(err, json) {
        if(err) throw err;
        callback(json);
      });
    });
  }
}

我们试着分析一下好处在哪?

  1. readfile 的 Either 对 filename 进行了一次判断, 也就是去掉了if else?

  2. readfile 返回的值, 在调用map 时, Either 会进行测一次判断?
    这里有疑问, readfile的Either到底是对1还是对2产生了作用?

  3. httpPost 返回的值, 又会得到一次 值判断. 也去掉了一个if else

  4. 其实很明显就能看到, 第一种比第二种代码量要少.
    更关键的是, 用第一种, 也就是声明式的方式, 可以减少设置变量.
    而第二种指令式, 就要设置很多变量.
    比如 filename,callback, err, contents,json
    而这些变量的设置,在后期改动的时候, 会比较麻烦.
    因为一处改, 所有引用这些变量的地方都要改.

练习题

            // 练习 1
            // ==========
            // 给定一个 user,使用 safeProp 和 map/join 或 chain 安全地获取 sreet 的 name

            var safeProp = _.curry(function(x, o) {
                return Maybe.of(o[x]);
            });
            var user = {
                id: 2,
                name: "albert",
                address: {
                    street: {
                        number: 22,
                        name: 'Walnut St'
                    }
                }
            };

            var ex1 = undefined;

            var ex1 = compose(chain(safeProp('street')), safeProp('address'))
            //调用方式
            ex1(user);
            // 或者可以这样
            var ex1 = mcompose(safeProp('street'), safeProp('address'));
            // 调用方式
            ex1(Maybe.of(user));

            // 练习 2
            // ==========
            // 使用 getFile 获取文件名并删除目录,所以返回值仅仅是文件,然后以纯的方式打印文件

            var getFile = function() {
                return new IO(function() {
                    return __filename;
                });
            }

            var pureLog = function(x) {
                return new IO(function() {
                    console.log(x);
                    return 'logged ' + x;
                });
            }

            var ex2 = undefined;
            // ex2 返回一个容器,
            var ex2 = getFile().chain(purelog)
            // 或者
            var ex2 = compose(chain(purelog), getFile)
            // ex2 返回一个 函数, 执行之后, 得到函数

            // 练习 3
            // ==========
            // 使用 getPost() 然后以 post 的 id 调用 getComments()
            var getPost = function(i) {
                return new Task(function(rej, res) {
                    setTimeout(function() {
                        res({
                            id: i,
                            title: 'Love them tasks'
                        });
                    }, 300);
                });
            }

            var getComments = function(i) {
                return new Task(function(rej, res) {
                    setTimeout(function() {
                        res([{
                                post_id: i,
                                body: "This book should be illegal"
                            },
                            {
                                post_id: i,
                                body: "Monads are like smelly shallots"
                            }
                        ]);
                    }, 300);
                });
            }

            var ex3 = undefined;

            var ex3 = getPost(i).fork(getComments)
            // 这是真不清楚

            // 练习 4
            // ==========
            // 用 validateEmail、addToMailingList 和 emailBlast 实现 ex4 的类型签名

            //  addToMailingList :: Email -> IO([Email])
            var addToMailingList = (function(list) {
                return function(email) {
                    return new IO(function() {
                        list.push(email);
                        return list;
                    });
                }
            })([]);

            function emailBlast(list) {
                return new IO(function() {
                    return 'emailed: ' + list.join(',');
                });
            }

            var validateEmail = function(x) {
                return x.match(/\S+@\S+\.\S+/) ? (new Right(x)) : (new Left('invalid email'));
            }

            //  ex4 :: Email -> Either String (IO String)
            var ex4 = undefined;
            
            var ex4 = mcompose(validateEmail,emailBlast,addToMailingList);
            // 或者
            var ex4 = compose(chain(validateEmail),chain(emailBlast),addToMailingList);

ap 函子 applicative functor

Container.prototype.ap = function(other_container) {
  return other_container.map(this.__value);
}

例子, 解决 2 + 3 的问题
第一种
// 使用可靠的 map 函数试试
var container_of_add_2 = map(add, Container.of(2));/ 返回一个函数
// Container(add(2))
Container.of(3).map(container_of_add_2)/ 完成 2 + 3

第二种
// 使用chain
Container.of(2).chain(function(two) {
  return Container.of(3).map(add(two));
});

第三种
// 使用ap
// add是个柯里化的函数
Container.of(2).map(add).ap(Container.of(3));
// Container(5)

ap 像不像 独立定义的map, 我觉得很像.
ap变成 点记法之后, f 来源变成 this.__value, m 来源就是other_container

var map = function (f,m) { return m.map(f) }

感官上ap 和 chain的区别
首先ap接收的是一个容器, 更准确来讲是, 让两个容器里的值碰头.
而chain接收的是一个函数, 一个返回容器的函数.
具体两者的应用场景的区别, 作为一个小白, 还没什么头绪.

还有一个注意的地方是, 调用ap的容器, 里的值必须是函数,
一个特性

F.of(x).map(f) == F.of(f).ap(F.of(x))
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)

对比一下 
Maybe.of(2).map(Maybe.of(3).map(add).join())
// 似乎作者不希望有值是完全暴露在容器之外.至少起码要包裹一层
// 所以 不怎么用join?

从使用的语义上来讲,
map的调用, 给人的感觉是, 我有值, 我找函数
而ap的调用, 给人的感觉是, 我有函数, 我找值.

作者说Task是,ap的用武之地

// Http.get :: String -> Task Error HTML

var renderPage = curry(function(destinations, events) { /* render page */  });

Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
// Task("<div>some page with dest and events</div>")

作者的原话是这样的
两个请求将会同时立即执行,当两者的响应都返回之后,renderPage 就会被调用。这与 monad 版本的那种必须等待前一个任务完成才能继续执行后面的操作完全不同。本来我们就无需根据目的地来获取事件,因此也就不需要依赖顺序执行。
作者的意思是, Http.get('/destinations') 和 Http.get('/events') 两个动作并行执行
但是我完全不理解,怎么做到的。

因为 Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
相当于, Http.get('/events').chain(Http.get('/destinations').map(renderPage))

就算是想成 Promise 也无法理解

            new Promise(function (res,rej) {
                setTimeout(function () {
                    console.log(123)
                    res(1234)
                },10000)
            }).then(function (data) {
                console.log(data);
                new Promise(function (res,rej) {
                setTimeout(function () {
                    console.log(223)
                },2000)
            })
            })

如果Task.map 相当于 Promise.then
那么上面的两个ap,里包含两个 map,
相当于 Promise 两个then
如果像上面这样模拟两个异步任务,
就会发现,
只有在第一个异步任务完成,返回结果之后, 才会执行第二个异步任务

那task到底是怎么实现的?
怎么做到.两个异步任务并行发出?
其实两个任务并行发出不是难事,
但按照作者的意思是, 当两个异步都返回结果时, 才会执行.

但到底怎么做到的? 是类似Pormiseall? 不能吧?

我还真是笨
百度搜不到 Task, 可以直接上github上 搜索啊
Data.Task源码
Data.Task 函子 源码 简书
源码虽然搞到手, 还是先等等再看吧, 看着有点头疼..

pointFree版ap

var liftA2 = curry(function(f, functor1, functor2) {
  return functor1.map(f).ap(functor2);
});

var liftA3 = curry(function(f, functor1, functor2, functor3) {
  return functor1.map(f).ap(functor2).ap(functor3);
});

使用

liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)

liftA2(renderPage, Http.get('/destinations'), Http.get('/events'))
// Task("<div>some page with dest and events</div>")

liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({id: 3, email: "gg@allin.com"}

免费开瓶器, 也就是 各种接口的互相转化

之前,我们是先定义 map, 和of
由这两个衍生出 ap
            X.of = function (x) {
                return new X(x)
            }
                        
            X.prototype.map = function (f) {
                return X.of(f(this.__value))
            }

根据这两个, 我们推导出 ap
            X.prototype.ap = function (m) {
                return m.map(this.__value);
            }                        

现在反过来, 我们先定义 of 和 ap  然后推导出map
用作者的话来说就是           // 从 of/ap 衍生出的 map
            X.of = function (f) {
                return new X(f)
            }
            X.prototype.ap = function (m) {/我们为了模拟的彻底. ap的定义不用map
                return X.of(this.__value(m.__value))
            }
由此推导出map
            X.prototype.map = function(f) {
                return this.constructor.of(f).ap(this);
            }

同样的意思, 
我们之前是先定义 of, map, join , 然后推导出 chain
            X.prototype.join = function () {
                return this.__value;
            }
然后推导出chain
            X.prototype.chain = function (f) {
                return this.map(f).join();
            }

返回来, 我们可以先定义chain, 由chain, 和 of 推导出 map
用作者的话来说// 从 chain 衍生出的 map
先定义chain
            X.prototype.chain = function (f) {/写完我也懵逼了, 好简单.
                return f(this.__value)
            }

然后推到出 map
X.prototype.map = function(f) {
  var m = this;
  return m.chain(function(a) {
    return m.constructor.of(f(a));
  });
}
// 简化之后就是, return this.constructor.of(f(this.__value));/ 这就跟我们之前理解的map 是一样的

// 从 chain/map 衍生出的 ap
X.prototype.ap = function(other) {
  return this.chain(function(f) {
    return other.map(f);
  });
};
/简化之后就是, return other.constructor.of(this.__value(other.__value))
/ 这就跟我们之前对ap 的理解是一样的, 取出前面的f 和后面的值见面.

作者说, ap比chain优点在于能够并行,
我实在不懂, ap为什么能够并行?

  var tOfM = compose(Task.of, Maybe.of);

  liftA2(_.concat, tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
  // Task(Maybe(Rainy Days and Mondays always get me down))
这我就有点不理解了,
除非这个concat 不是我知道的那个concat, 否则这是不成立的.
因为会执行 concat(Maybe.of(),Maybe.of()) , 如果我我知道的concat, 那这个肯定不成立.

练习题

            require('./support');
            var Task = require('data.task');
            var _ = require('ramda');

            // 模拟浏览器的 localStorage 对象
            var localStorage = {};
            // localStorage 对象? 什么意思? localStrorage 对象有什么特征嘛?
            // 不会..

            // 练习 1
            // ==========
            // 写一个函数,使用 Maybe 和 ap() 实现让两个可能是 null 的数值相加。

            //  ex1 :: Number -> Number -> Maybe Number
            var ex1 = function(x, y) {
                return Maybe.of(x).ap(Mapbe.of(y))
            };
            // 这也是挺神奇的. 对 x,y 进行了空值检查. 只是返回值就是个容器了

            // 练习 2
            // ==========
            // 写一个函数,接收两个 Maybe 为参数,让它们相加。使用 liftA2 代替 ap()。

            //  ex2 :: Maybe Number -> Maybe Number -> Maybe Number
            var ex2 = undefined;

            var add = curry(function(x, y) {
                return x + y;
            })
            // 是这样嘛?
            var ex2 = function(m1, m2) {
                return liftA2(add, m1, m2);
            }

            // 练习 3
            // ==========
            // 运行 getPost(n) 和 getComments(n),两者都运行完毕后执行渲染页面的操作。(参数 n 可以是任意值)
            var makeComments = _.reduce(function(acc, c) {
                return acc + "<li>" + c + "</li>"
            }, "");
            var render = _.curry(function(p, cs) {
                return "<div>" + p.title + "</div>" + makeComments(cs);
            });

            function getComments(i) {// 话说这个i 有个屁用?
                return new Task(function(rej, res) {
                    setTimeout(function() {
                        res(["This book should be illegal", "Monads are like space burritos"]);
                    }, 300);
                });
            }

            function getPost(i) {
                return new Task(function(rej, res) {
                    setTimeout(function() {
                        res({
                            id: i,
                            title: 'Love them futures'
                        });
                    }, 300);
                });
            }

            //  ex3 :: Task Error HTML
            var ex3 = undefined;

            // 不懂..... 
            var ex3 = liftA2(render, getPost(2), getComments(6));
            // 只能这么理解了. 不过感觉好奇怪.
            // 我八辈子可能都无法应用这种代码

            // 练习 4
            // ==========
            // 写一个 IO,从缓存中读取 player1 和 player2,然后开始游戏。

            localStorage.player1 = "toby";
            localStorage.player2 = "sally";

            var getCache = function(x) {
                return new IO(function() {
                    return localStorage[x];
                });
            }
            var game = _.curry(function(p1, p2) {
                return p1 + ' vs ' + p2;
            });

            //  ex4 :: IO String
            var ex4 = undefined;

            var ex4 = IO.of(game).ap(getCache('player1')).ap(getCache('player2'));
            // 或者
            var ex4 = liftA2(game, getCache('player1'), getCache('player2'));
            // 如果用 monad 呢?

            var ex4 = IO.of(game).chain(function(f) {
                return getCache('player1').map(f)
            }).chain(function(f) {
                return getCache('player2').map(f)
            })
            // 把我自己都弄晕了.

另外的一些补充

至此, 把 js 函数式编程指南看来三遍.
用时10天. 我对我的效率感到稍许绝望. 还是自控力不行的表现.
好在, 即使效率是致命伤, 但函数式编程指南本身的阅读, 对我是一种脑洞大开的体验.
只能说, 大概明白是怎么回事了. 只能算稍微理解了.远远谈不上深入理解, 也谈不上应用.
其实初衷只是想要解开我对函数的一些疑惑.
函数这个我经常用, 却总是无法完全掌握用法的东西.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容