理解函数式编程

这篇文章是在学习函数式编程时的学习笔记,里面有很多自己对函数式编程的理解,有些理解可能不一定准确,希望对大家学习函数式编程有些参考价值,有不对的地方也请大家指出

一、什么是函数式编程?

函数式编程是一种编程思想,与面向对象编程平级。
面向对象的编程思想是把现实世界的事物抽象成类和对象
函数式编程思想是把运算过程抽象成一个函数
这里的函数不是指代码中的函数,而是数学中的函数,y = sin(x),这里的x和y通过sin函数就绑定了一种映射关系,输入x,通过sin函数的运算(处理)就会得到一个y的值

1、函数是一等公民

函数可以存储在变量中
函数可以作为参数传递
函数可以作为值返回

2、高阶函数

可以把函数作为参数传递给另一个函数
可以把函数作为另一个函数的结果返回

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

3、使用高阶函数的意义

高阶函数屏蔽调了处理细节,只需要关注最终结果
高阶函数用来抽象通用的问题

// 面向过程
let arry = [1,2,3,4]
for (let i=0; i{
  console.log(item)
})

上面的例子中可以看到,使用函数式编程,数据和处理细节是分开的,我们不需要关心如何循环这个数组,只需要传递一个函数对每一项进行处理,常用的高阶函数有:
forEach
map
filter
every
some find/findIndex reduce
sort

4、纯函数

上面的函数始终都会有一个返回值,纯函数指的是相同的输入,始终会有相同的输出
用数组的slice和splice举例

let arr = [1,2,3,4,5,6,7]
// slice是纯函数,每次调用都会返回相同的值
arr.slice(0,2)  //1,2
arr.slice(0,2)  //1,2
// splice是不纯函数,每次调用都会改变原数组,返回的值不一样
arr.splice(0,2) //1,2
arr.splice(0,2) //3,4

纯函数的好处
可缓存:由于纯函数具有相同的输入具有相同的输出的特性,如果一个函数在参数相同的情况下要执行多次,只需把第一次的结果缓存起来,后面调用的时候会直接返回结果。lodash有memoize函数,可直接调用,手动实现原理如下:

function getArea (r) {
  console.log(r)
  return Math.PI * r * r
}
function memoize (fn) {
  let catchObj = {}
  return function () {
    let key = JSON.stringify(arguments)
    catchObj[key] = catchObj[key] || fn.apply(fn, arguments)
    return catchObj[key]
  }
}
const getAreaMemory = memoize(getArea)
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))

上面代码中,后面即使多次调用getAreaMemory方法,也不会执行Math.PI * r * r的运算

可测试:由于纯函数都有返回值,且参数不变,返回值统一,便于写测试用例
并行处理:由于纯函数不会操作共享的内存,所以在并行处理的时候不会产生冲突
副作用
先看下面的例子

//非纯函数
let min = 20;
function compare (num) {
  return num >= min
}

上面代码中,函数的返回值依赖外部的min的值,相同的输入不一定有相同的输出,为了解决这个问题,有两个方案;一个是将变量min放在函数内部

function compare (num) {
  let min = 20;
  return num >= min
}

还有一个就是函数的柯里化

function compare (min) {
  return funciton (num) {
    return num >= min
  }
}

5、函数柯里化

我对函数柯里化的理解是,一个函数接受很多参数,先传递部分参数调用,返回一个函数接受另一部分参数,最终返回结果,例如loadsh中的柯里化函数

const _ = require('lodash') // 要柯里化的函数
function getSum (a, b, c) {
  return a + b + c
}
// 柯里化后的函数
let curried = _.curry(getSum) // 测试
curried(1, 2, 3) curried(1)(2)(3)
curried(1, 2)(3)

手动实现柯里化函数如下:

const curry = function (func) {
  return function curriedFn (...args) {  
    if (args.length < func.length) {
      return function (...args2) {
        return curriedFn(...args.concat(args2))
      }
    } else {
      return func(...args)
    }
  }
}

函数柯里化的意义:
使函数参数缓存
让函数的粒度更小
使多元函数变为一元函数,组合这些一元函数产生强大的功能

6、函数组合

洋葱代码:一个函数的入参依赖另一个函数的出参,函数一层套一层
例如:fn1(fn2(fn3()))
函数组合可以规避这个问题的发生,把fn1,fn2,fn3想象成一个三段小管道,将这三个小管道拼接起来,最终组装成一个大管道,组装后的大管道就是一个新函数,这个函数接受入参,依次经过fn1,fn2,fn3的处理,返回最终的结果,组装的过程就是函数组合,lodash内置函数组合函数,例如将一个数组先翻转,然后提取数组第一项,最后将第一项转为大写:

const _ = require('lodash')

const reverse = value => value.reverse()
const toUper = value => value.toUpperCase()
const  first = value => value[0]

const fn = _.flowRight(toUper, first, reverse);
console.log(fn(['one', 'two', 'three']))     //THREE

flowRight是lodash内置的函数组合函数,内部实现原理如下:

const reverse = value => value.reverse()
const toUper = value => value.toUpperCase()
const  first = value => value[0]
function compose (...fnLists) {
  return function (value) {
    return fnLists.reverse().reduce((acc, fn)=>{
      return fn(acc)
    }, value)
  }
}
const fn2 = compose(toUper, first, reverse);
console.log(fn2(['one', 'two', 'three'])) //THREE

函数的组合要满足结合律

7、Point Free

Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数

let fp = require('lodash/fp')
// web world with --> W. W. W.
const firstLetterToupper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))

console.log(firstLetterToupper('web world with'))

8、函子

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

我理解的容器就是把函数想象成一个容器盒子,入参一个数值,经过该容器盒子的处理,返回一个最终的结果,而函子是具有map、of方法的容器,通过of方法入参一个值,通过map方法传入对这个值的处理函数,map方法执行后会返回一个包含新值的函子,可以继续调用map方法对上一map方法返回的值进行处理,直到得到最终想要的结果,下面是一个例子:

// 函子
class Container {
  static of (value) {
    return new Container(value)
  }
  constructor (value) {
    this._value = value
  }
  map (fn) {
    return Container.of(fn(this._value))
  }
}

const r = Container.of(5)
          .map((x)=>x+1)

console.log(r)

调用of方法省去了new Container这一步,执行of方法后,参数5的这个值就被存储到了对象内部_value上,调用map方法时,会将该值当做入参传递给map的入参函数内

函子有很多种
处理空值的MayBe 函子

// maybe函子
class Maybe {
  static of (val) {
    return new Maybe(val)
  }
  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
  }
}
const r2 = Maybe.of(5).map(x=>x+1)
const r3 = Maybe.of('hello').map(x=>x+'1').map(x=>null).map(x=>x.split(' '))
console.log(r2,r3) //Maybe { _value: 6 } Maybe { _value: null }

处理异常的either函子

// eithier函子
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))
  }
}
function parseStr (str) {
  try {
    return Right.of(JSON.parse(str))
  } catch (err) {
    return Left.of({error: err.message})
  }
}
const r4 = parseStr('{ "name": "zs" }').map(x=>x.name.toUpperCase())
console.log(r4)

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

const fp = require('lodash/fp')
// IO函子
class IO {
  static of (value) {
    return new IO(()=>{
      return value
    })
  }
  constructor (fn) {
    this._value = fn
  }
  map (fn) {
    return new IO(fp.flowRight(fn, this._value))
  }
}
const _io = IO.of(process).map(p=>p.execPath).map(k=>{
  return k.toUpperCase()
})
console.log(_io._value())

这个例子中,_io并不是一个执行结果,而是一个拥有执行函数的IO函子,操作者手动调用_value()时,函数才会执行
Monad(单子)
在使用 IO 函子的时候,如果我们写出如下代码:

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() {
    console.log(x)
return x })
}
// IO(IO(x))
let cat = fp.flowRight(print, readFile)
// 调用
let r = cat('package.json')._value()._value() 
console.log(r)

此时readFile返回的IO函子会作为入参传递给print,print返回一个IO函子

Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad

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

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

function readFile (fileNmae) {
  return new IO(()=>{
    return fs.readFileSync(fileNmae, 'utf-8')
  })
}

function print (x) {
  return new IO(()=>{
    return x
  })
}

const r = readFile('package.json')
          .map(fp.toUpper)
          .flatMap(print)
          .join()
console.log(r)

通过内部的join方法,多调用了一次_value()

函子这一部分有些抽象,理解的不够深入,可能在实际开发过程中会有不一样的体会,大家共勉

你可能感兴趣的:(理解函数式编程)