30

编程范式 —— 函数式编程入门

 5 years ago
source link: https://segmentfault.com/a/1190000018101201?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

yyi2emB.jpg!web

该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog

命令式编程和声明式编程

拿泡茶这个事例进行区分命令式编程和声明式编程

  • 命令式编程

1.烧开水(为第一人称)

2.拿个茶杯

3.放茶叶

4.冲水

  • 声明式编程

1.给我泡杯茶(为第二人称)

举个 demo

// 命令式编程
const convert = function(arr) {
  const result = []
  for (let i = 0; i < arr.length; i++) {
    result[i] = arr[i].toLowerCase()
  }
  return result
}

// 声明式编程
const convert = function(arr) {
  return arr.map(r => r.toLowerCase())
}

什么是函数式编程

函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。

函数式编程可以用简单如 交换律、结合律、分配律 的数学之法来帮我们简化代码的实现。

它具有如下一些特性:

  • 纯粹性: 纯函数不改变除当前作用域以外的值;
// 反面示例
let a = 0
const add = (b) => a = a + b // 两次 add(1) 结果不一致

// 正确示例
const add = (a, b) => a + b
  • 数据不可变性: Immutable
// 反面示例
const arr = [1, 2]
const arrAdd = (value) => {
  arr.push(value)
  return arr
}

arrAdd(3) // [1, 2, 3]
arrAdd(3) // [1, 2, 3, 3]

// 正面示例
const arr = [1, 2]
const arrAdd = (value) => {
  return arr.concat(value)
}

arrAdd(3) // [1, 2, 3]
arrAdd(3) // [1, 2, 3]

在后记 1 中对数组字符串方法是否对原值有影响作了整理

  • 函数柯里化: 将多个入参的函数转化为一个入参的函数;
const add = a => b => c => a + b + c
add(1)(2)(3)
  • 偏函数: 将多个入参的函数转化成两部分;
const add = a => (b, c) => a + b + c
add(1)(2, 3)
  • 可组合: 函数之间能组合使用
const add = (x) => x + x
const mult = (x) => x * x

const addAndMult = (x) => add(mult(x))

柯里化(curry)

如下是一个加法函数:

var add = (a, b, c) => a + b + c

add(1, 2, 3) // 6

假如有这样一个 curry 函数, 用其包装 add 函数后返回一个新的函数 curryAdd , 我们可以将参数 a、b 进行分开传递进行调用。

var curryAdd = curry(add)

// 以下输出结果都相同
curryAdd(1, 2, 3) // 6
curryAdd(1, 2)(3) // 6
curryAdd(1)(2)(3) // 6
curryAdd(1)(2, 3) // 6

动手实现一个 curry 函数

核心思路: 若传进去的参数个数未达到 curryAdd 的个数,则将参数缓存在闭包变量 lists 中:

function curry(fn, ...args) {
  const length = fn.length
  let lists = args || []

  let listLen
  return function (..._args) {
    lists = [...lists, ..._args]
    listLen = lists.length

    if (listLen < length) {
      const that = lists
      lists = []
      return curry(fn, ...that)
    } else if (listLen === length) {
      const that = lists
      lists = []
      return fn.apply(this, that)
    }
  }
}

代码组合(compose)

现在有 toUpperCasereversehead 三个函数, 分别如下:

var toUpperCase = (str) => str.toUpperCase()
var reverse = (arr) => arr.reverse()
var head = (arr) => arr[0]

接着使用它们实现将数组末位元素大写化输出, 可以这样做:

var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr)))

reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"

此时在构建 reverseHeadUpperCase 函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose 函数让使用者更加友好的使用呢? 类似如下形式:

var reverseHeadUpperCase = compose(toUpperCase, head, reverse)

reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"

此外 compose 函数符合 结合律 , 我们可以这样子使用:

compose(compose(toUpperCase, head), reverse)
compose(toUpperCase, compose(head, reverse))

以上两种写法与 compose(toUpperCase, head, reverse) 的效果完全相同, 都是依次从右到左执行传参中的函数。

此外 composemap 一起使用时也有相关的结合律, 以下两种写法效果相等

compose(map(f), map(g))
map(compose(f, g))

动手实现一个 compose 函数

代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。

var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)

范畴论

范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:

aYvi2iJ.jpg!web

学习函数式编程就是学习各种函子的过程。

函数式编程中, 函子(Functor) 是实现了 map 函数的容器, 下文中将函子视为范畴,模型可表示如下:

class Functor {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Functor(fn(this.value))
  }
}

但是在函数式编程中, 要避免使用 new 这种面向对象的编程方式, 取而代之对外暴露了一个 of 的接口, 也称为 pointed functor

Functor.of = value => new Functor(value)

Maybe 函子

Maybe 函子 是为了解决 this.value 为 null 的情形, 用法如下:

Maybe.of(null).map(r => r.toUpperCase()) // null
Maybe.of('m').map(r => r.toUpperCase())  // Maybe {value: "M"}

实现代码如下:

class Maybe {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return this.value ? new Maybe(fn(this.value)) : null
  }
}

Maybe.of = value => new Maybe(value)

Either 函子

Either 函子 是为了对应 if...else... 的语法, 即 非左即右 。因此可以将之拆分为 LeftRight 两个函子, 它们的用法如下:

Left.of(1).map(r => r + 1)  // Left {value: 1}

Right.of(1).map(r => r + 1) // Right {value: 2}

Left 函子 实现代码如下:

class Left {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return this
  }
}

Left.of = value => new Left(value)

Right 函子 实现代码如下(其实就是上面的 Functor ):

class Right {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Right(fn(this.value))
  }
}

Right.of = value => new Right(value)

具体 Either 函数只是对调用 Left 函子Right 函子 作一层筛选, 其接收 fg 两个函数以及一个函子( Left or Right )

var Either = function(f, g, functor) {
  switch(functor.constructor) {
    case 'Left':
      return f(functor.value)
    case 'Right':
      return g(functor.value)
    default:
      return f(functor.value)
  }
}

使用 demo:

Either((v) => console.log('left', v), (v) => console.log('def', v), left)   // left 1
Either((v) => console.log('rigth', v), (v) => console.log('def', v), rigth) // rigth 2

Monad 函子

函子会发生嵌套, 比如下面这样:

Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }

Monad 函子 对外暴露了 joinflatmap 接口, 调用者从而可以扁平化嵌套的函子。

class Monad {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Monad(fn(this.value))
  }

  join() {
    return this.value
  }

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

Monad.of = value => new Monad(value)

使用方法:

// join
Monad.of(Monad.of(1).join()) // Monad { value: 1 }
Monad.of(Monad.of(1)).join() // Monad { value: 1 }

// flatmap
Monad.of(1).flatmap(r => r + 1)  // 2

Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。

后记 1: 数组字符串方法小结(是否对原值有影响)

不会对原数组有影响的方法

slice

var test = [1, 2, 3]
var result = test.slice(0, 1)

console.log(test)   // [1, 2, 3]
console.log(result) // [1]

concat

var test = [1, 2, 3]
var result = test.concat(4)

console.log(test)   // [1, 2, 3]
console.log(result) // [1, 2, 3, 4]

对原数组有影响的方法

splice(这个需要特别记一下)

var test = [1, 2, 3]
var result = test.splice(0, 1)

console.log(test)   // [2, 3]
console.log(result) // [1]

sort

var arr = [2, 1, 3, 4]
arr.sort((r1, r2) => (r1 - r2))

console.log(arr) // [1, 2, 3, 4]

reverse

var test = [1, 2, 3]
var result = test.reverse()

console.log(test)   // [3, 2, 1]
console.log(result) // [3, 2, 1]

push/pop/unshift/shift

var test = [1, 2, 3]
var result = test.push(4)

console.log(test)   // [1, 2, 3, 4]
console.log(result) // 4

不会对原字符串造成影响的方法

substr/substring/slice

// substr
var test = 'abc'
var result = test.substr(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

// substring
var test = 'abc'
var result = test.substring(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

// slice
var test = 'abc'
var result = test.slice(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK