函数式编程入门教程

什么是函数式编程

函数式编程入门教程

函数式编程(Functional Programming,FP)是一种编程范式,跟"面向对象"(OOP)、"过程式"(Procedural)并列。核心思想:把程序构造成一系列函数的组合,函数本身是"一等公民"(可以传递、返回、组合),且尽量避免"可变状态"和"副作用"。

FP 不是某种语言专属,任何主流语言(JS、Python、Java、C++)都能写函数式代码,但 Haskell、Erlang、Clojure、Elixir 这种是"原生函数式"语言,FP 是第一选择。

这篇是函数式编程的入门梳理,翻译/整理自阮一峰的函数式编程入门教程等多篇资料,以 JavaScript 示例为主。

核心特性

1. 函数是一等公民

函数可以像普通值一样:赋值给变量、作为参数传递、作为返回值。

const sum = (a, b) => a + b

const arity = sum.length
console.log(arity)        // 2

// 函数sum的arity为2。

2. 不可变性(Immutability)

数据一旦创建就不能修改。要"修改"实际是创建新数据。

const filter = (predicate, xs) => xs.filter(predicate)

const is = (type) => (x) => Object(x) instanceof type

filter(is(Number), [0, '1', 2, null]) // 0, 2

3. 纯函数(Pure Function)

纯函数:

  • 给定相同输入,总是返回相同输出(确定性)
  • 没有副作用(不修改外部状态)
const addTo = x => y => x + y;
var addToFive = addTo(5);
addToFive(3); //返回 8

4. 无副作用

// 创建偏函数,固定一些参数
const partial = (f, ...args) =>
  // 返回一个带有剩余参数的函数
  (...moreArgs) =>
    // 调用原始函数
    f(...args, ...moreArgs)

const add3 = (a, b, c) => a + b + c // (c) => 2 + 3 + c

// 部分地将`2`和`3`应用于`add3`,得到一个只有一个参数的函数
const fivePlus = partial(add3, 2, 3)

fivePlus(4)  // 9

核心概念

高阶函数

接收函数作为参数或返回函数的函数。map / filter / reduce 是最常见的高阶函数。

const add1More = add3.bind(null, 2, 3) // (c) => 2 + 3 + c
const sum = (a, b) => a + b

const curriedSum = (a) => (b) => a + b

curriedSum(3)(4)         // 7

const add2 = curriedSum(2)

add2(10)     // 12

柯里化(Currying)

把"接收多个参数的函数"转化成"接收单个参数,返回新函数"的序列。

const add = (x, y) => x + y

const curriedAdd = _.curry(add)

curriedAdd(1, 2)   // 3
curriedAdd(1)(2)   // 3
curriedAdd(1)      // (y) => 1 + y
const compose = (f, g) => (a) => f(g(a))    // 定义
const floorAndToString = compose((val) => val.toString(), Math.floor) // 使用
floorAndToString(12.12)   // '12'

函数组合(Composition)

把多个函数组合成一个,数学上的 f(g(x))。

const printAsString = (num) => console.log(`Given ${num}`)

const addOneAndContinue = (num, cc) => {
  const result = num + 1
  cc(result)
}

addOneAndContinue(2, printAsString) // 'Given 3'
const continueProgramWith = (data) => {
  // 继续执行程序
}

readFileAsync('path/to/file', (err, response) => {
  if (err) {
    // 错误处理
    return
  }
  continueProgramWith(response)
})

Pipe(管道)

跟 compose 类似,但从左到右执行,更接近自然阅读顺序。

const greet = (name) => `hello, ${name}`

greet('world')

剩余的代码示例

window.name = 'Brianne'

const greet = () => `Hi, ${window.name}`

greet() // "Hi, Brianne"
let greeting

const greet = (name) => {
    greeting = `Hi, ${name}`
}

greet('Brianne')
greeting // "Hi, Brianne"
const differentEveryTime = new Date()
console.log('IO就是一种副作用!')
f(f(x))f(x)
Math.abs(Math.abs(10))
sort(sort(sort([2, 1])))
// 已知:
const map = (fn) => (list) => list.map(fn)
const add = (a) => (b) => a + b

// 所以:

// 非Points-Free —— number 是显式参数
const incrementAll = (numbers) => map(add(1))(numbers)

// Points-Free —— list 是隐式参数
const incrementAll2 = map(add(1))
const predicate = (a) => a > 2

;[1, 2, 3, 4].filter(predicate)
// 定义的contract: int -> boolean
const contract = (input) => {
  if (typeof input === 'number') return true
  throw new Error('Contract Violated: expected int -> int')
}

const addOne = (num) => contract(num) && num + 1

addOne(2) // 3
addOne('hello') // 违反了contract: int -> boolean
5
Object.freeze({name: 'John', age: 30})
;(a) => a
;[1]
undefined
const five = 5
const john = Object.freeze({name: 'John', age: 30})
john.age + five === ({name: 'John', age: 30}).age + (5)
object.map(x => x) ≍ object
object.map(compose(f, g)) ≍ object.map(g).map(f)  // f, g 为任意函数
const f = x => x + 1
const g = x => x * 2

;[1, 2, 3].map(x => f(g(x)))
;[1, 2, 3].map(g).map(f)
Array.of(1)
const liftA2 = (f) => (a, b) => a.map(f).ap(b) // 注意这里是 ap 而不是 map.

const mult = a => b => a * b

const liftedMult = liftA2(mult) // 这个函数现在可以作用于函子,如Array

liftedMult([1, 2], [3]) // [3, 6]
liftA2(a => b => a + b)([1, 2], [3, 4]) // [4, 5, 5, 6]
const increment = (x) => x + 1

lift(increment)([2]) // [3]
;[2].map(increment) // [3]
const greet = () => 'hello, world.'
;(function (a) {
    return a + 1
})

;(a) => a + 1
[1, 2].map((a) => a + 1)
const add1 = (a) => a + 1
const rand = function* () {
  while (true) {
    yield Math.random()  
  } 
}

const randIter = rand()
randIter.next() // 每次执行产生一个随机值,表达式会在需要时求值。
1 + 1   // 2
1 + 0   // 1
1 + (2 + 3) === (1 + 2) + 3 // true
;[1, 2].concat([3, 4]) // [1, 2, 3, 4]
;[1, 2].concat([])
0 - 4 === 4 - 0 // false
Array.prototype.chain = function (f) {
  return this.reduce((acc, it) => acc.concat(f(it)), [])  
}

// 使用
;Array.of('cat,dog', 'fish,bird').chain(s => s.split(',')) // ['cat', 'dog', 'fish', 'bird']

// 和 map 相比
;Array.of('cat,dog', 'fish,bird').map(s => s.split(',')) // [['cat', 'dog'], ['fish', 'bird']]
const CoIdentity = (v) => ({
  val: v,
  extract () {
    return this.val  
  },
  extend (f) {
    return CoIdentity(f(this))  
  }
})
CoIdentity(1).extract() // 1
CoIdentity(1).extend(x => x.extract() + 1) // CoIdentity(2)
// 实现
Array.prototype.ap = function (xs) {
    return this.reduce((acc, f) => acc.concat(xs.map(f)), [])
}

// 示例
;[(a) => a + 1].ap([1]) // [2]
// 你想要组合的两个数组
const arg1 = [1, 3]
const arg2 = [4, 5]

// 组合函数 - 必须要柯里化
const add = (x) => (y) => x + y

const partiallyAppliedAdds = [add].ap(arg1) // [(y) => 1 + y, (y) => 3 + y]
partiallyAppliedAdds.ap(arg2) // [5, 6, 7, 8]
// uppercase :: String -> String
const uppercase = (str) => str.toUpperCase()

// decrement :: Number -> Number
const decrement = (x) => x - 1
// 提供函数在两种类型间互相转换
const pairToCoords = (pair) => ({x: pair[0], y: pair[1]})

const coordsToPair = (coords) => [coords.x, coords.y]

coordsToPair(pairToCoords([1, 2])) // [1, 2]

pairToCoords(coordsToPair({x: 1, y: 2})) // {x: 1, y: 2}
A.of(f).ap(A.of(x)) == A.of(f(x))

Either.of(_.toUpper).ap(Either.of("oreos")) == Either.of(_.toUpper("oreos"))
const unfold = (f, seed) => {
  function go(f, seed, acc) {
    const res = f(seed);
    return res ? go(f, res[1], acc.concat([res[0]])) : acc;
  }
  return go(f, seed, [])
}
const countDown = n => unfold((n) => {
  return n <= 0 ? undefined : [n, n - 1]
}, n)

countDown(5) // [5, 4, 3, 2, 1]
// 包含 undefined 对于列表来说显然是不安全的,
// 但是足以说明问题。
const para = (reducer, accumulator, elements) => {
  if (elements.length === 0)
    return accumulator

  const head = elements[0]
  const tail = elements.slice(1)

  return reducer(head, tail, para(reducer, accumulator, tail))
}

const suffixes = list => para(
  (x, xs, suffxs) => [xs, ... suffxs],
  [],
  list
)

suffixes([1, 2, 3, 4, 5]) // [[2, 3, 4, 5], [3, 4, 5], [4, 5], [5], []]
Array.prototype.equals = function (arr) {
  const len = this.length
  if (len !== arr.length) {
    return false
  }
  for (let i = 0; i < len; i++) {
    if (this[i] !== arr[i]) {
      return false
    }
  }
  return true
}

;[1, 2].equals([1, 2])   // true
;[1, 2].equals([3, 4])   // false
;[1].concat([2]) // [1, 2]
const sum = (list) => list.reduce((acc, val) => acc + val, 0)
sum([1, 2, 3])        // 6
// 使用 [Ramda's lens](http://ramdajs.com/docs/#lens)
const nameLens = R.lens(
  // 一个对象的 name 属性的 getter
  (obj) => obj.name,
  // name 属性的 setter
  (val, obj) => Object.assign({}, obj, {name: val})
)
const person = {name: 'Gertrude Blanch'}

// 调用 getter
R.view(nameLens, person) // 'Gertrude Blanch'

// 调用 setter
R.set(nameLens, 'Shafi Goldwasser', person) // {name: 'Shafi Goldwasser'}

// 将函数应用于结构中的值
R.over(nameLens, uppercase, person) // {name: 'GERTRUDE BLANCH'}
// 这个 lens 关注一个非空数组中的第一个元素
const firstLens = R.lens(
  // 获取数组的第一个元素
  xs => xs[0],
  // 数组的第一个元素的非可变 setter
  (val, [__, ...xs]) => [val, ...xs]
)

const people = [{name: 'Gertrude Blanch'}, {name: 'Shafi Goldwasser'}]

// 无论你怎么想,lens 是从左到右合成的
R.over(compose(firstLens, nameLens), uppercase, people) // [{'name': 'GERTRUDE BLANCH'}, {'name': 'Shafi Goldwasser'}]
// functionName :: firstArgType -> secondArgType -> returnType

// add :: Number -> Number -> Number
const add = (x) => (y) => x + y

// increment :: Number -> Number
const increment = (x) => x + 1
// call :: (a -> b) -> a -> b
const call = (f) => (x) => f(x)
// map :: (a -> b) -> [a] -> [b]
const map = (f) => (list) => list.map(f)
// 想象这些不是 set,而是仅包含这些值的某种类型。
const bools = new Set([true, false])
const halfTrue = new Set(['half-true'])

// 这个 weakLogic 类型包含 bools 类型和 halfTrue 类型的和。
const weakLogicValues = new Set([...bools, ...halfTrue])
// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({x: x, y: y})
// 简单的定义
const Some = (v) => ({
  val: v,
  map (f) {
    return Some(f(this.val))
  },
  chain (f) {
    return f(this.val)
  }
})

const None = () => ({
  map (f) {
    return this
  },
  chain (f) {
    return this
  }
})

// maybeProp :: (String, {a}) -> Option a
const maybeProp = (key, obj) => typeof obj[key] === 'undefined' ? None() : Some(obj[key])
// getItem :: Cart -> Option CartItem
const getItem = (cart) => maybeProp('item', cart)

// getPrice :: Item -> Option Number
const getPrice = (item) => maybeProp('price', item)

// getNestedPrice :: cart -> Option a
const getNestedPrice = (cart) => getItem(obj).chain(getPrice)

getNestedPrice({}) // None()
getNestedPrice({item: {foo: 1}}) // None()
getNestedPrice({item: {price: 9.99}}) // Some(9.99)
// times2 :: Number -> Number
const times2 = n => n * 2

[1, 2, 3].map(times2) // [2, 4, 6]
// 例1: 列表的和
// sum :: [Number] -> Number
const sum = arr => arr.reduce((a, b) => a + b)
sum([1, 2, 3]) // 6
sum([]) // TypeError: Reduce of empty array with no initial value

// 例2: 获取列表的第一个值
// first :: [A] -> A
const first = a => a[0]
first([42]) // 42
first([]) // undefined
// 甚至更糟: 
first([[42]])[0] // 42
first([])[0] // Uncaught TypeError: Cannot read property '0' of undefined

// 例3: 将函数重复 N 次
// times :: Number -> (Number -> Number) -> Number
const times = n => fn => n && (fn(n), times(n - 1)(fn))
times(3)(console.log)
// 3
// 2
// 1
times(-1)(console.log)
// RangeError: Maximum call stack size exceeded
// 例1: 列表的和
// 我们可以提供默认值,使它总会返回结果
// sum :: [Number] -> Number
const sum = arr => arr.reduce((a, b) => a + b, 0)
sum([1, 2, 3]) // 6
sum([]) // 0

// 例2: 获取列表的第一个值
// 将结果改为 Option
// first :: [A] -> A
const first = a => a.length ? Some(a[0]) : None()
first([42]).map(a => console.log(a)) // 42
first([]).map(a => console.log(a)) // console.log 不会执行
//我们之前的糟糕情况
first([[42]]).map(a => console.log(a[0])) // 42
first([]).map(a => console.log(a[0])) // 不会执行,所以不会有 error
// 更重要的是,通过返回类型 (Option) ,我们会知道:
// 我们应该使用 .map 方法来访问数据,所以我们不会忘记检查输入,
// 因为这样的检查会被内建在函数中。

// 例3: 将函数重复 N 次
// 我们需要通过改变条件来确保函数总会终止: 
// times :: Number -> (Number -> Number) -> Number
const times = n => fn => n > 0 && (fn(n), times(n - 1)(fn))
times(3)(console.log)
// 3
// 2
// 1
times(-1)(console.log)
// 不会再执行

函数式编程的实际收益

掌握函数式编程后,代码会变这样:

  1. 更易测试 —— 纯函数不依赖外部状态,单元测试极简(给输入,验证输出)
  2. 更易并发 —— 没有共享可变状态,多线程 / 多进程跑安全
  3. 更易推理 —— 函数行为只看签名就知道(无副作用),改代码时不怕"动了 A 引发 B 出问题"
  4. 更易复用 —— 小函数组合大功能,粒度小,组合性强

实际项目里怎么用

纯函数式语言(Haskell)对 JS 开发者来说门槛高,但 JS / TS 项目里"局部函数式"非常实用:

  • 数组操作优先用 map / filter / reduce,不用 for 循环
  • 数据更新用扩展运算符 / immer.js,不直接 mutate
  • 组件用函数式风格(React 函数组件 + hooks,Vue 3 setup)
  • 工具函数尽量写成纯函数,易测易复用
  • 用 Lodash/fp 或 Ramda 替代 Lodash,所有方法都是 curry + immutable

学习资源

FP 这套思想极有价值,但不必从面向对象切到全函数式。两种范式都用,根据问题挑合适的,才是工程师的成熟做法。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

公共DNS服务介绍

2024-7-4 16:45:10

技术教程

规避笔记陷阱 [译]

2024-7-4 16:56:47

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索