2020-04-04:
坚持「相同输入得到相同输出」原则
一、什么是纯函数:
定义: 对相同的输入它保证能返回相同的输出。
例子:
var xs = [1,2,3,4,5]; // 纯的 xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] // 不纯的 xs.splice(0,3); //=> [1,2,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []
二、什么是副作用:
定义:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。(作者认为,副作用是产生bug 的温床)
副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。
三、为什么要追求“纯”
(1)因为同一输入总是能得到唯一输出,因此结果可缓存:
toolz.memoize 实现以上功能
值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:
(2)可移植性/自文档性,更易于观察和理解:换言之,不存在函数以外的动作,搭配类型签名完美。
(3)可测试性:只需简单地给函数一个输入,然后断言输出就好了。
(4)合理性,也就是引用透明性:意味着,同一输入下,改段代码可以替换成它执行所得的结果
(5)并行代码:能做成pipe(管道)执行,因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。
四、“柯里化”:
这个平时用的最多,总结为:是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。
// 有趣的练习: // 1、包裹数组的 `slice` 函数使之成为 curry 函数 // //[1,2,3].slice(0, 2) var slice = _.curry(function(start, end, xs){ return xs.slice(start, end); }); // 2、借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。 var take = slice(0); // 使用: [1, 2, 3, 4].take(2) == [1, 2]
五、代码组合:
var compose = function(f,g) { return function(x) { return f(g(x)); }; };
(1)f
和 g
都是函数,x
是在它们之间通过“管道”传输的值。(2)g
将先于 f
执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用
(3)compose 里多少个函数都可以
Pointfree:
定义:函数无须提及将要操作的数据是什么样的。
好处:pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。
Debug:
使用不纯的 trace
函数来追踪代码的执行情况:
// trace: var trace = curry(function(tag, x){ console.log(tag, x); return x; }); // 例子: var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/s{2,}/ig, ' ')); // after split [ 'The', 'world', 'is', 'a', 'vampire' ]
六、什么是声明式代码:
它指明的是做什么
,不是怎么做。
// 命令式硬编码了那种一步接一步的执行方式。而compose
表达式只是简单地指出了这样一个事实:用户验证是toUser
和logIn
两个行为的组合。
// 命令式 var authenticate = function(form) { var user = toUser(form); return logIn(user); }; // 声明式 var authenticate = compose(logIn, toUser);
七、Hindley-Milner 类型签名(TypeScript):
类型签名是以 “Hindley-Milner” 系统写就的
类型签名在写纯函数时所起的作用非常大:(1)短短一行,就能暴露函数的行为和目的;(2)让它们保持通用、抽象;(3)类型签名不但可以用于编译时检测(compile time checks),还是最好的文档。
八、特百惠(容器思想):
(1)首先定义一个什么功能都没有的容器,只用于装数据,和定义了一个创建对象的简便方法:
var Container = function(x) { this.__value = x; } Container.of = function(x) { return new Container(x); }; // 使用: Container.of(3) //=> Container(3) Container.of("hotdogs") //=> Container("hotdogs")
(2)functor容器,其实就是在container基础上加了个map方法,使之mappable:
// (a -> b) -> Container a -> Container b Container.prototype.map = function(f){ return Container.of(f(this.__value)) } // 使用: Container.of(2).map(function(two){ return two + 2 }) //=> Container(4)
(3)Maybe容器,functor基础上,在map的方法中加入一个判断,使之出现两个可能的结果:
实际当中,Maybe
最常用在那些可能会无法成功返回结果的函数中。
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)); } // Maybe 会先检查自己的值是否为空,然后才调用传进来的函数。这样我们在使用 map 的时候就能避免恼人的空值了(注意这个实现出于教学目的做了简化)。 // 使用: Maybe.of("Malkovich Malkovich").map(match(/a/ig)); //=> Maybe(['a', 'a']) Maybe.of(null).map(match(/a/ig)); //=> Maybe(null)
(4) 纯的错误处理:left和right
right容器执行正常操作,left容器无视正常操作而是内部嵌入一个错误消息达到纯的错误处理。
left跟maybe的区别:用 Maybe(null)
来表示失败并把程序引向另一个分支,但是这并没有告诉我们太多信息。
left跟maybe的共同点:就像 Maybe(null)
,当返回一个 Left
的时候就直接让程序短路。
var moment = require('moment'); // 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')); }); getAge(moment(), {birthdate: '2005-12-12'}); // Right(9) getAge(moment(), {birthdate: 'balloons!'}); // Left("Birth date could not be parsed")
-- either容器:将left和right整合
// fortune :: Number -> String var fortune = compose(concat("If you survive, you will be "), add(1)); // zoltar :: User -> Either(String, _) var zoltar = compose(map(console.log), map(fortune), getAge(moment())); // either :: (a -> c) -> (b -> c) -> Either a b -> c 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())); zoltar({birthdate: '2005-12-12'}); // "If you survive, you will be 10" // undefined zoltar({birthdate: 'balloons!'}); // "Birth date could not be parsed" // undefined
九、Monad(洋葱):
(1)pointed functor :实现了of方法的functor,of方法实际是用来把值放到默认最小化上下文(default minimal context)中的(希望容器类型里的任意值都能发生 lift
,然后像所有的 functor 那样再 map
出去。)。
(2)monad 是可以变扁(flatten)的 pointed functor:一个 functor,只要它定义个了一个 join
方法(有两层相同类型的嵌套可以用该方法压扁到一块)和一个 of
方法,并遵守一些定律,那么它就是一个 monad。
var mmo = Maybe.of(Maybe.of("nunchucks")); // Maybe(Maybe("nunchucks")) Maybe.prototype.join = function() { return this.isNothing() ? Maybe.of(null) : this.__value; } // 使用: mmo.join(); // Maybe("nunchucks")
(3)chain函数:
chain函数把 map/join 套餐打包到一个单独的函数中。如果你之前了解过 monad,那你可能已经看出来 chain
叫做 >>=
(读作 bind)或者 flatMap
;都是同一个概念的不同名称罢了。
// chain 的实现: // 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) }); Maybe.of(3).chain(function(x) { return Maybe.of(2).map(add(x)); }); // Maybe(5);
另一种实现:chain
可以自动从任意类型的 map
和 join
衍生出来,就像这样:t.prototype.chain = function(f) { return this.map(f).join(); }
。
(4)一些例子:
十、applicative functor:实现了 ap
方法的 pointed functor
问题来了:假设有两个同类型的 functor,我们想把这两者作为一个函数的两个参数传递过去来调用这个函数,
解决办法:ap 能够把一个 functor 的函数值应用到另一个 functor 的值上。
Container(3)
从嵌套的 monad 函数的牢笼中释放了出来。需要再次强调的是,本例中的 add
是被 map
所局部调用(partially apply)的,所以 add
必须是一个 curry 函数。
关于ap:
// ap实现: Container.prototype.ap = function(other_container) { return other_container.map(this.__value); } // ap特性: //of/ap
等价于map
F.of(x).map(f) == F.of(f).ap(F.of(x)) //因此它是个从左到右填入参数的: // 应用:sign in var $ = function(selector) { return new IO(function(){ return document.querySelector(selector) }); } // getVal :: String -> IO String var getVal = compose(map(_.prop('value')), $); // Example: // =============== // signIn :: String -> String -> Bool -> User var signIn = curry(function(username, password, remember_me){ /* signing in */ }) IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false)); // IO({id: 3, email: "gg@allin.com"})
signIn
是一个接收 3 个参数的 curry 函数,因此我们需要调用 ap
3 次。在每一次的 ap
调用中,signIn
就收到一个参数然后运行,直到所有的参数都传进来,它也就执行完毕了。
我们可以继续扩展这种模式,处理任意多的参数。另外,左边两个参数在使用 getVal
调用后自然而然地成为了一个 IO
,但是最右边的那个却需要手动 lift
,然后变成一个 IO
,这是因为 ap
需要调用者及其参数都属于同一类型。
十一、关于 lift:pointfree 的方式调用 applicative functor
(1)lift 实现:
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); }); //liftA4, etc
(2)举例:liftA2的应用(A2指需要两个参数)
// checkEmail :: User -> Either String Email // checkName :: User -> Either String String // createUser :: Email -> String -> IO User var createUser = curry(function(email, name) { /* creating... */ }); Either.of(createUser).ap(checkEmail(user)).ap(checkName(user)); // Left("invalid email") //等价于
liftA2(createUser, checkEmail(user), checkName(user)); // Left("invalid email")
十二、of、ap、map之间的替代:
总结1:含of方法:容器、 map的容器 :functor、 join的容器:monad、 map+join容器:chain、 ap的functor:applicative、 lift是特殊的applicative
总结2: of/ap == map
总结3: chain可以衍生出map、 chain/map能衍生出ap
// 从 chain 衍生出的 map X.prototype.map = function(f) { var m = this; return m.chain(function(a) { return m.constructor.of(f(a)); }); } // 从 chain/map 衍生出的 ap X.prototype.ap = function(other) { return this.chain(function(f) { return other.map(f); }); };
(1)定律:
-- 同一律: A.of(id).ap(v) == v
-- 同态:A.of(f).ap(A.of(x)) == A.of(f(x)) 同态就是一个能够保持结构的映射(structure preserving map)。实际上,functor 就是一个在不同范畴间的同态,因为 functor 在经过映射之后保持了原始范畴的结构。
所以,不管是把所有的计算都放在容器里(等式左边),还是先在外面进行计算然后再放到容器里(等式右边),其结果都是一样的。
(2)练习:
require('../../support'); var Task = require('data.task'); var _ = require('ramda'); // fib browser for test var localStorage = {}; // Exercise 1 // ========== // Write a function that add's two possibly null numbers together using Maybe and ap() var ex1 = function(x, y) { return Maybe.of(_.add).ap(Maybe.of(x)).ap(Maybe.of(y)); }; // Exercise 2 // ========== // Rewrite 1 to use liftA2 instead of ap() var ex2 = liftA2(_.add); // Exercise 3 // ========== // Run both getPost(n) and getComments(n) then render the page with both. (the n arg is arbitrary) var makeComments = _.reduce(function(acc, c){ return acc+"<li>"+c+"</li>" }, ""); var render = _.curry(function(p, cs) { return "<div>"+p.title+"</div>"+makeComments(cs); }); var ex3 = Task.of(render).ap(getPost(2)).ap(getComments(2)); // or // var ex3 = liftA2(render, getPost(2), getComments(2)) // Exercise 4 // ========== // Write an IO that gets both player1 and player2 from the cache and starts the game 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; }); var ex4 = liftA2(game, getCache('player1'), getCache('player2'));
ap
的左边还是右边发生 lift 是无关紧要的。