函数式编程入门

120 阅读6分钟

开篇

从2015年大学毕业到现在有5年的时间,一直在IT行业工作,由于自己是材料化学专业和计算机专业并没有直接关系,所以计算机相关基础一直比较薄弱。随着工作时间越久,在编程方面越来越深入,越来越感觉自己技术不成体系,最近报了一个前端的进阶班。在今后的学习过程中学到的知识点、工作中的解决方案,将会在这里进行分享。顺便养成自己写博客的良好习惯。

基础篇

函数式编程、异步编程。

  • 函数式编程和面向对象编程的关系。
    • 函数式编程是指关心数据之间的映射关系。命令行式解决问题的编程方式。
    • 面向对象是把数据及对数据的操作方法放在一起,作为一个相互依存的整体对象。
  • 函数式编程的基本概念与特性
    • 函数是指的数学中的函数,即对数集施加的运算法则
    • 函数有相同的输入肯定有相同的输出
  • 函数式编程的入门基础
    • 函数是一等公民
    • 高阶函数
    • 闭包
  • 常见的函数式编程类库
    • lodash/fp
    • folktale

函数是一等公民 (eg. 略)

  • 函数可以作为变量
  • 函数可以作为返回值
  • 函数可以作为参数

高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值
  • 高阶函数的意义
    • 抽象可以帮我们屏蔽细节,只需要关注我们的目标
    • 高阶函数是用来抽象通用的问题

闭包

和GC回收机制有关。网上资料一大把,不多阐述

纯函数

相同的输入永远有相同的输出,并且没有任何可观察的副作用

副作用指的是 函数依赖外部状态,会导致相同的输入有不同的输出。 例如:全局变量、外部配置文件、用户输入的数据、数据库配置等等

  • 容易进行测试
  • 更容易进行结果缓存
  • 可以并行处理

柯里化

柯里化是为了减少外部状态对函数的影响

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

函数组合

函数组合是为了解决函数嵌套调用的问题。把嵌套调用改写成管道调用。 即:洋葱代码转换为管道代码。就像.net core中的中间件形成的管道。

Point Free

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

函子

函子的目的是为了把副作用控制在尽可能小的范围内、处理异常、异步操作等。

  • Functor
    • 容器:包含值和值的变形关系(这个变形关系就是函数)
    • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)
    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子就是一个实现了 map 契约的对象
    • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这 个函数来对值进行处理 最终 map 方法返回一个包含新值的盒子(函子)
// 一个容器,包裹一个值
class Container {
// of 静态方法,可以省略 new 关键字创建对象
static of (value) {
  return new Container(value)
}
constructor (value) {
  this._value = value
}
// map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
map (fn) {
  return Container.of(fn(this._value))
  }
}
// 测试
Container.of(3)
.map(x => x + 2)
.map(x => x * x)
  • MayBe
    • 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
    • MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
     class MayBe {
       static of (value) {
         return new MayBe(value)
       }
     constructor (value) {
         this._value = value
       }
     // 如果对空值变形的话直接返回 值为 null 的函子
     map (fn) {
         return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
       }
     isNothing () {
         return this._value === null || this._value === undefined
       }
     }
     MayBe.of('Hello World')
     .map(x => x.toUpperCase())
     
     MayBe.of(null)
     .map(x => x.toUpperCase())
    
    • 在 MayBe 函子中,我们很难确认是哪一步产生的空值问题
  • Either

类似与if else 用来处理异常

// 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 r = parseJSON('{ name: "zs" }')
          .map(x => x.name.toUpperCase())
console.log(r)
  • IO 函子
    • IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
    • IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行)。包装当前的纯操 作
    • 把不纯的操作交给调用者来处理
  // IO 函子
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())
  • Task 异步执行
// Task 处理异步任务
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(单子)
// 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)