函数式编程(Functional Programming, FP)

定义

对运算过程抽象, 描述数据(函数)间的映射

  • 一等公民
  • 高阶函数
  • 闭包

高阶函数

抽象可以屏蔽细节,抽象通用的问题

闭包

可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域成员

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

纯函数

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

eg: slice(纯函数)/splice(非纯函数)

特点

函数式编程不会保留计算结果 所以变量是不可变的(无状态的);

可把函数的执行结果交给另一个函数去处理;

lodash(纯函数的代表)

纯函数的好处

  • 可缓存
//记忆函数
const _ = require("lodash");

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

// let getAreaWithMemory = _.memoize(getArea)

function memoize(f) {
    let cache = {}
    return function() {
        let args = JSON.stringify(arguments);
            cache[args] = cache[args] || f.apply(f, arguments)
            return cache[args]
    }
}

let getAreaWithMemory = memoize(getArea)

getAreaWithMemory(4);
getAreaWithMemory(4);
getAreaWithMemory(4); 
  • 可测试

    • 可断言
  • 并行处理

    • 纯函数不需要访问共享的内存数据,可任意运行纯函数

纯函数的副作用

如果函数依赖外部状态,无法保证输出相同

副作用来源:

  • 全局变量

  • 配置文件

  • 数据库

  • 用户输入

  • ....

\color{red}{柯里化}

lodash中的柯里化

_.curry(fn)

  • 功能:创建一个函数,该函数接受一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行结果。否则继续返回该函数并等待接收剩余的参数。
  • 参数:需要柯里化的函数
  • 返回值:柯里化后的函数
    eg: 数组中过滤空字符串
const _ = require("lodash")

const match = _.curry(function (reg, str) {
    return str.match(reg)
})
const haveSpace = match(/\s+/g)
// 数组过滤 ''
const filter = _.curry(fn => arrary => arrary.filter(fn))

const findSpace = filter(haveSpace)
console.log(findSpace(['hello world', 'helloworld']))

实现

function curry(fn) {
    return function curriedFn(...args) {
        //判断实参和形参的个数
        if (args.length < fn.length) {
            return function () {
                //拼接参数
                return curriedFn(...args.concat(...Array.from(arguments)))
            }
        }
        return fn.apply(fn, args)
        //亦或者
        //return fn(...args)
    }
}

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

总结

  • 柯里化\color{red}{可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数}
  • 对函数的”缓存“
  • 函数更灵活,粒度更小
  • 把多元函数转换成一元函数,可以组合使用函数

函数组合

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

  • \color{red}{函数组合默认是从右向左执行}

eg: 取数组的最后一个元素

// 组合函数
function compose (f, g) {
  return function (x) {
    return f(g(x))
} }
function first (arr) {
  return arr[0]
}
function reverse (arr) {
  return arr.reverse()
}
// 从右到左运行
let last = compose(first, reverse) console.log(last([1, 2, 3, 4]))

lodash中的组合函数

_.flowRight
从右到做执行

const _ = require("lodash")

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

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

console.log(fn(["a","b","c","d"]));

//D

模拟实现

const _ = require("lodash")

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

// 模拟实现flowRight
// reduce复习:遍历数组对给定初始值按照传入函数进行处理
//reduce(fn,initValue)接收2个参数。第一个是迭代器函数,函数的作用是对数组中从左到右的每一个元素进行处理。函数有4个参数,分别是accumulator、currentValue、currentIndex、array。
//     accumulator 累加器,即函数上一次调用的返回值。第一次的时候为 initialValue || arr[0]
//     currentValue 数组中函数正在处理的的值。第一次的时候initialValue || arr[1]
//     currentIndex 数组中函数正在处理的的索引
//     array 函数调用的数组
//     initValue reduce 的第二个可选参数,累加器的初始值。没有时,累加器第一次的值为currentValue;
// const compose = function(...args) {
//     return function(value) {
//         return args.reverse().reduce((acc, fn) => {
//             return fn(acc)
//         }, value)
//     }
// }

//箭头函数改造
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const fn = compose(toUpper, first, reverse)

函数组合满足结合律

let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))

调试组合函数

eg:

// NEVER GIVE UP --> never give up
const _ = require("lodash")

const split = _.curry((separator,  str) => _.split(str, separator))

// _.toLower

const join = _.curry((separator, arr) => _.join(arr, separator))

const f = _.flowRight(join('-'),_.toLower,split(' '))

console.log(f('NEVER GIVE UP'))

//n-e-v-e-r-,-g-i-v-e-,-u-p

显而易见目前得到的结果是错误的,那么如何去debug,需要我们结合组合函数的特性去处理,在可能执行错误的函数后面家一个专门用于打印阶段结果的函数,并原封不动的返回传入的值,如下:

const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})
const f = _.flowRight(join('-'),trace("执行 toLower后结果为:"),_.toLower,trace("执行 split后结果为:"),split(' '))

//执行 split后结果为: [ 'NEVER', 'GIVE', 'UP' ]
//执行 toLower后结果为: never,give,up

发现错误的位置,那么发现 _.toLower 的返回值为string, 需要我们去改造

const map = _.curry((fn, arr) => _.map(arr, fn))

const f = _.flowRight(join('-'),map(_.toLower),split(' '))

console.log(f('NEVER GIVE UP'))
// never-give-up

如此得到我们期待的结果

lodash/fp

  • lodash中的FP模块提供了对函数式编程友好的方法
  • 提供了不可变的auto-curried iteratee-first data-last等方法

eg:

const _ = require('lodash')

_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C'] _.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ') // lodash/fp 模块


const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')

lodash/fp 中的map方法

const _ = require("lodash")
console.log(_.map(['23', '8', '10'], parseInt))  // 错误  返回 [ 23, NaN, 2 ]   _.map的第二个参数  iteratee里面有  三个参数  即调用  parseInt("23", 0, array) 而parseInt本身第二个参数为进制
console.log(_.map(['23', '8', '10'], val => parseInt(val)))

// fp模块
const fp = require("lodash/fp")
console.log(fp.map(parseInt, ['23', '8', '10']))

Point Free

point free 是一种风格 手段是函数组合

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
    eg:
// point free
const fp = require("lodash/fp")

// const firstLetterToUpper = fp.flowRight( fp.join(". "),fp.map(fp.first),fp.map(fp.toUpper),fp.split(" "))
const firstLetterToUpper = fp.flowRight( fp.join(". "),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(" "))

console.log(firstLetterToUpper("Hello world"))

可以看到我们将fp.firstfp.toUpper做了一次合并,减少了一次数组遍历

函子 Functor

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

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

    constructor(value) {
        this._value = value
    }

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

let f = Container.of(5).map(x => x + 1).map(x => Math.pow(x, 2))
console.log(f);

这里的 container类就可以看做一个函子,map中传入的fn就用来变形
但是,假如传入函子的值为null undefined的时候如何处理?这时候用到了maybe 函子

MayBe函子

//maybe 函子

class MayBe {

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

    constructor(value) {
        this._value = value
    }

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

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

let r = MayBe.of().map(x => x.toUpperCase())
console.log(r)

MayBe函子可以解决输入值异常的问题,但是假如多次调用map,却无法定位是哪个map阶段出了问题,那么就引出了 Either函子

Either函子

我们实现一个Either函子

//either 函子

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

    constructor(value) {
        this._value = value
    }

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

class Left extends Container {
    map(fn) {
        return this
    }
}

class Right extends Container {
    map(fn) {
        return Right.of(fn(this._value))
    }
}

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

我们调用一下

let r = parseJSON("{key: a}")
// Container { _value: { error: 'Unexpected token k in JSON at position 1' } }

let r = parseJSON('{"key": "a"}')
// Container { _value: { key: 'a' } }

IO函子

  • IO 函子中 _value是一个函数,这里把函数作为值处理
  • IO 函子可以把不纯的操作存储到_value里,延迟执行不纯的操作(惰性执行),只包装纯的操作
  • 把不纯的操作交给调用者来处理
const fp = require("lodash/fp")

class IO {
    static of(x ) {
        return new IO(() => x)
    }
    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._value()) 

r._value() 最后执行了不纯的操作

Task异步执行

folktale 一个标准的函数式编程库

  • lodashramda不同的是,没有提供很多功能函数
  • 只提供了一些函数式的处理操作 eg: compose curry等,函子TaskEitherMayBe

下面提供一个简单的应用: 从文件中读取某些符合条件的信息

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("package.json")
    .map(split("\n"))
    .map(find(x => x.includes("version")))
    .run()
    .listen({
        onRejected: err => console.log(err),
        onResolved: value => console.log(value)
    })

Pointed函子

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

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

Monad函子

Monad函子是包含joinof静态方法的函子,主要用来解决函子嵌套问题

实现

const fs = require('fs')
const fp = require('lodash/fp')
let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8')
    })
}
let print = function (x) {
    return new IO(function () {
        return x
    })
}
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 r = readFile('package.json')
    .map(fp.toUpper)  //当我们想去合并一个函数,如果返回为一个值用 `map`
    .flatMap(print) //如果返回值为一个 函子 用 `flatMap`
    .join()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 拉勾大前端的笔记,仅作为学习记录 课程介绍 为什么学习函数式编程,以及什么是函数编程 函数式编程的特性(纯函数,柯...
    yapingXu阅读 283评论 0 3
  • 文章内容输出来源:拉勾教育大前端高薪训练营 和自我总结 学习函数式编程的意义 1.受React的流行而被人们越来越...
    油菜又矮吹阅读 403评论 0 0
  • #### 函数式编程 #### 函数式编程总结 1. 认识函数式编程 2. 函数复习 (1)函数是一等公民 ...
    爵迹01阅读 614评论 0 1
  • 1. 什么是函数式编程 函数式编程(Functional Programming, FP),FP是编程范式之一,我...
    zxhnext阅读 1,910评论 1 48
  • 作者:酸菜牛肉 文章内容输出来源:拉勾教育大前端高薪训练营课程 函数式编程概念: 函数式...
    酸菜牛肉阅读 565评论 0 1