函数式编程学习笔记

/post/functional-programing article cover image

函数式编程的概念、特性及具体应用。

概念

函数式编程(Functinal Programing, FP)是相对于面向过程、面向对象编程的范式之一。

面向对象编程思维:把现实世界中的食物抽象成程序世界中的类和对象,通过封装、继承和多台来演示事物事件的联系。

函数式编程思维:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)。

  • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会设计很多有输入输出的函数。
  • x > f(联系、映射) > y, y = f(x)
  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如: y = sin(x),x和y的关系.
  • 相同的输入始终要得到相同的输出(纯函数)。
  • 函数式编程用来描述数据(函数)之间的映射
js
// 面向过程
let num1 = 1;
let num2 = 2;
let sum = num1 + num2;

// 函数式
function add(n1, n2) {
	return n1 + n2;
}

let sum = add(1, 2);

函数式编程特点: <u>通过细粒度的拆分使得函数复用性更高,再通过组合产生更强大的函数。</u>

在Javascript中的函数式编程: <u>函数作为一等公民,既可以作为参数也可以作为返回值是高阶函数的基础概念。</u>

高阶函数hoc

高阶函数是用来抽象通用的问题,屏蔽实现细节以此达到只需要关注与我们的目标。

js
let arr = [1,2,3];

// 面向过程
for (let i = 0; i < arr.length; i++) {
	// ...
}

// 高级函数
forEach(arr, item => {
	// ...
})

// map、filter、reduce等等

闭包closure

概念:嵌套函数引用外部函数作用域的成员造成的引用捆绑从形成闭包。

本质:函数在执行时会放到一个执行栈上当函数执行完毕后从执行栈删除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

js
// 函数作为返回值

function makeFn() {
    let msg = 'xxx';
    return function() {
    	console.log(msg);
    }
}

// once
function once(fn) {
	let done = false;
	return function() {
		if (!done) {
			done = true;
			return fn.apply(this, arguments);
        }
    }
}

纯函数

相同输入永远会得到相同的输出,没有任何可观察的副作用

js
const arr = [1,2,3,4,5];

// 纯函数
console.log(arr.slice(0, 3))

// 非纯函数
console.log(arr.splice(0, 3))

<u>好处:可缓存、可测试、有利于并行处理</u>

js
// 1.记忆函数:可缓存
function memorize (fn) {
	let cache = {};
	return function() {
      const key = JSON.stringify(arguments);
	  cache[key] = cache[fn] || fn.apply(fn, arguments);
      return cache[key];
    }
}

// 2.可测试
// 纯函数使得单元测试更加方便

// 3.并行处理 Web Worker
// 再多线程环境下并行操作共享的内存数据很可能会出现意外情况
// 纯函数不需要访问共享的内存数据,所以并行环境下可以任意运行纯函数

柯里化haskell-brooks-curry

当一个函数由多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变) 然后返回一个新的函数接受剩余的参数,返回结果

js
function curry (fn) {
	return function curriedFn() {
		const toArray = arrLike => Array.prototype.slice.call(arrLike);
        const innerArgs = toArray(arguments);
        // 判断实参和形参的个数
		if (innerArgs.length < fn.length) {
			return function() {
                return curriedFn.apply(null, innerArgs.concat(toArray(arguments)));
            }
        }
        return fn.apply(fn, arguments);
    }
}

总结:<u> 通过闭包对函数参数进行缓存、将多元函数转成一元函数</u>

组合compose

纯函数和柯里化很容易写出洋葱代码h(g(f(x)))

函数组合可以让我们把细粒度的函数重新组合成一个新的函数

管道:处理数据输入到输出的中间过程被称为管道

组合:把处理数据的管道(函数)链接起来,让数据穿过多个管道形成最终结果

js
// 简易版
function compose (f, g) {
	return function(param) {
		return f(g(param))
    }
}

function reverse (arr) {
	return arr.reverse();
}

function first (arr) {
	return arr[0];
}

const result  = compose(first, reverse);

console.log(result([1, 2, 3]));
js
// 公用版

// ES5
function compose () {
	const fns = Array.from(arguments);
	return function(param) {
		return  fns.reduceRight((acc, cur) => {
		  	return cur(acc);
        }, param)
    }
}

// ES6
const compose = (...fns) => param => fns.reduceRight((acc, cur) => cur(acc), param);

函子functor

函子由以下构成

容器:包含值和值的变形关系(这个变形关系就是函数)

函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法可以运行一个函数对值进行处理(变形关系)

利用函子可以吧副作用控制在可控的范围内、异常处理、异步操作等

js
class Container {
	static of (value) {
		return new Container(value);
    }

    constructor (value) {
		this._value = value;
    }

    map (fn) {
		return Container.of(fn(this._value));
    }
}

let r = Container.of(2)
          .map(x => x + 3)
          .map(x => x * x);

console.log(r);

总结:<u>函数式编程的运算不直接操作值,而是由函子完成,被操作的值为似有变量,只能通过函子暴露出的map方法处理,最终map方法返回一个包含新值的函子。所以不同场景就衍生出不同类型的函子</u>

maybe函子

对外部控制情况做处理的函子(控制副作用在允许的范围)

js
class Maybe {
	static of (value) {
		return new Mabe(value);
    }

    constructor(value) {
		this._value = value;
    }

    map (fn) {
		return Maybe.of(this.isNothing() ? null : fn(this._value));
    }

    isNothing () {
		this. this._value === null || this._value === undefined;
    }
}

// let r = Maybe.of('hello world')
//           .map(x => x.toUpperCase());

// let r = Maybe.of(null)
//           .map(x => x.toUpperCase());


// 对于设置null的情况不好排查和调试
let r = Maybe.of('hello world')
          .map(x => x.toUpperCase())
          .map(x => null)
          .map(x => x.split(' '));

either函子

Either 两者中的任何一个,类似于if...else...的处理 异常会让函数变的不纯,Either函子可以用来做异常处理

js
class Left {
	static of (value) {
		return new Left(value);
    }

    constructor(value) {
		this._value = value;
    }

    map(fn) {
	    return this;
    }
}

class Right {
	static of (value) {
		return new Right(value);
    }

    constructor(value) {
		this._value = value;
    }

    map(fn) {
		return Right.of(fn(this._value));
    }
}

const r1 = Right.of(12).map(x => x + 2);
const r2 = Left.of(12).map(x => x + 2);

console.log(r1, r2); //  14 12

function parseJSON (str) {
	try {
		return Right.of(JSON.parse(str));
    } catch (e) {
		return Left.of({ error: e.message })
    }
}

// 异常
// let r = parseJSON('{ name: zs }');
// console.log(r);

// 正常
let r = parseJSON('{ "name": "zs" }')
        .map(x => x.name.toUpperCase());

console.log(r);

io函子

IO函子中的_value是一个函数,这里是把函数作为值来处理

IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯

把不纯的操作交给调用者来处理

js
const fp = require('lodash/fp');

class IO {
	static of (x) {
		return new IO(function() {
			return x;
        })
    }

    constructor (fn) {
		this._value = fn;
    }

    map (fn) {
		return new IO(fp.flowRight(fn, this._value))
    }
}

const r = IO.of({a:1}).map(obj => console.log(obj));

console.log(r);

folktale库重的task异步执行

js
// 基本用法
const { compose, curry } = require('folktale/core/lamda');
const { toUpper, first } = require('lodash/fp');

let f = curry(2, function (x, y) {
	console.log(x + y);
})

f(3, 4);
f(3)(4);

let f = compose(toUpper, first);
f(['one', 'two']);
js
const fs = require('fs');
const { task } = require('folktale/concurrency/task');
const { split, find } = require('lodash/fp');

function readFile (filename) {
	return task(resolver => {
		fs.readFile(filename, 'utf-8', (err, data) => {
			if (err) resolver.reject(err);

			resolver.resolve(data);
        })
    })
}

readFile('plugins.json')
  .map(split('\n'))
  .map(find(item => item.includes('version')))
  .run()
  .listen({
	  onRejected: err => console.log(err),
	  onResolved: data => console.log(data)
  })

pointer函子

Pointer函子是实现了of静态方法的函子

of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中使用map来处理值)

js
class Container {
	static of (value) {
		return new Container(value);
    }

    // ...
}

Container.of(2).map(x => x + 5);

monad函子

js
// IO函子嵌套问题

const fp = require('lodash/fp');

class IO {
	static of (x) {
		return new IO(function() {
			return x;
		})
	}

	constructor (fn) {
		this._value = fn;
	}

	map (fn) {
		return new IO(fp.flowRight(fn, this._value))
	}
}

let readFile = function (filename) {
	return new IO(function() {
		return fs.readFileSync(filename, 'utf-8');
    })
}

let print = function (x) {
	return new IO(function() {
		console.log(x);
		return x;
    })
}

let car = fp.flowRight(print, readFile);
let r = cat('plugins.json')._value()._value();

console.log(r);

Monad函子是可以变扁的Pointed函子, IO(IO(x))

一个函子如果具有join和of两个方法并遵循一些定律就是一个Monad

js
const fp = require('lodash/fp');

class IO {
	static of (x) {
		return new IO(function() {
			return x;
		})
	}

	constructor (fn) {
		this._value = fn;
	}

	map (fn) {
		return new IO(fp.flowRight(fn, this._value))
	}

	join () {
		return this._value();
    }

    flatMap (fn) {
		return this.map(fn).join();
    }
}

let readFile = function (filename) {
	return new IO(function() {
		return fs.readFileSync(filename, 'utf-8');
    })
}

let print = function (x) {
	return new IO(function() {
		console.log(x);
		return x;
    })
}

let r = cat('plugins.json')
  //      .map(...)
          .flatMap(print)
          .join();

console.log(r);

<u>当一个函子返回一个函子时,且操作步骤较多存在嵌套关系时使用Monad函子</u>