笔记:函数式编程学习

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 在经过映射之后保持了原始范畴的结构。

    所以,不管是把所有的计算都放在容器里(等式左边),还是先在外面进行计算然后再放到容器里(等式右边),其结果都是一样的。

-- 互换:v.ap(A.of(x)) == A.of(function(f){ return f(x) }).ap(v)   选择让函数在 ap 的左边还是右边发生 lift 是无关紧要的。

 (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'));

 

posted @ 2020-04-06 19:18  Marvin_Tang  阅读(459)  评论(0编辑  收藏  举报