1

JS函数式编程


函数式,可能并不是那么难。


在学习JS函数式编程之初,首先要知道在这一“技能叶子”中包含有多少个相关词,其次要知道它和我们是否从未有过遇见。

一等公民、纯函数、柯里化、代码组合、pointfree、命令式与申明式、

Hindley-Milner类型签名、“特百惠”(Container、functor、Maybe、Either)、lift

Monad(pointed functor、chain)、Applicative Functor


接下来,我将根据JS函数式编程说说自己对每个相关词的看法。

一等公民(将函数与数字做平等对待)

    // 数字
    var x = 1;
    var y = x;
    
    // 函数, 不平等对待
    var fx = function(x) {
        return x;
    }
    var fy = function(y) {
        return fx(y);
    }
    
    // 函数,平等对待
    // ...
    var fy = fx;

在编程之初,我们就将函数与其他数据类型从思想上分隔开,但在函数式编程中,我们要摒弃这种思想,将函数和其他数据类型做相等看待

让我们来看看下面这个是否将函数当做一等公民?

    var fz = function(f){
        return fy(function(y){
            return f(y);
        });
    }

上述代码让我们看的很绕是吧,那我们将其转换一下如何?

    var fz = function(f){
        var fx = f;
        return fy(function(y){
            return fx(y)
        })
    }

根据之前的 fy = fx 等式便可知 function(y){return fx(y)} 其实就是等于 fx 的,所以就可以转化成

    var fz = function(f){
        return fy(f);
    }
    // 同上,fz 即等于 fy
    var fz = fy;

于是乎,这就是一等公民的函数。思想切莫先入为主。


纯函数

  • 纯函数是一种函数,即相同的输入,永远得到相同的输出。正如 O = kI + b,IO在数学上的函数关系。(O: 输出 , I: 输入)

  • 函数式编程追求的是纯函数

    var xs = [1,2,3,4,5];
    // 纯
    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]
纯函数的好处

可缓存性、可移植性、可测试性、合理性、并行代码
这些好处都可依据“纯函数不会随外部环境的改变而改变其内部运算逻辑”而得出。


柯里化(curry)

概念:只传递给函数我们需要传递的所有参数的一部分,让它返回一个函数去处理剩下的参数。

    var add = function(x){
        return function(y){
            return x + y;
        }
    }

    var addOne = add(1);
    var addTen = add(10);

    addOne(2);
    // => 3
    addTen(2);
    // => 12 

柯里化很好的体现了纯函数一个输入对应一个输出的概念,柯里化就是每传递一个参数,就返回一个新函数处理剩下的参数


代码组合

组合(compare), 就像工厂流水线一样,将多个函数按顺序拼凑,从右到左 依次加工数据

    var compare = function(f, g){
        return function(x){
            return f(g(x));
        }
    }

下面是一个反转数组取第一个元素得到其首字母并大写的例子

    var reduce = (f) => (arr) => arr.reduce(f)
    var reverse = reduce(function(src, next){return [next].concat(src)}, []);
    var head = (x) => x[0]
    var toUpperCase = (x) => x.toUpperCase();

    // 记住是从右向左
    var f = compare(compare(compare(head,toUpperCase), head), reverse);
    f(["abc","def","ghi"])
    // => G

pointfree就是函数组合,而这些函数包含一等公民与柯里化的概念。


申明式与命令式

作个对比就能知道这两个的区别了

    // 命令式
    var arr1 = [1,2,3,4,5];
    var arr2 = [];
    for(let i = 0; i < arr1.length; i++) {
        arr.push(arr1[i]);
    }

    // 申明式
    arr2 = arr1.map(function(c){return c;})

命令式是那种一步接着一步的执行方式,而申明式是并行运算,正如上述的代码组合的例子一样,如果我们用命令式来写肯定是一句一个逻辑,写起来看起来都很费劲,但是申明式不同,它能让我们只使用一次就可执行多条逻辑,而且可以在不同情况环境下重复使用,这就是纯函数的可移植性
再看一个例子

    // 命令式
    var headToUpperCase = function(str){
        var h = head(str);
        return h.toUpperCase();
    }

    // 声明式
    var headToUpperCase = compare(toUpperCase, head);

Hindley-Milner类型签名

此玩意就是对你构造的函数进行一个说明

    // 例一
    // 下面就是Hindley-Milner类型签名
    // strLength :: String -> Number
    var strLength = function(str){
        return str.length;
    }

    // 例二
    // join :: String -> [String] -> String
    var join = curry(function(what, xs){
        return xs.join(what);
    })
    // curry就是将传入的函数参数转换成curry函数并返回
    // 第一个String指代what, [string]指代xs,第二个string指代return的值


    // 例三
    // concat :: a -> b -> c
    var concat = curry(function(src, next){
        return src.concat(next);
    })
    // a,b可以用任何字母代替,但不能相同,这样表示的是不同的类型,而不是从同一数据中脱离出来,如去数组中的某几个元素组成新的数组

    // 例四
    // map :: (a -> b) -> [a] -> [b]
    var map = curry(function(f, xs){
        return xs.map(f);
    })
    // a -> b 指代 f ; [a] 指代 xs ; [b] 指代 return 的值

    // 例五
    // reduce :: (a -> b -> a) -> b -> [a] -> b
    var reduce  = curry(function(f, x, xs){
        return xs.reduce(f, x)
    })

下面会介绍两个functor(Container, Maybe)

Container

先给源码

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

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

    Container.map = function(f) {
        return Container.of(f(this.__value))
    }

使用Container将我们的值进行包裹
使用Container.of让我们不用写new
使用Container.map让我们在不访问__value的情况下得到容器内部的值并进行运算


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));
    }

Maybe 和 Container其实差不多,唯一的区别在于它出现了对空值的检测,让容器现在能够存储空值了。

Either(Left and Right)
先上源码

    var Left = function(x){
        this.__value = x;
    }
    Left.of = function(x){
        return new Left(x);
    }
    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))
    }

Maybe(null) 一样,当返回一个Left 时就直接让程序短路,但有了Left 至少可以让我们用if 做一次条件判断来知道是什么情况导致输出Left

lift
一个函数在调用的时候,如果被map包裹从一个非functor函数转换为一个functor函数,就叫做lift。这样让普通函数变成可以操作容器的函数,且兼容任意functor。
以下是例子

    var headToUpperCase = map(compare(toUpperCase, head));
    headToUpperCase(Container.of("hello!"));

IO

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

现在this.__value 是一个函数,因而如果执行map(head) 等操作时其实是将这个函数压入一个“执行栈”,而这栈中全部是要执行的函数,就想是代码组合一样,将所有压入的函数延迟执行。而看起来,我们容器的最终形态就是能容纳一个函数。

那么问题就来了,为什么要用容器,而且最好是容纳函数呢?
函数式程序即通过管道把数据在一系列纯函数间传递的程序,而我们之前所有的例子都是关于同步编码的,如果出现异步情况怎么办?如下:

    var fs = require("fs");
    var readFile = function(filename){
        return function(reject, result){
            fs.readFile(filename, 'utf-8', function(reject, result){
                err ? reject(err) : result(data);
            })
        }
    }

ok ,这的确使用了函数式,但异步之后呢,依旧是回调阶梯,所以这么做并没有真正意义上的使用函数式。
我们需要延迟执行,因而我们需要一个类似IO但并非IO的容器类型,由于能力有限,我只能借用Quildreen Motta 所处理的Folktale 里的Data.Task

    var fs = require('fs');
    var readFile = function(filename){
        return new Task(function(reject, result){
            fs.readFile(filename, 'utf-8', function(err, data){
                err ? reject(err) : result(data);
            });
        })
    }
    
    readFile('helloworld').map(split(' ')).map(head).map(toUpperCase).map(head);
    // => Task("H")

Monad

先给例子

    var fs = require('fs');

    // readFile :: String -> IO String
    var readFile = function(filename){
        return new IO(function(){
            return fs.readFileSync(filename, 'utf-8');
        })
    }

    // print :: String -> IO String
    var print = function(x){
        return new IO(function(){
            return x
        })
    }

    var hello = compose(map(print), readFile);
    
    hello('helloworld');
    // => IO(IO("helloworld"))

    // 包了两层IO,于是要想得到值,我们就得执行两次__value
    hello('helloworld').__value().__value();
    // => helloworld

那么如何才能消去这多的层数呢,我们需要使用join

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

    var ioio = IO.of(IO.of("hello"));
    // => IO(IO("hello"))
    ioio.join();
    // => IO("hello")

于是我们在map 之后就要使用 join ,让我们将其叫做chain

    var chain = curry(function(f, m){
        return m.map(f).join();
        // or compose(join, map(f))(m)
    })
    
    // map/join
    var hello = compose(join, map(print), readFile);
    
    // chain
    var hello = compose(chain(print), readFile);

    // 给Maybe也加上chain
    Maybe.of(3).chain(function(three){
        return Maybe.of(2).map(add(three))
    })
    // => Maybe(5);

applicative functor

如下实例

    var add = curry(function(x, y){
        return x + y;
    })
    add(Container.of(2), Container.of(3));
    // 很明显是不能这么进行计算的

    // 但是用chain,我们可以
    Container.of(2).chain(function(two){
        return Container.of(3).map(add(two))
    })

可是这看起来挺费劲的不是吗
于是我们就要使用applicative functor

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

ap 就是一种函数,能够把一个functor的函数值应用到另一个functor的值上。
而根据上述例子,我们可知map 是等价于 of/ap

    F.prototype.map = function(f){
        return this.constructor.of(f).ap(this);
    }

chain 则可以分别得到 functor 和 applicative

    // map
    F.prototype.map = function(){
        var ct = this;
        return ct.chain(function(a){
            return ct.constructor.of(f(a))
        })
    }

    // ap
    F.prototype.ap = function(other){
        return this.chain(function(f){
            return other.map(f);
        })
    }

定律

代码组合的定律

    // 结合律
    var _bool = compose(f, compose(g, h)) == compose(compose(f, g), h);
    // => true

map的组合律

    var _bool = compose(map(f), map(g)) == map(compose(f, g))
    // => true

Monad

    // 结合律
    var _bool = compose(join, map(join)) == compose(join, join)
    // => true

    // 同一律
    compose(join, of) == compose(join, map(of))

    var mcompose = function(f, g){
        return compose(chain(f), chain(g))
    }
    // 左同一律
    mcompose(M, f) == f
    // 右同一律
    mcompose(f, M) == f
    // 结合律
    mcompose(mcompose(f,g), h) == mcompose(f, mcompose(g, h))

Applicative Functor

    var tOfM = compose(Task.of, Maybe.of);
    tOfM('hello').map(concat).ap(tOfM(' world')));
    // => Task(Maybe(hello world))

    // 同一律
    A.of(id).ap(v) == v

    // 同态
    A.of(f).ap(A.of(x)) == A.of(f(x))

    // 互换
    var v = Task.of(reverse)
    var x = 'olleh'
    v.ap(A.of(x)) == A.of(function(f){return f(x)}).ap(v)

    // 组合
    var u = IO.of(toUpper)
    var v = IO.of(concat(" world"))
    var w = IO.of("hello")

    IO.of(compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w))

参考链接

https://www.gitbook.com/book/...


fieryheart
17 声望0 粉丝

Do less, think more.