Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数式编程(functional programming) #1

Open
janeLLLL opened this issue Aug 18, 2020 · 0 comments
Open

函数式编程(functional programming) #1

janeLLLL opened this issue Aug 18, 2020 · 0 comments

Comments

@janeLLLL
Copy link
Owner

janeLLLL commented Aug 18, 2020

函数式编程

一、介绍

functional programming , FP是编程范式之一,编程范式也包括面向过程编程、面向对象编程

面向对象:把事物抽象成程序中的类和对象,通过封装、继承和多态演示事物联系(抽象事物)
函数式编程:把事物和事物之间的联系抽象到程序世界(抽象运算)
程序本质 :根据输入通过某种运算获得相应的输出
函数式编程中函数指的不是程序中的函数/方法,而是数学中的函数即映射关系:y = sin(x),y和x的关系
相同的输入始终要得到相同的输出(纯函数)
作用:函数式编程就是用来描述数据之间的映射

function add (n1, n2) {
    return n1 + n2
}
let sum = add(2, 3)
console.log(sum)

相同输入-》相同输出
优点:代码重用

特点:
Vue3、React框架
可以抛弃this
打包过程中可以更好地利用tree shaking过滤无用代码
方便测试、方便并行处理
函数式开发库:lodash、underscore、ramda


二、函数式的特性

Ⅰ、函数是一等公民 first-class function
1.函数可以存储在变量中
//把函数赋值给变量

let fn = function(){
    console.log('hello')
}
fu()

2.函数作为参数(高阶函数)
3.函数作为返回值(高阶函数)

Ⅱ、高阶函数 higher-oder function
1.可以把函数作为参数传递给另一个函数
优点:灵活、不需要考虑函数内部如何实现
2.可以把函数作为另一个函数的返回值

function makeFn(){
    let msg = 'hello fucntion'
    return function(){
        console.log(msg)
    }
}

makeFn()()//返回结果是一个函数


function once(fn){
    return function(){
            done = true
            //控制只执行一次
            return fn.apply(this,arguments)
            //拷贝了fn的属性和方法,实现了类的继承
            //apply(): this 指当前对象,也就是正在调用这个函数的对象。 使用 apply, 你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法
            //等价于:return fn(...arguments)
    }
}

let pay = once(function(money){
    console.log(`支付:${money}RMB`)
})

pay(5)

意义:
抽象可以屏蔽细节,只需要关心目标(封装内容,使用时并不需要关注怎么实现的)
高阶函数用来抽象通用的问题‘

常用的高阶函数:
foreach map filter every some find/findindex/reduce/sort...

const map = (array,fn) => {
    let results = []
    for(let value of array){
        results.push(fn(value))//让fn处理数组里的每一个元素,也就是v*v
    }
    return results
}

//test
let arr = [1,2,3,4]
//求平方
arr = map(arr, v => v * v)
console.log(arr)



const every = (array, fn) => {
    let result = true
    for(let value of array) {
        result = fn(value)
        //fn()返回bool类型值
        if(!result){
            break
        }
    }
    return result
    //判断数组中每个元素是否满足某条件(fn内容)
}

let arr = [11,12,14]
let r = every(arr, v => v > 10)



const some = (array,fn) => {
    let result = false
    for(let value of array){
        result = fn(value)
        if(result){
            break
        }
    }
    return result
}

let arr = [1,2,4,9]
let r = some(arr, v => v & 2 === 0)
console.log(r)

//r = true
//数组里还是有一偶数的

总结:可以让函数变得更加灵活

Ⅲ、闭包(closure)
定义:函数和周围的状态(词法环境)的引用捆绑在一起形成闭包
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

//函数作为返回值
function makeFn(){
    let msg = 'hello'//外部变量
    return function(){
        console.log(msg)//内部函数,虽然函数已经从执行栈上释放,但是msg没有从内存移除
    }
}

const fn = makeFn()
fn()

//作用域调用makeFn()函数的时候,访问到makeFn()内部的成员msg,这样就形成了闭包,
//延长了外部变量的作用范围,不会及时进行内存释放

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


三、纯函数

Ⅰ、介绍
“纯函数”是相对于函数式编程的概念。即,相同的输入永远会得到相同的输出,且没有副作用。
类似于数学中的函数,映射关系y = f(x)

功能库:lodash 提供了对数组,数字,对象,字符串,函数等一些方法
纯函数:slice,截取数组元素。返回数组中的指定部分,不会改变原数组(相同的输入得到相同的输出)
不纯的函数:splice,对数组操作返回该数组,会改变原数组(会移除数组中的元素)

纯函数必须有输入和输出!
函数式编程不会保留计算中间的结果,所以变量不可变(无状态的)
可以把一个函数的执行结果交给另一个函数处理

npm i -y
npm i loadsh
const _ = require('loadsh')

Ⅱ、优势
可缓存:因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来。

const _ = require('loadsh')

function getArea(r){
    return Math.PI  * r *r
}

let getAreaWithMemory = _.memoize(getArea)
//使用loadsh自带的记忆函数memoize()对已有函数进行缓存操作
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
//多次调用过程中,实际上只有在第一次执行了函数调用操作,之后的都是使用缓存


//模拟memoize方法的实现
//他是一个函数,也需要返回一个函数
function memoize(f){
    let cache = {}
    //缓存
    return function(){
        let key = JSON.stringify(arguments)
        //arguments是一个伪数组,需要转换成字符串,作为一个键
        cache[key] = cache[key] || f.apply(f, arguments)
        //apply的第二个参数可以把一个伪数组或者数组展开,分别传给f
        return cache[key]
    }
}

可测试
我们只需要给函数一个输入,然后只看输出结果就可以了
并行处理

  1. 在多线程环境下并行操作共享的内存数据可能会出现意外情况
  2. 纯函数不需要访问共享的内存数据,在并行环境下可以任意运行纯函数(ES6新增了Web Worker是多线程的,但是JavaScript还是单线程的)
    纯函数只依赖于参数,是一个封闭的空间

Ⅲ、副作用
函数依赖外部作用来决定输出结果,就会带来副作用

let mini = 18
function checkAge(age){
    return age >= mini
}
//因为全局变量是可改变的,所以并不是纯函数

//改造(硬编码,可以通过柯里化解决)
function checkAge(age){
    let mini = 18
    return age >= mini
}

来源:配置文件、数据库、获取用户的输入……
缺点:
使得方法通用性下降,不适合扩展和可重用性
给程序带来安全隐患和不确定性
但不可能完全禁止,尽可能控制它们在可控范围内发生


四、柯里化(Haskell Brooks Curry)

//普通纯函数,不再依赖外部变量,也没有硬编码
function checkAge(mini, age){
    return age >= mini
}

console.log(checkAge(18,20))
console.log(checkAge(18,22))
console.log(checkAge(18,23))
//18经常使用的话,就可以使用闭包避免18重复

function checkAge(mini){
    return function(age){
        return age >= mini
    }
}

let checkAge18 = checkAge(18) //返回一个新的函数,新的基准值就是18
let checkAge20 = checkAge(20)

console.log(checkAge18(20))
console.log(checkAge18(22))
console.log(checkAge18(23))

//这样就是函数的柯里化!

柯里化(currying):
调用一个函数只传递部分的参数,并且返回新的函数,新函数去接受剩余的参数,并且返回相应的结果。

//函数的柯里化,ES6箭头函数
let checkAge = mini => (age => age >= mini)
//箭头函数如果只有一句代码的话相当于直接返回结果

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

Ⅱ、Lodash里的柯里化:_.curry(func)
//本身就是一个纯函数
功能:该函数接收一个或者多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数
返回值:柯里化后的函数

const _ = require('loadsh')

function getSum(a,b,c){
    return a+b+c
}
//三元函数,柯里化可以把多元函数转换成一元函数

const curried = _.curry(getSum)

console.log(curried(1,2,3))

console.log(curried(1)(2,3))

console.log(curried(1,2)(3))
//返回一个函数等待接收新的参数,此时它就变成了一个一元函数

Ⅲ、柯里化示例
生成提取字符串空格(某个特定正则规则)的函数

const _ = require('loadsh')

const match =  _.curry(function (reg, str){
    return str.match(reg)
})//传递一个匿名函数

const haveSpace = match(/\s+/g)
//先传递一个参数,返回一个等待剩余参数的函数

console.log(haveSpace('heelooo  sffd'))
//输出的是['   ']

这样就可以最大程度地重用函数

Ⅳ、总结:
让一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数
对函数参数的一种"缓存"(闭包)
函数灵活、粒度小(组合函数)
多元函数转换成一元函数,可以组合使用函数


五、函数组合

纯函数和柯里化容易写出洋葱代码h(g(f(x)))
函数组合可以让我们把细粒度的函数重新组合成一个新的函数
Ⅰ、管道

fn = compose(f1,f2,f3)//不需要考虑中间数据
b = fn(a)

Ⅱ、概念
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
函数组合默认是从右到左执行

function compose(f, g){
    //返回的函数,需要接收一个参数value.它相当于一个管道,需要输入n个参数,并且处理输出1个结果
    return function(value){
        return f(g(value))
        //先右到左处理函数
    }
}

//求数组最后一个元素
function reverse(array){
    return array.reverse()
}

function first(array){
    return array[0]
}

const last = compose(first,reverse)

console.log(last([1,2,3,4]))

Ⅲ、loadsh中的组合函数
_.flow()从左到右运行
_.flowRight()从右到左运行

const _ = require('loadsh')

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()

const f = _.flowRight(toUpper,first,reverse)
//从右到左:翻转、取1、转换大写

Ⅳ、满足结合律
即,可以把g和f结合,还可以把f和g结合,结果应一样

//结合律 (associativty)
let f = compose(f,g,h)
let associative = compose(compose(f,g),h) == compose(f,compose(g,h))
//true

const f = _.flowRight(_.toUpper,_.first,_.reverse)
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)

注意,输出结果和fllowRight内部函数运行顺序有关!

Ⅴ、调试

const _ = require('loadsh')
//NEVER SAY DIE --> never-say-die

// _.split() 拆分字符串为数组
const split = _.curry((sep,str) => _.split(str, sep))
//sep分隔符

// _.toLower()

//_.join() 转字符串
const join = _.curry((sep, array) => _.join(array,sep))

//_.map() 迭代遍历处理数组或对象元素
const map = _.curry((fn,array) => _.map(array,fn))

const trace = _.curry((tag,v) => {
    console.log(tag,v)
    return v
})

const f = _.flowRight(join('-'),trace('map 之后'),map(_.toLower),trace('split 之后'), split(' '))
//封装起来就变成了一元函数

console.log(f('NEVER SAY DIE'))

/**
split 之后 [ 'NEVER', 'SAY', 'DIE' ]
map 之后 [ 'never', 'say', 'die' ]
never-say-die
**/

六、loadsh/fp

Ⅰ、介绍
不可变的,auto-curried iteratee-first data-last的方法

const _ = require('loadsh/fp')

//loadsh模块-数据优先 函数置后
_.split('hello',' ')

//loadsh/fp模块-函数优先 数据置后
fp.map(fp.toUpper,['a','b','c'])
fp.split(' ','hello world')
//如果只传入一个数据,要是返回的也是一个函数,说明它是被柯里化后的函数


const fp = require('loadsh/fp')
//eg:
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), sp.split(' '))
console.log(f('NEVER SAY DIE'))

Ⅱ、map方法的区别

const _ = require('loadsh')

let x = _.map(['23','8','10'], parseInt)
//第二个参数是遍历数组的每个函数的时候,把数组中的每个元素传递给此函数去处理

console.log(x)
//parseInt('23',0,array)
//parseInt('8',1,array)
//parseInt('10',2,array)
//接收的数据、索引、数组

const fp = require('loadsh/fp')

console.log(fp.map(parseInt, ['23', '8' ,'10']))
//map的函数接收的数据只有一个数据

//两个map接收的函数的参数不一样

七、Point Free

一种编程的风格,具体实现是函数的组合,更抽象
Ⅰ、介绍
把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合在一起(函数的组合),在使用此模式之前我们需要定义一些辅助的基本运算函数
不需要指明处理的数据
只需要合成运算的过程
需要定义一些辅助的基本运算函数

const fp = require('loadsh/fp')

const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)

console.log(f('Hello    world'))

Ⅱ、案例

//自己写的

const fp = require('loadsh/fp')
const _ = require('loadsh')
//world wild web ==> W. W. W
/**
 * 1.字符串转换为数组
 * 2.将数组转换为大写
 * 3.每个元素取第一个元素
 * 4.将数组转换为字符串,并且在间隔替换为'. '
 */

const trace = _.curry((tag, v) => {
    console.log(tag,v)
    return v
})

 const f = fp.flowRight(fp.join('. '), trace('map 之后'), fp.map(fp.first), trace('toUpper 之后'), fp.map(fp.toUpper), trace('split 之后'), fp.split(' '))

 console.log(f('world wild web'))
 
 //问题:使用了两次map循环了两次,性能低
 
 const f = fp.flowRight(fp.join('. '), trace('map 之后'), 
    fp.map(fp.flowRight( fp.first, fp.toUpper)), trace('split 之后'), fp.split(' '))
//在map内部嵌套了组合函数,满足函数组合的结合律

八、函子 (Functor)

Ⅰ、介绍
通过函控制副作用,也可以进行异常处理、异步操作等
容器:包含值和值的变形关系
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

和面向对象的编程有何区别????
函数式编程一般约定,函子有一个of方法,用来生成新的容器

// 函子
class Container{
    constructor(value){
        this._value = value
    }
    //构造函数

    map(fn){
        return new Container(fn(this._value))
        //调用fn去处理内部值,然后传给一个新的对象,返回一个新的函子对象
    }
}

let r = new Container(5)
    .map(x => x + 1)//返回一个新的函子
    .map(x => x * x)//

console.log(r)


//=======================使用of改造函子更函数式编程=======================

class Container{
    static of (value) {
        return new Container(value)
    }
    //避免进行new,将new封装到静态方法里

    constructor(value){
        this._value = value
    }
    //构造函数

    map(fn){
        console.log(fn(this._value))
        return Container.of(fn(this._value))
        //调用fn去处理内部值,然后传给一个新的对象,返回一个新的函子对象
    }
}

let r = Container.of(5)
    .map(x => x + 2)
    .map(x => x * x)
//可以不停地点map,链式编程

console.log(r)
//r并不是一个值,而是一个函子对象,值在函子对象里
//我们不碰这个值,而是通过函子里的方法去使用它,也可以在方法里打印它

Ⅱ、总结
函数式编程的运算不直接操作值,而是由函子完成
函子就是一个实现了map契约的对象
函子就是一个盒子,盒子封装了一个值
处理盒子的值,需要给map方法传递一个处理值得函数(纯函数)
最终map方法会返回一个包含新值的盒子(函子)

Ⅲ、MayBe函子——函子处理空值问题

Container.of(null)
    .map(x => x.toUpperCase())
//null就是副作用


//===================改进map,先判断是否是空值,再返回对应值================
map(fn){
        return this._value ? MayBe.of(fn( this._value)) : MayBe.of(null)
        }
    
    //但仍旧存在问题
    let r = MayBe.of(5)
        .map(x => x + 1)
        .map(x => null)
        .map(x => x.split(' '))
        //此时r输出仍是为null

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

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

class Left{
    constructor(value){
        this._value = value
    }

    static of(value){
        return new Left(value)
    }

    map(fn){
        return this
    }
    //直接返回对象,没有调用fn
}

class Right{
    constructor(value){
        this._value = value
    }

    static of(value){
        return new Right(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,r2)

function parseJSON(str){
    try{
        return Right.of(JSON.parse(str))
        //字符串转换成JSON对象,传递给函子
    }catch(e){
        return Left.of({ error:e.message })
        //传给Left一个错误的信息
        //对于纯函数来说,相同的函数,始终要有相同的输入和输出,所以也要返回一个函子
    }
}

let r = parseJSON('{name:zs}')//解析会出错
console.log(r)

let r1 = parseJSON('{"name":"zs"}')//解析会出错
    .map(x => x.name.toUpperCase())
console.log(r1)

Ⅴ、IO函子
IO函子中的_value是一个函数,这里是把函数作为值来处理
IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
把不纯的操作交给调用者来处理

const fp = require('loadsh/fp')

class IO{
    constructor(fn){
        this._value = fn
    }

    static of(x){
        return new IO(function(){
            return x
        })
    }//接收一个数据,返回一个新的IO函子

    map(fn){
        return new IO(fp.flowRight(fn,this._value))
        //此次调用IO的构造函数,传递的是一个value和传入函数的组合函数
    }
}

//调用
let r = IO.of(process).map(p => p.execPath)

console.log(r)
console.log(r._value,'_value是一个函数')
/**
 * 1.调用of的时候,传入一个对象process
 * 2.通过of,将process包装到类里
 * 3.调用map方法,p=>p.execPath 和 process作为一个组合函数被IO构造函数调用
 * 4.返回一个新函子 IO { _value: [Function (anonymous)] }
 * 5.map传递的函数可以纯可以不纯
 * 6.IO函数把不纯的操作延迟到调用的时候,即 r._value
 */

Ⅵ、folktale
一、Task异步执行
folktale一个标准的函数式编程库
和lodash、ramda不同的是,他没有提供很多功能函数
只提供了一些函数式处理的操作,例如:compose、curry等,一些函子Task、Either、MayBe等

演示compose和curry:

const {compose , curry} = require('folktale/core/lambda')

const {toUpper, first} = require('loadsh/fp')

let f = curry(2,(x,y) =>{
    return x + y
})

console.log(f(1)(2))

let fn = compose(toUpper,first)
console.log(fn(['one','two']))

二、Task函子(处理异步任务)

const {task} = require('folktale/concurrency/task')
const fs = require('fs')
//node自带的文件系统
const { resolve } = require('path')

//读取文件函数,filename是绝对路径
function readFile (filename){
    return task(resolver => {
        fs.readFile(filename , 'utf-8' , (err,data) => {
            if(err) resolver.reject(err)

            resolver.resolve(data)
        })
        /**
         * 1.readFile是异步执行读取文件
         * 2.resolver提供了两个方法resolver.reject失败 ,resolver.resolve成功
         * 3.task()返回值是一个Task的对象
        */
    })
}

readFile('package.json')
    .map(split('\n'))//清除换行
    .map(find(x => x.indludes('version')))
    .run()
    .listen({
        onRejected:err => {
            console.log(err)
        },
        conResolved:value => {
            console.log(value)
        }
    })

Ⅶ、Pointed函子(概念,一直在用)
Pointed函子是实现了of静态方法的函子
of方法是为了避免使用new来创建对象,of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)

class Container{
    static of (value){
        return new Container(value)
        //返回的就是一个上下文
    }
}

Ⅷ、Monad(单子)
可以延迟函数的使用,把纯函数借助IO函子封装起来,避免副作用

const fp = require('loadsh/fp')
const fs = require('fs')

class IO{
    constructor(fn){
        this._value = fn
    }
    static of(x){
        return new IO(function(){
            return x
        })
    }//接收一个数据,返回一个新的IO函子
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
        //此次调用IO的构造函数,传递的是一个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 
        //x拿到的是readFile返回的函子
    })
}
let cat = fp.flowRight(print,readFile)
/**
 * 1.readFile调用了IO的构造对象,返回一个IO函子
 * 2.此函子传递给print,又去返回一个IO函子
 * 3.cat里就是一个IO(IO(x))
 */
let r = cat('package.json')
//r还没有执行读取文件,延迟执行
let r1 = cat('package.json')._value()
//此时r1调用的是print里的function
let r2 = cat('package.json')._value()._value()
//此时r2调用的就是readFile里的function

Ⅸ、Monad函子
Monad函子是可以变扁的Pointed函子,IO(IO(x)) 解决函子嵌套的问题(函数嵌套就用组合函数解决)
一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad


class IO{
    ...
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
        //此次调用IO的构造函数,传递的是一个value和传入函数的组合函数
    }
    join(){
        return this._value()
    }
    //其实就是在IO函子里添加一个join方法,可以直接返回内部的_value()
    flatMap(fn){
        return this.map(fn).join()
    }
    //联合调用map和join
}

    ...

let r = readFile('package.json')
    //.map(x => x.toUpperCase())
    .map(fp.toUpper)
    //map方法是处理函子内部的值
    .flatMap(print)
    .join()

/**flatMap:
 * readFile返回的函子去调用flatMap
 * flatMap接收的函数则是print
 * flatMap调用了map方式,此时map里的_value就是readFile返回的函子
 * map的作用就是将print方法和readFile返回的函子进行合并,组成一个新的函子
 * .join()的作用就是返回map方法返回的函子的value、
 * 所以最后flatMap返回的则是print返回的函子
 */
 
@janeLLLL janeLLLL changed the title 函数式编程(functional programming)-基础概念 函数式编程(functional programming) Aug 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant