理解函数式编程

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

一、什么是函数式编程?

函数式编程是一种编程思想,与面向对象编程平级。
面向对象的编程思想是把现实世界的事物抽象成类和对象
函数式编程思想是把运算过程抽象成一个函数
这里的函数不是指代码中的函数,而是数学中的函数,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<arry.length; i++) {
  // 此处对arry进行一些处理
  console.log(arry[i])
}
// 函数式编程

let arry2 = [1,2,3,4]
// lodash内的forEach函数
forEach(arry2, (item)=>{
  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()

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342