跳到主要内容

函数式✅

函数式编程了解多少?

答案

函数式编程是一种将程序视为函数组合的编程范式,其中函数是基本构建单元。

核心概念

  • 纯函数:输出仅取决于输入,无副作用,相同输入总是产生相同输出

    // 纯函数示例
    const add = (a, b) => a + b
  • 不可变性:数据不允许被修改,避免副作用

    // 不可变操作示例
    const addToArray = (arr, item) => [...arr, item] // 返回新数组而非修改原数组
  • 函数组合:将简单函数组合成复杂函数

    const compose = (f, g) => x => f(g(x))
    const addOne = x => x + 1
    const double = x => x * 2
    const addOneThenDouble = compose(double, addOne)
  • 高阶函数:可接收或返回其他函数的函数,如 mapfilterreduce

  • 柯里化:将多参数函数转换为一系列单参数函数,实现参数复用

  • 惰性计算:仅在必要时执行计算,提高性能

JavaScript中的函数式编程

  • 原生支持高阶函数:Array.prototype.map, filter, reduce
  • 流行库:Lodash/fp, Ramda, RxJS
  • React函数式组件和Hooks体现了函数式思想

函数式编程 vs 面向对象编程

  • 函数式:强调数据不可变和无状态操作
  • OOP:强调对象封装状态和行为

实际应用

  • 状态管理(Redux)
  • 异步流程控制
  • 数据转换和验证
  • 事件处理

优势

  • 代码可读性和可维护性更高
  • 减少bug,特别是由副作用引起的错误
  • 更容易测试,因为函数行为可预测
  • 简化并发编程,避免状态共享问题
  • 促进代码复用和模块化

其他

  • 对性能有潜在影响(如创建大量小对象)

函数式编程通过强调纯函数和不可变数据,使代码更可靠、更易于理解和维护。

函数柯里化是什么?

答案
  1. 柯里化的定义 柯里化(Currying)它将一个接受多个参数的函数转换成一系列使用一个参数的函数。简单来说,就是把形如 f(a, b, c) 的函数转换为 f(a)(b)(c) 的形式,使得函数的参数可以逐步传入而非一次性传入。

  2. 柯里化的作用

    1. 参数复用:可以预先固定一些参数,减少重复代码

      const multiply = curry((a, b) => a * b)
      const double = multiply(2) // 固定第一个参数为2
      const triple = multiply(3) // 固定第一个参数为3
    2. 延迟计算:将函数调用分批进行,在需要的时候才执行计算

      const curriedFilter = curry((predicate, arr) => arr.filter(predicate))
      const filterGreaterThanThree = curriedFilter(num => num > 3) // 创建特定过滤函数
      // 在需要时才传入数据执行计算
      const result = filterGreaterThanThree([1, 2, 3, 4, 5]) // [4, 5]
    3. 代码组合与复用:更容易构建模块化、可复用的代码

      const curriedMap = curry((fn, arr) => arr.map(fn))
      const doubleAll = curriedMap(double)
      const tripleAll = curriedMap(triple)
    4. 提高可读性:使代码更加函数式和模块化,每个函数更加聚焦

  3. 如何实现柯里化

    function currying (fn, ...args) {
    return args.length >= fn.length
    ? fn(...args)
    : currying.bind(null, fn, ...args)
    }

    function add (a, b, c) {
    return a + b + c
    }

    // 使用柯里化函数
    const curriedAdd = currying(add)

    console.log(curriedAdd(1)(2)(3)) // 6
    console.log(curriedAdd(1, 2)(3)) // 6
    console.log(curriedAdd(1)(2, 3)) // 6
    console.log(curriedAdd(1, 2, 3)) // 6

什么是偏函数 ?

答案

偏函数(Partial Application)它指的是通过固定一个函数的一个或多个参数,从而产生一个新的函数。

偏函数与柯里化的区别

  • 偏函数:预先填充原函数的部分参数,返回一个新函数处理剩余参数
  • 柯里化:将多参数函数转换为一系列单参数函数的链式调用

偏函数的特点

  1. 参数固定:预先固定一个或多个参数,但不一定是第一个参数

    function add (a, b, c) {
    return a + b + c
    }

    // 偏函数:固定第一个参数为10
    const add10 = partial(add, 10)
    add10(5, 7) // 22 (10 + 5 + 7)

    // 偏函数:固定第二个参数为10
    const addSecond10 = partial(add, undefined, 10)
    addSecond10(5, 7) // 22 (5 + 10 + 7)
  2. 简化复杂函数:减少函数调用时所需参数数量

  3. 创建特定用途的函数:基于通用函数创建特定场景下的函数

偏函数的实现

function partial (fn, ...args) {
return (args.length >= fn.length && args.every(el => el !== undefined))
? fn(...args)
: (...leftArgs) => {
const allArgys = Array.from({ length: fn.length }).map((el, index) => {
return args[index] !== undefined ? args[index] : leftArgs.shift()
})
return partial(fn, ...allArgys)
}
}

// 使用示例
function greet (greeting, name) {
return `${greeting}, ${name}!`
}

const sayHello = partial(greet, 'Hello')
console.log(sayHello('World')) // "Hello, World!"

const greetJohn = partial(greet, undefined, 'John')
console.log(greetJohn('Hi')) // "Hi, John!"

实际应用

  • 配置API调用时预设参数
  • 简化事件处理函数
  • 函数组合中固定部分参数
  • 创建更具语义的函数名称

什么是 Compose ?

答案

Compose 是一种将多个函数组合成一个函数的技术或者模式。 在 Compose 过程中,一个函数的输出会作为下一个函数的输入,最终将多个函数串联起来,形成一个流水线,对数据进行连续处理。

Compose 的特点

  1. 从右到左执行

    Compose 组合的函数,从右到左的顺序依次执行。也就是最右边的函数先执行,然后将结果传递给左边的函数,依次类推。

  2. 数据流向

    数据像管道一样,从右向左依次流经各个函数,经过每个函数的处理转换,最终得到结果。

  3. 函数组合 Compose 的核心在于将多个函数组合成一个函数,使得代码更加简洁、可读性更高。

Compose 的实现

function compose (...fns) {
// 1. 兜底返回空函数
if (fns.length === 0) return () => undefined
return (...args) => fns.reverse().reduce((res, fn) => {
const inputArgs = [].concat(res)
// 2. 多个参数的话需要 deepmerge
const afterRes = fn(...inputArgs.concat(args.slice(inputArgs.length)))
return afterRes
}, args)
}

// 使用示例
function add (a) {
return a + 10
}
function multiply (a) {
return a * 10
}
const composedFunction = compose(multiply, add)
console.log(composedFunction(5)) // (5 + 10) * 10 = 150

Compose 的实际应用

  1. 简化复杂操作

    Compose 可以将多个简单的函数组合成一个复杂的函数,简化代码逻辑,提高可读性。

  2. 中间件

    在 Koa.js 等框架中,Compose 被广泛应用于中间件的实现,将多个中间件函数组合成一个函数,依次处理请求。

  3. 数据处理

    Compose 可以用于对数据进行连续处理,例如先对数据进行格式化,然后进行验证,最后进行转换等操作。

什么是管道 (Pipeline)?

答案

管道(Pipeline)按照从左到右的顺序将多个函数组合在一起,形成一个数据处理的流水线。每个函数接收前一个函数的输出作为输入,最终返回结果。

管道的特点

  1. 顺序执行:管道中的函数按照顺序依次执行,一个函数的输出作为下一个函数的输入。
  2. 数据流向:数据像流水一样,依次流经各个函数,经过每个函数的处理转换,最终得到结果。
  3. 链式调用:管道通常通过链式调用的方式实现,使得代码更加简洁、易读。

管道的实现

function pipeline (...fns) {
// 1. 兜底返回空函数
if (fns.length === 0) return () => undefined
return (...args) => fns.reduce((res, fn) => {
const inputArgs = [].concat(res)
// 2. 多个参数的话需要 deepmerge
const afterRes = fn(...inputArgs.concat(args.slice(inputArgs.length)))
return afterRes
}, args)
}
// 使用示例
function add (a) {
return a + 10
}
function multiply (a) {
return a * 10
}
const pipeProcess = pipeline(add, multiply)
const result = pipeProcess(5) // (5 + 10) * 10 = 150

console.log(result)

管道 vs Compose

特性管道(Pipeline)Compose
执行顺序从左到右从右到左
参数传递前一个函数的输出作为下一个函数的输入后一个函数的输出作为前一个函数的输入
适用场景数据需要依次经过多个函数处理的场景需要将多个函数组合成一个函数的场景
实现方式通常使用 reduce 方法实现通常使用 reduceRightreverse 实现
代码可读性更符合直觉,易于理解数据的处理流程需要一定的函数式编程基础才能理解
组合方式将函数作为参数传递给管道函数通过 compose 函数将多个函数组合成一个新函数

什么是函子?

答案

函子(Functor) 是一种拥有 map 方法的数据结构,它能让我们在**不改变上下文(结构)**的前提下对其中的值进行变换。 JavaScript 中的 ArrayPromise 本身就是天然的函子。

函子的特点

  1. 容器:函子是一个容器,它可以包含任何类型的值。
  2. 提供 map 方法,map(fn) 会返回一个新的函子
  3. 遵守两个 Functor 定律:恒等律组合律
    1. 恒等律f.map(x => x) 应该等于 f
    2. 组合律f.map(x => fn(g(x))) 应该等于 f.map(g).map(fn)

函子的实现

class Functor {
// 支持持有一个值,注意这个值可以是任意类型,且不可变
constructor (value) {
this.value = value
}

// 提供一个映射方法,注意这个方法会返回一个新的 Functor
map (fn) {
return new Functor(fn(this.value))
}
}

// 使用示例
const functor = new Functor(5)
const newFunctor = functor.map(x => x + 10)

console.log(newFunctor.value) // 15

函子的实际应用

  1. 处理 null 或 undefined 值:可以使用 Maybe 函子来避免空指针异常。
  2. 处理异步操作:可以使用 Promise 函子来处理异步操作的结果。
  3. 数据转换:可以使用函子来对数据进行转换,例如将字符串转换为数字。

一些典型的函子

  • Maybe:一个能处理可空值的函子。它根据是否存在值(Just vs Nothing)决定是否执行 map 中的函数。
  • Either:处理可能失败的操作。它有两个分支:Left 表示错误,Right 表示成功。map 只作用于 Right 分支。

什么是 Monad?

答案

Monad 是一种设计模式,用于将一系列操作以链式的方式组合起来,尤其适用于处理具有上下文的计算,如异步操作、错误处理或可空值等。​

Monad 是对 Functor 的扩展,除了具备 map 方法外,还提供了两个核心操作:​

  1. unit(或 of):​用于将一个普通值封装到 Monad 容器中。
  2. flatMap(或 bind、chain):​用于将一个返回 Monad 的函数应用于容器中的值,并将结果“扁平化”,避免嵌套的 Monad。​

通过这两个操作,Monad 允许我们以纯函数的方式处理带有上下文的计算,保持代码的可组合性和可预测性。

Monad 的核心特性

  1. 封装值:Monad 是一个容器,它可以包含任何类型的值。
  2. unit/return 方法:Monad 必须提供一个 unitreturn 方法,该方法接收一个值作为参数,并将该值封装到 Monad 容器中。
  3. bind/flatMap 方法:Monad 必须提供一个 bindflatMap 方法,该方法接收一个函数作为参数,该函数接收 Monad 容器中的值作为参数,并返回一个新的 Monad 容器。bind 方法将该函数应用到 Monad 容器中的值上,返回一个新的 Monad 容器,该 Monad 容器包含应用函数后的结果。
  4. 一个符合 Monad 定义的类型应满足以下三个定律:​
    1. 左单位律unit(a).flatMap(f) 等价于 f(a)
    2. 右单位律m.flatMap(unit) 等价于 m
    3. 结合律m.flatMap(f).flatMap(g) 等价于 m.flatMap(x => f(x).flatMap(g))

以下是一个简单的 Maybe Monad 实现,用于处理可能为 null 或 undefined 的值:

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

static of (value) {
return new Maybe(value)
}

isNothing () {
return this.value === null || this.value === undefined
}

map (fn) {
return this.isNothing() ? this : Maybe.of(fn(this.value))
}

flatMap (fn) {
return this.isNothing() ? this : fn(this.value)
}

getOrElse (defaultValue) {
return this.isNothing() ? defaultValue : this.value
}
}

// Maybe Monad 通过 map 和 flatMap 方法,允许安全地对可能为空的值进行操作,避免了显式的空值检查。
const result = Maybe.of(5)
.map(x => x + 1)
.flatMap(x => Maybe.of(x * 2))
.getOrElse(0)

console.log(result) // 输出: 12

Monad 的实际应用

  1. 异步操作:​Promise 是 JavaScript 中的一个 Monad,用于处理异步计算。通过 then 方法(类似于 flatMap),我们可以将异步操作串联起来。​
  2. 错误处理:​Either Monad 允许我们在不使用异常的情况下处理错误。它通常有两个分支:Right 表示成功,Left 表示失败。​
  3. 可空值处理:​Maybe Monad 提供了一种优雅的方式来处理可能为 null 或 undefined 的值,避免了繁琐的空值检查。​
  4. 日志记录和状态管理:​Writer 和 State Monad 可用于在不引入副作用的情况下进行日志记录和状态管理。

λ 演算是什么

答案

λ 演算(Lambda Calculus)是一种形式系统,用于研究函数定义、函数应用和递归。它可以被认为是世界上最小的编程语言。

λ 演算的特点

  1. 函数是头等公民:在 λ 演算中,函数可以作为参数传递给其他函数,也可以作为返回值返回。
  2. 匿名函数:λ 演算使用匿名函数来定义函数。
  3. 变量替换:λ 演算使用变量替换来执行函数。

λ 演算的实现

λ 演算可以使用 JavaScript 来实现:

// 变量
const variable = name => ({
type: 'variable',
name
})

// 抽象
const abstraction = (param, body) => ({
type: 'abstraction',
param,
body
})

// 应用
const application = (func, arg) => ({
type: 'application',
func,
arg
})

// 示例
// λx.x
const identity = abstraction(variable('x'), variable('x'))

// (λx.x) y
const applicationExample = application(identity, variable('y'))

λ 演算的实际应用

  1. 编程语言设计:λ 演算是许多编程语言的基础,例如 Haskell、Lisp 和 JavaScript。
  2. 编译器设计:λ 演算可以用于编译器设计,例如将高级语言转换为机器代码。
  3. 理论计算机科学:λ 演算是理论计算机科学的重要组成部分,例如用于研究可计算性理论。
22%