函数式编程学习笔记

310 阅读13分钟
首先立个flag,今后的开发要把函数式编程的思想运用到日常开发中

    然后第一次写博客,格式什么的也不懂,哈哈哈哈。

写在开头

    本文内容呢主要来源于自己在拉勾教育前端高薪训练营的学习,虽然交钱的时候有些心痛,但确确实实能学到东西啊,函数式编程自己之前也去了解过,在19年的上班地铁上,抱着那个公众号文章啃啊,学啊,结果学了个寂寞。这次老老实实跟了拉勾的课程吧,总感觉有那么一种茅塞顿开,恍然大悟的感觉,觉得这个函数式编程好像确实可以用到自己的项目里面(vue),之后会再去看看之前的那篇文章,但是呢因为刚刚有这么一种思想,再加对vue还有一些困惑,所以肯定不是最佳实践,甚至一开始写出来的都不是纯函数,总之慢慢努力吧。现状虽然很艰难,但找到出口的方向,就只管埋头前进就好。

    读到此文的有缘人,愿我们都有光明的未来。

什么是函数式编程

函数式编程的个人理解

    对于俺来说,学习函数式编程最重要的是摈弃自我认知里函数式编程,在开课前俺还憨憨地认为函数式编程就是用函数来编程,这种错误认知真的害人不浅。如果一直这样认为,可能得写一辈子的面向过程编程。

    要学习函数式编程,首先最重要的是清楚什么是函数式编程的函数不是程序中的函数(方法),而是数学中的函数即映射关系!。比如说y=sin(x)的sin()。

    程序说到底就是将输入转变成希望得到输出的工具。如果我们可以把现实世界的事物和事物之间的联系抽象到程序世界,把输入到输出这期间的运算过程抽象出来,我们就培养出了函数式编程的思维了。下面通过简单的代码来说明函数式和非函数式的区别:

   const num1 = 1;
   const numb2 = 2;
   //实现两个数求和
   //非函数式
   cons sum1 = num1 + numb2;
   // 函数式
   function add(a, b) {
       return a + b;
   }
   const sum2 = add(num1, num2)

上面代码因为运算过程比较简单,所以有点难体会出函数式编程有啥好处,去深入一点想的话,就可以有一种“函数编程好啊”的feel了。面向过程编程的话,我们每一次进行输入到输出的运算都要写一堆过程balabala,但是当我们把这一堆balabala抽象出来封装成函数,之后的运算我们只需要把值传给这个函数就能够得到对应的输出了。这么说来,是不是感觉函数式编程比起面向过程编程要优雅很多

    所以对于什么是函数式编程,个人根据老师的讲课内容,做了以下两点总结: 1、函数式编程用来描述数据(函数)之间的映射。2、函数式编程的最大好处:最大程度地重用定义好的函数(复用性)。

下周一咱就打算重构一下自己的祖传代码,看看有哪些地方可以用函数编程优化一下,希望只是重构不是重写吧,哈哈哈哈哈。

纯函数

    除了上述的个人理解还有一个很重要的概念需要死记硬背一下,那就是纯函数:相同的输入始终要得到相同的输出,而且没有任何可观察的副作用。。 定义很简洁,但要我判断一个函数是不是纯函数还是很艰难的,因为在我看来相同输入得到相同输出不是理所应当,天经地义的吗,这咋个会有不纯的函数?但是后来的学习狠狠打了我的脸,再仔细一想自己平常开发的代码其实很多都是不纯的函数,举个🌰:

function getUserName (userId) {
   axios.get('getUserName', , { params: { userId } })
       .then(res => res)
       .catch(error => error)
}

初看上面这个getUserName函数,我们理所当然地认为每一次给一样的userId自然会get到一样的userName,但是其实它是不纯的,我们根本没依据证明相同的输入始终要得到相同的输出这一点,因为可能会因为用户改了名字导致数据库里面这条数据的userName变化,同样的userId获得的userName不同,还可能因为网络原因返回的是个error......所以如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用(只要有和外部的交互就可能变得不纯)。而这个副作用无法完全消除,我们只能尽可能地写纯函数,将不纯的部分都收集在一起。那哪些是纯函数呢,比如数组的slice等方法。

纯函数给我们带来的好处主要有下面三条:

  1. 可缓存,因为只和输入有关,可以借助lodash中的memoize进行缓存
  2. 方便测试
  3. 并行处理

函数知识点

函数是一等公民

    函数是一等公民,这个真的听起来很高大上,但其实就是想说函数是对象,他可以赋值给对象,可以作为参数,可以作为返回值,这是函数式编程的基础。关于函数是对象这一点,大家可以在浏览器控制台执行下function(){} instanceof Object,也可以看下面的原型图去理解。 image.png

高阶函数

高阶函数指的是:

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

    高阶函数对于函数式编程来说是很重要的一点。基本上函数编程都是依赖高阶函数来实现的。为什么这么说呢?大家都使用过数组的map,forEach,filter等函数吧,在这些方法中我们只需要传入对应的函数,就可以获得想要的结果,而不用在自己累死累活写for循环了,代码看起来也优雅的多。高阶函数提高函数灵活性、隐藏了具体细节,我们只需要关注目标、使代码更简洁。所以当我们需要抽象通用问题时候,就可以用高阶函数来实现。

    下面开始报菜名,让俺们来瞅瞅有哪些常用的高阶函数:forEach,map,fifilter,every,some,find/findIndex,reduce,sort(这些招银云创的面试考过,其实按照功能来划分记得更牢靠)。

闭包

    这个网上资料很多,我就按照自己的思路总结一下,不一定太专业。闭包这个概念有点绕,对初学者不是特别友好,当年自己也吃尽了苦头,但是有一篇文章挺好的,看了他之后就有那么种入门的感觉,这回拉勾老师讲的也和这个对的上,这个是那篇文章的链接谈谈我对JS作用域的理解

    闭包的出现场景:另一个作用域中访问一个函数的内部函数,并且内部函数有访问该函数(外部函数)的作用域中的成员。

    闭包的好处:延长了外部函数内部变量的作用范围(外部函数会在调用栈上移除掉)。

    闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员(具体还可以借鉴以下红宝书的垃圾回收机制、活动变量)。

柯里化

    柯里化也是一个比较神秘的概念,但其实说通了就是多元函数的分步执行(拼多多面试考过柯里化的实现)。下面也分享一下自己的总结:

    柯里化实现方案:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果。

    柯里化的好处:将过程抽象了出来,提高复用性。通过课上的🌰来演示一下:

 // 普通的纯函数
function checkAge (min, age) {
  return age >= min
}

console.log(checkAge(18, 20))
console.log(checkAge(18, 24))
console.log(checkAge(22, 24))

//柯里化
// 函数的柯里化
// function checkAge (min) {
//   return function (age) {
//     return age >= min
//   }
// }

// ES6
let checkAge = min => (age => age >= min)

let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)

console.log(checkAge18(20))
console.log(checkAge18(24))

同样是通过函数比较年龄,当我们用柯里化的实现方案时,每次比较的时候只用传入一个参数,代码变得更加方便简洁。

    柯里化的实现也是面试的一个点,第一个是自己之前学会的实现,第二个是改良拉勾老师的实现

//自己之前学的
 const curry = (fn, ...args) => args.length < fn.length
     // 参数长度不足时,重新柯里化该函数,等待接受新参数
    ? (...arguments) => curry(fn, ...args, ...arguments)
    // 参数长度满足时,执行函数
    : fn(...args);
//课程教的改良版
 const curry = fn => {
    const curriedFn = (...args) => {
        return args.length < fn.length
            ? (...arguments) => curriedFn(...args, ...arguments)
            : fn(...args);
    }
    return curriedFn;
}

    柯里化之所以能提高复用性,是因为它让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数(闭包)的新函数,这是一种对函数参数的'缓存',让函数变的更灵活,让函数的粒度更小,可以把多元函数转换成一元函数,再通过组合使用函数产生强大的功能。

函数组合

    函数组合的出现目的是为了避免洋葱代码,如果纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))。 函数处理数据的过程我们可以看作一个管道

image.png 当一个管道太长(可以理解为运算过程太复杂的时候),我们就可以进行拆分(该用什么不用说了吧#手动滑稽.jpg),可以想象成把 fn 这个管道拆分成了3个管道 f1, f2, f3,数据 a 通过管道 f3 得到结果 m,m再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b

image.png 拆分成多个操作后我们再把这个细粒度的操作组合起来,就是函数组合,也就是可以这样用代码来表示:

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

    函数组合默认执行顺序从右向左,同时满足函数结合律,也就是

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

    了解了它的定义以及目标后,让我们手动实现一个组合函数compose

const compoese = 
    (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value);

lodash

    lodash 的fp(free pointed: 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量)模块提供了实用的对函数式编程友好的方法lodash-fp的文档地址 。它提供了提供了不可变 auto-curried(已柯里化) iteratee-first(函数优先) data-last (数据滞后)的方法。下面还是通过🌰来演示:

// lodash 模块 
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')

函子

    对于这个概念,我也是新学的,吃的还不是特别透,就简单地分享一下自己的笔记好了. 在了解函子之前,我们需要知道什么是容器,容器包含值和值的变形关系(这个变形关系就是函数)。而函子就是一个特殊的容器,是一个特殊的容器,具有 map 方法,map方法可以运行一个函数(纯函数)对值进行处理,最终返回一个包含新值的函子(变形关系)。

个人认为学习函子看代码理解的更快,所以接下来都展示各个函子的代码,笔记就写在注释里

//Functor 最简单的函子,没有错误处理机制,用数码宝贝来比喻的话可以理解为幼儿期
class Container {
  static of (value) {
    return new Container(value)
  }

  constructor (value) {
    this._value = value
  }

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

//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
  }
}
//Either 异常会让函数变的不纯,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))
  }
}

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

let error = parseJSON('{ name: zs }')
console.log(error)

let success = parseJSON('{ "name": "zs" }')
          .map(x => x.name.toUpperCase())
console.log(success)

//IO  这个我认为是完全提
//IO函子跟前面那几个Functor不同的地方在于,它的__value是一个函数。它把不纯的操作(比如IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。所以我们认为,IO包含的是被包裹的操作的返回值
//IO函子可以把不纯的动作存储到 _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())

//Monad 没错它就是究极体
//第一次学习的时候就觉得函子和Promise很像,看到monad就更像了,感觉是为后面的手写promise打基础
//一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
//Monad让我们避开了嵌套式地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其他异步任务。
// IO Monad
const fs = require('fs')
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))
  }

  join () {
    return this._value()
  }

  flatMap (fn) {
    return this.map(fn).join()
  }
}

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

let r = readFile('package.json')
          // .map(x => x.toUpperCase())
          .map(fp.toUpper)
          .flatMap(print)
          .join()

console.log(r)

结语

    感谢看到这里的有缘人,第一次写博客,有点辛苦啊,拉勾的课有一说一确实能让人学到知识,希望自己不浪费这次训练营的机会,摆脱初级程序员阶段,也愿看到这篇文章的你,能有所收获。“而世之奇伟,瑰怪,非常之观,常在于险远,而人之所罕至焉”,米娜桑一起干巴爹把,哈哈哈哈哈哈~