03函数组合

本文探讨了函数组合的概念,如何通过Lodash的flow()和flowRight()方法创建数据管道,以及结合律的应用实例。还介绍了PointFree编程风格和各种函数式编程工具,如MayBe、Either和Monad,以及它们在处理空值、异常和IO操作中的作用。
摘要由CSDN通过智能技术生成

函数组合

概念

  • 纯函数和柯里化很容易写出洋葱代码(h(g(f(x))))
获取数组最后一个元素再转换成大写
例:_.toUpper(_.filter(_.reverse(array)))
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

数据管道

fn = compose(f1,f2,f3)
b = fn(a)

​ 函数组合:如果一个函数都要经过过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数;

  • 函数就是数据管道,函数组合就是把这些管道连接起来,让数据通过多个管道形成最终结果;
  • 函数组合都是默认从右往左
函数组合演示
function compose (f,g) {
    return function(value) {
        return f(g(value))
    }
}
// 使用演示:求数组中最后一个元素
// 数组先反转,再获取数组中第一个元素
function reverse (array) {
    return array.reverse()
}
function first(array) {
    return array[0];
}
// 先反转再取第一个
const last = compose(first,reverse);
last([1,2,5,9,7]);

lodash 中组合函数

  • flow()和 flowRight(),都可以组合多个函数
  • flow()从左往右运行
  • flowRight() 从右万往左,使用会多些

_.flowRight()

const _ = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
// 获取数组最后一个元素并转换成大写
const f = _.flowRight(toUpper,first,reverse);
console.log(f(['one','two','three']));
// 模拟实现 lodash 的 flowRight 方法
// 多函数组合
function compose (...fns) {
	return function (value) {
		return fns.reverse().reduce(function (acc, fn) {
			return fn(acc)
		}, value)
	}
}
// ES6
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)

结合律

​ 函数组合要满足结合律,就是既可以把g和h组合,还可以把h和g组合,结果还一样。

let f = compose(f,g,h) 
let assetiative  = compose(compose(f, g), h) == compose(f, compose(g, h))
// 改造
const _ = require('lodash');
const f = _.flowRight(_.toUpper,_.first,_.reverse);
const f = _.flowRight( _.flowRight(_.toUpper,_.first),_.reverse);
console.log(f(['one','two','three']));

函数组合-调试

如果执行结果和预期不一致,如何调试?

const f = _.flowRight(_.toUpper,_.first,_.reverse);
console.log(f(['one','two','three']));
// 方法案例
// NEVER SAY DIE   => never-day-die
const _ = require('lodash');
// _.split()
const split = _.curry((sep,str)=> _.split(str,sep))
// _.toLower()
// _.join()
const join = _curry((sep,array) => _.join(array,sep))
const f = _.flowRight(join('-'),_.toLower,split(' '))
console.log(f('NEVER SAY DIE'));// 结果不符合预期
// 修改调试
// 思路:定义函数将上一个函数结果打印出来
const log = v => {
	console.log(v)
	return v;
}
// 使用
const f = _.flowRight(join('-'),log,_.toLower,log,split(' '));
// 遍历数组每个元素转换成小写,再返回一个新数组
// _.map
const map = _.curry((fn, array) => _.map(array,fn));
const f = _.flowRight(join('-'),map(_.toLower),split(' '));
// 优化打印结果
const trace = _.curry((tag,v) => {
	console.log(tag,v);
	return v
})
// 演示使用
const f = _.flowRight(join('-'),trace('map之后'),map(_.toLower),split(' '));

Lodash 中 FP 模块

  • lodash 提供的实用的对函数式编程友好的方法;
  • 提供了不可变的 auto-currid iteratee-first data-last 的方法;
// 使用
const fp = require('lodash/fp');
// 函数优先,数据置后
// NEVER SAY DIE   => never-day-die
const _ = require('lodash');
const split = _.curry((sep,str)=> _.split(str,sep))
const join = _curry((sep,array) => _.join(array,sep))
const f = _.flowRight(join('-'),_.toLower,split(' '))
console.log(f('NEVER SAY DIE'));
// 改造
const fp = require('lodash/fp');
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));
console.log(f('NEVER SAY DIE'));
// fp 模块中提供方法都是已经柯里化的,可以直接使用

lodash-map 方法的小问题

lodash 和 lodash/fp 模块中 map 方法的区别

// 演示区别案例
// 把字符串数组中所有元素转换成整数
// lodash方法
const _ = require('lodash');
console.log(_.map(['23','8','10'],parseInt))
// 输出[23,NaN,2]
// 分析
// parseInt('23',0,array);
// parseInt('21',1,array);
// parseInt('10',2,array);
// parseInt第二个参数范围是 2-36,所以1不支持所以为NaN
const fp = require('lodash/fp');
console.log(fp.map(parseInt,['23','8','10']))
// fp中map只接受一个参数value
// 输出[23,8,10]
区别就是它所接受的函数的参数不一样:lodash 中是三个,fp 中只有一个就是当前要处理的函数;

Point Free

是一种编程风格,具体实现是函数的组合更抽象。

  • 不需要指明处理数据。
  • 只需要合成过程。
  • 需要定义一些辅助基本运算函数。
案例 1
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello     World'))
案例 2
// 把一个字符串中首字母提取并转换成大写,使用. 作为分隔符
// world wild web => W. W. W. 
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(fp.map(fp.join('. ',fp.map(fp.first),fp.map(fp.toUpper),fp.split(' '));
// 优化两次map问题
const firstLetterToUpper = fp.flowRight(fp.map(fp.join('. ',fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));
console.log(firstLetterToUpper('world wild web'))

函子

为了把函数式编程中的副作用控制在可控范围内,异常处理,异常操作等

Functor 函子

什么是 Functor?

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊容器,通过一个普通对象来实现该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
class Container {
    constructor(value) {
        this._value = value
    }
    
    map (fn) {
        return new Container(fn(this._value))
    }
}
let r = new Container(5).map(x => x+1).map(x => x * x); // 36
console.log(r)
// 优化new
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(5).map(x => x+2).map(x => x * x); // 49
console.log(r)
// 不用取值,可以再map中打印出来

总结:

  • 函数式编程运算不直接操作值,而是由函子完成;
  • 函子就是实现了 map 契约对象(所有函子都有 map 方法);
  • 可以把函子想象成一个盒子,这个盒子里封装了一个值;
  • 想要处理盒子里的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终 map 方法返回一个包含新值的盒子(函子),所以可以.map 进行链式调用

演示 null undefined 问题?需要解决

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(null).map(x => x.toUpperCase); //转化大写
console.log(r) // 报错 空值异常

MayBe 函子

可以处理 null 情况

  • 在编程过程中可能会遇到很多错误,需要对这些错误进行相应处理;
  • MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许范围)
class MayBe {
    staic of (value) {
        return new MayBe(value);
    }
    
    constructor(value) {
        this._value = value;
    }
    
    map(fn) {
        // return MayBe.of(fn(this._value))
        return this.isNoting() ? MeyBe.of(null) : MayBe.of(fn(this._value))
    }
    
    isNoting () {
        return this._value === null || this._value === undefined
    }
}
let r = MayBe.of('hello word').map(x => x.toUpperCase());
console.log(r)
let r = MayBe.of(null).map(x => x.toUpperCase());
console.log(r)
let r = MayBe.of('hello word').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '));
console.log(r)

​ 虽然可以处理空值问题,但是多次调用MayBe,哪次出现空值问题是不太明确的;

Either 函子

解决异常及时提示这个问题

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

需要定义两种类型

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))
    }
}
// 使用
let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)
console.log(r1)
console.log(r2) // 只返回了传入的值 12
// 错误函数
// 这样如果发生异常则直接调用Left输出输入值
function parseJSON(str) {
    try {
        return Right.of(JSON.parse(str));
    } catch(e) {
        return Left.of({error:e.message})
    }
}
// 使用
let r = parseJSON('{name:zs}') // 错误
let r = parseJSON('{"name":"zs"}')  // 正确
console.log(r)
// 最后使用
let r = parseJSON('{"name":"zs"}').map(x => x.name.toUpperCase()) 
总结:Ether 函数可以处理异常,并且返回这个异常信息;

IO 函子

  • IO 函子中的_value 是一个函数,把函数作为值处理
  • 把不纯的动作存储到_value 中,延迟执行这个不纯的操作,包装当前操作
  • 把不纯的操作交给调用者处理
// 演示案例
const fp = require('lodash/fp');
class IO {
    static of(value) {
        return new IO(function() {
        	return value
		})
    }
    constructor(fn) {
    	this._value = fn
	}
	
	map (fn) {
        return new IO(fp.flowRight(fn,this._value))
	}
}
// 使用
let r = IO.of(process).map(p => p.execPath)
console.log(r);
console.log(r._value())

IO 函子问题

const fp = require('lodash/fp');
const fs = require('fs');
class IO {
    static of(value) {
        return new IO(function() {
        	return value
		})
    }
    constructor(fn) {
    	this._value = fn
	}
	
	map (fn) {
        return new IO(fp.flowRight(fn,this._value))
	}
}
// cat 命令:读取文件内容并且把内容打印出来
let readFile = function(filename) {
    return new IO (fucntion() {
    	// 同步读取文件,并把文件内容返回
        return fs.readFileSync(filename,'utf-8')
    })
}
// 打印
let print = fuction (x) {
    return new IO (function () {
    	console.log(x)
    	return x
	})
}
let cat = fp.flowRight(print,readFile)
// IO(IO(x))
let r = cat('package.json')._value()._value();
console.log(r)
// 问题
嵌套函子中的函数不方便,必须._value()._value(),会很麻烦需要改造;
通过使用 Monad 函子来解决这个问题

Folktale 函子

Tak 异步执行,使用 folktale 中的 task 来演示

folktale 中标准的函数式编程库

  • 和 loadash/ramda 不同的是,没有提供很多功能函数
  • 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Ethier、MayBe 等

安装

npm i folktale

使用

const {compose,curry} = require('folktale/core/lambda');
const {toUpper,first} = require('lodash/fp')
// 第一个参数是指传入参数个数,第二个参数是指传入的方法
let f = curry(2,(x,y)=>{
    return x + y
})
console.log(f(1,2))
console.log(f(1)(2))
let f = compose(toUpper, first)
console.log(f(['one','two']))

Task 函子

Task 异步执行

// 读取文件
const fs = require('fs') // node 专门读取文件模块
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.resolver(data)
        })
	})
}
// 解析version
readFile('package.json')
	.map(split('\n'))
	.map(find(x => x.includes('version')))
	.run()
	.listen({
		onRejected: err => {
			console.log(err)
		},
		onResolved: value => {
			console.log(value)
		}
})

Pointed 函子

  • 实现了 of 静态方法函子
  • of 方法为了避免使用 new 来创建对象,更深层含义是 of 用来把值放在上下文 Context(把值放在容器中,使用 map 处理值)
class Container{
	static of (value) {
        return new Container(value)
	}
	...
}
Contaniner.of(2)
	.map(x => x + 5)

Monad 函子

解决 IO 函子嵌套调用麻烦问题

一个具有 join 和 of 两个方法并遵守一定定律就是一个 Monad

const fp = require('lodash/fp');
const fs = require('fs');
class IO {
    static of(value) {
        return new IO(function() {
        	return value
		})
    }
    constructor(fn) {
    	this._value = fn
	}
	
	map (fn) {
        return new IO(fp.flowRight(fn,this._value))
	}
	
	join() {
		// 调用_value,并返回值
		return this._value()
	}
	
	flatMap(fn) {
		// 同时调用join和map
		return this.map(fn).join();
	}
}
// cat 命令:读取文件内容并且把内容打印出来
let readFile = function(filename) {
    return new IO (fucntion() {
    	// 同步读取文件,并把文件内容返回
        return fs.readFileSync(filename,'utf-8')
    })
}
// 打印
let print = fuction (x) {
    return new IO (function () {
    	console.log(x)
    	return x
	})
}
// 使用
let r = readFile('package.json')
				.flatMap(print)
				.join()
console.log(r)
// 读完文件后,处理文件内容,全部转换成大写
let r = readFile('package.json')
				//.map(x => x.toUpperCase())
				.map(fp.toUpper)
				.flatMap(print)
				.join()
// 这样就比IO更具有可读性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值