DevToolBox免费
博客

函数式编程指南:纯函数、不可变性、Monad、组合与 JavaScript/TypeScript 中的 FP

20 分钟阅读作者 DevToolBox Team

函数式编程指南:纯函数、不可变性、Monad 与更多

掌握函数式编程核心概念:纯函数、不可变性、高阶函数、闭包、柯里化、组合、Monad、Functor、模式匹配和代数数据类型 — 包含 JavaScript/TypeScript 与 Haskell/Scala 对比。

TL;DR — 60 秒速览函数式编程
  • 纯函数:相同输入 → 相同输出,无副作用,可缓存、可并行
  • 不可变性:数据创建后不可修改;使用 spread、Immer 或 persistent 数据结构
  • 高阶函数:map、filter、reduce 是核心工具;函数是一等公民
  • 柯里化与组合:将多参数函数转化为单参数链,通过 pipe/compose 构建管道
  • Monad(Maybe、Either、Promise):以可组合的方式处理空值、错误和异步
  • 代数数据类型 + 模式匹配:让非法状态不可表示
  • Haskell 是纯函数式标杆;TypeScript 通过库和约定支持 FP 风格
关键要点
  • 函数式编程是一种声明式范式,强调表达式而非语句
  • 引用透明性使代码像数学方程一样可推理
  • TypeScript 的 Readonly、as const、discriminated unions 是实用 FP 工具
  • fp-ts、Effect-TS、Ramda 和 Lodash/fp 将 FP 带入 JS/TS 生态
  • Scala 融合 OOP 和 FP;Haskell 强制纯函数式
  • 学习 FP 思想可改善所有范式中的代码质量

1. 纯函数与引用透明性

纯函数是函数式编程的基石。一个函数是"纯"的,当且仅当:(1) 对于相同的输入,总是返回相同的输出;(2) 不产生任何可观察的副作用(不修改外部状态、不做 I/O、不依赖随机数或时间)。引用透明性意味着你可以用函数的返回值替换函数调用本身,而不改变程序行为。

JavaScript/TypeScript 中的纯函数

// Pure: same input → same output, no side effects
const add = (a: number, b: number): number => a + b;
const toUpper = (s: string): string => s.toUpperCase();

// Impure: depends on external state
let tax = 0.1;
const calcPrice = (price: number) => price * (1 + tax); // reads external var

// Impure: side effect (mutates input)
const addItem = (arr: number[], item: number) => {
  arr.push(item); // mutates original array!
  return arr;
};

// Pure alternative: returns new array
const addItemPure = (arr: readonly number[], item: number): number[] => [
  ...arr,
  item,
];

Haskell 中的纯函数

-- All Haskell functions are pure by default
-- Side effects are tracked by the type system (IO monad)

add :: Int -> Int -> Int
add a b = a + b

double :: Int -> Int
double = (*2)  -- point-free style

-- Referential transparency:
-- double 5 can always be replaced with 10
-- This enables equational reasoning and compiler optimizations

2. 不可变性与持久化数据结构

不可变数据一旦创建就不能被修改。任何"变更"都会产生一个新的副本。这消除了共享可变状态带来的 bug,使并发安全,并简化了撤销/重做功能。持久化数据结构通过结构共享来高效地创建"修改后"的副本,避免了完全复制的性能开销。

TypeScript 中的不可变性

// 1. Object.freeze — shallow immutability
const config = Object.freeze({ host: "localhost", port: 3000 });
// config.port = 8080; // TypeError in strict mode

// 2. Readonly<T> — compile-time enforcement
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
}

// 3. ReadonlyArray and as const
const colors = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]

// 4. Spread-based updates (pure)
const updateUser = (user: User, name: string): User => ({
  ...user,
  name,
});

// 5. Immer — mutable API, immutable result
import { produce } from "immer";
const nextState = produce(state, (draft) => {
  draft.users[0].name = "Alice"; // looks mutable, but produces new object
});

Haskell/Scala 的默认不可变性

-- Haskell: all values are immutable by default
let xs = [1, 2, 3]
let ys = 0 : xs          -- [0, 1, 2, 3] — xs is unchanged
let zs = map (*2) xs     -- [2, 4, 6]   — xs is unchanged

// Scala: val (immutable) vs var (mutable)
val xs = List(1, 2, 3)          // immutable List
val ys = 0 :: xs                // List(0, 1, 2, 3)
val zs = xs.map(_ * 2)          // List(2, 4, 6)

// Scala case classes are immutable by default
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // new instance, alice unchanged

3. 高阶函数

高阶函数要么接受函数作为参数,要么返回函数,或者两者兼有。它们是函数式编程中代码复用的主要机制。map、filter 和 reduce 是三个最基本的高阶函数,几乎所有数据转换都可以用它们来表达。

// TypeScript higher-order functions
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map: transform each element
const doubled = numbers.map((n) => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter: select elements matching predicate
const evens = numbers.filter((n) => n % 2 === 0);
// [2, 4, 6, 8, 10]

// reduce: fold into single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 55

// Returning a function (function factory)
const multiplier = (factor: number) => (n: number) => n * factor;
const triple = multiplier(3);
triple(7); // 21

// Generic higher-order function
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}
const lengths = mapArray(["hello", "world"], (s) => s.length);
// [5, 5]

Haskell 中的高阶函数

-- Haskell: functions are curried by default
map (*2) [1..10]          -- [2,4,6,8,10,12,14,16,18,20]
filter even [1..10]       -- [2,4,6,8,10]
foldl (+) 0 [1..10]       -- 55

-- Function composition with (.)
sumOfDoubledEvens :: [Int] -> Int
sumOfDoubledEvens = foldl (+) 0 . map (*2) . filter even

-- ($) eliminates parentheses
print $ map (*2) $ filter even [1..10]
-- equivalent to: print (map (*2) (filter even [1..10]))

-- Custom higher-order function
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
applyTwice (+3) 10   -- 16
applyTwice (*2) 3    -- 12

4. 闭包

闭包是一个函数加上它引用的外部作用域中的变量。闭包"捕获"了创建时的环境,即使外部函数已经返回,被捕获的变量仍然存活。闭包是柯里化、偏函数应用和模块模式的基础。

// Closure: inner function captures variables from outer scope
function makeCounter(initial = 0) {
  let count = initial; // captured by returned functions
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}
const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12

// Closure for configuration (curried logger)
const createLogger = (prefix: string) => (msg: string) =>
  console.log(`[${prefix}] ${msg}`);

const dbLog = createLogger("DB");
dbLog("Connected");  // [DB] Connected

5. 柯里化与偏函数应用

柯里化将一个多参数函数转换为一系列单参数函数的链。偏函数应用固定一些参数后返回一个接受剩余参数的新函数。Haskell 中所有函数都是自动柯里化的;在 JavaScript/TypeScript 中需要手动实现或使用库。

// Manual currying in TypeScript
const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
add5(3);  // 8
add(2)(3); // 5

// Generic curry function (for 2-arg functions)
function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
  return (a: A) => (b: B) => fn(a, b);
}

const curriedMultiply = curry((a: number, b: number) => a * b);
const double = curriedMultiply(2);
double(10); // 20

// Partial application — fix first argument
function partial<A, B extends unknown[], C>(
  fn: (a: A, ...rest: B) => C,
  a: A
): (...rest: B) => C {
  return (...rest: B) => fn(a, ...rest);
}

const log = (level: string, module: string, msg: string) =>
  `[${level}][${module}] ${msg}`;

const errorLog = partial(log, "ERROR");
errorLog("DB", "Connection failed");
// [ERROR][DB] Connection failed

Haskell 的自动柯里化

-- All functions are curried: add :: Int -> Int -> Int = Int -> (Int -> Int)
add a b = a + b
add5 = add 5             -- partial application is trivial
add5 3                   -- 8

-- Sections: partially apply operators
map (*2) [1,2,3]         -- [2,4,6]
filter (>0) [-2,0,3,5]   -- [3,5]

6. 函数组合与管道

函数组合是将两个或多个函数合并成一个新函数,其中一个函数的输出成为下一个函数的输入。compose(f, g)(x) = f(g(x)) 从右到左执行,而 pipe(f, g)(x) = g(f(x)) 从左到右执行。组合让代码像搭积木一样构建复杂变换。

// compose: right-to-left
const compose = <T>(...fns: Array<(arg: T) => T>) =>
  (x: T): T => fns.reduceRight((acc, fn) => fn(acc), x);

// pipe: left-to-right (more readable)
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
  (x: T): T => fns.reduce((acc, fn) => fn(acc), x);

// Building a text processing pipeline
const trim = (s: string) => s.trim();
const toLower = (s: string) => s.toLowerCase();
const replaceSpaces = (s: string) => s.replace(/\s+/g, "-");

const slugify = pipe(trim, toLower, replaceSpaces);

slugify("  Hello World  "); // "hello-world"

Haskell 中的组合

-- (.) is the composition operator: (f . g) x = f (g x)
sumOfSquares :: [Int] -> Int
sumOfSquares = sum . map (^2)   -- point-free style

-- ($) eliminates parentheses: f $ x = f x
main = print $ sumOfSquares [1,2,3,4,5]  -- 55

-- Scala: f andThen g (left-to-right) or f compose g (right-to-left)

7. Monad:管理副作用与链式计算

Monad 是一种设计模式,用于将带有上下文(可能为空、可能失败、异步等)的计算进行组合。一个 Monad 需要实现两个操作:unit(也叫 return / of,将值包装进容器)和 flatMap(也叫 bind / chain / >>=,将函数应用到包装值并扁平化结果)。Promise 就是 JavaScript 中最常见的 Monad。

Maybe Monad(处理空值)

// Maybe monad in TypeScript
type Maybe<T> = { tag: "Just"; value: T } | { tag: "Nothing" };

const Just = <T>(value: T): Maybe<T> => ({ tag: "Just", value });
const Nothing = <T>(): Maybe<T> => ({ tag: "Nothing" });

// unit: wrap a value
const of = <T>(value: T | null | undefined): Maybe<T> =>
  value != null ? Just(value) : Nothing();

// flatMap (bind): chain computations
const flatMap = <T, U>(
  maybe: Maybe<T>,
  fn: (value: T) => Maybe<U>
): Maybe<U> =>
  maybe.tag === "Just" ? fn(maybe.value) : Nothing();

// map: apply function without flattening
const map = <T, U>(maybe: Maybe<T>, fn: (value: T) => U): Maybe<U> =>
  flatMap(maybe, (v) => Just(fn(v)));

// Usage: safe chained property access
const getCity = (co: { address?: { city?: string } }): Maybe<string> =>
  flatMap(of(co.address), (addr) => of(addr.city));

getCity({ address: { city: "NYC" } }); // { tag: "Just", value: "NYC" }
getCity({});                             // { tag: "Nothing" }

Either Monad(错误处理)

// Either: Right = success, Left = error
type Either<E, A> =
  | { tag: "Left"; error: E }
  | { tag: "Right"; value: A };

const Left = <E, A>(error: E): Either<E, A> => ({ tag: "Left", error });
const Right = <E, A>(value: A): Either<E, A> => ({ tag: "Right", value });

const flatMap = <E, A, B>(
  either: Either<E, A>, fn: (a: A) => Either<E, B>
): Either<E, B> =>
  either.tag === "Right" ? fn(either.value) : either;

// Railway-oriented programming: chain validations
const parseAge = (s: string): Either<string, number> => {
  const n = parseInt(s, 10);
  return isNaN(n) ? Left("Invalid number") : Right(n);
};
const validateRange = (n: number): Either<string, number> =>
  n >= 0 && n <= 150 ? Right(n) : Left("Out of range");

flatMap(parseAge("25"), validateRange); // { tag: "Right", value: 25 }

Haskell 中的 Monad

-- Monad typeclass: return + (>>=)
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

-- do notation (syntactic sugar for >>=)
result :: Maybe Double
result = do
  x <- Just 100
  y <- safeDivide x 5
  safeDivide y 2         -- Just 10.0

-- desugared: Just 100 >>= \x -> safeDivide x 5 >>= \y -> safeDivide y 2

8. Functor:可映射的容器

Functor 是一个实现了 map 操作的容器类型,它可以在不改变容器结构的前提下对内部值进行变换。数组、Maybe、Either、Promise 都是 Functor。Functor 必须遵守两条法则:恒等法则(map(id) === id)和组合法则(map(f . g) === map(f) . map(g))。

// Functor interface in TypeScript
interface Functor<A> {
  map<B>(fn: (a: A) => B): Functor<B>;
}

// Box functor: simplest possible container
class Box<A> implements Functor<A> {
  constructor(private value: A) {}

  map<B>(fn: (a: A) => B): Box<B> {
    return new Box(fn(this.value));
  }

  getValue(): A {
    return this.value;
  }
}

// Chain transformations
const result = new Box(5)
  .map((n) => n * 2)      // Box(10)
  .map((n) => n + 1)      // Box(11)
  .map(String)             // Box("11")
  .getValue();             // "11"

// Functor laws verification:
// 1. Identity: box.map(x => x) ≡ box
// 2. Composition: box.map(f).map(g) ≡ box.map(x => g(f(x)))

Haskell 中的 Functor

-- class Functor f where fmap :: (a -> b) -> f a -> f b

fmap (*2) (Just 5)      -- Just 10
fmap (*2) Nothing       -- Nothing
fmap (*2) [1,2,3]       -- [2,4,6]

-- <$> is infix fmap
(*2) <$> Just 5         -- Just 10

-- Custom Functor: derive fmap for your own types
data Tree a = Leaf a | Branch (Tree a) (Tree a)
instance Functor Tree where
  fmap f (Leaf x)     = Leaf (f x)
  fmap f (Branch l r) = Branch (fmap f l) (fmap f r)

9. 模式匹配

模式匹配是一种检查值的形状并解构数据的机制。它比 if/else 或 switch 更强大,因为它可以同时进行类型检查、解构和条件判断。Haskell 和 Scala 内置了完整的模式匹配并带有穷尽性检查。TypeScript 通过 discriminated unions 加 switch 可以实现类似效果。

TypeScript Discriminated Unions

// Algebraic Data Type via discriminated union
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

// Exhaustive pattern matching
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      // Exhaustiveness check: this will error if a case is missed
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Result type: another common discriminated union
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

Haskell 的模式匹配

data Shape = Circle Double | Rectangle Double Double | Triangle Double Double

area :: Shape -> Double
area (Circle r)      = pi * r^2
area (Rectangle w h) = w * h
area (Triangle b h)  = 0.5 * b * h

-- Guards: conditions within pattern matches
bmi :: Double -> String
bmi x | x < 18.5 = "Underweight" | x < 25.0 = "Normal" | otherwise = "Overweight"

-- Nested pattern matching
describe :: Maybe (Either String Int) -> String
describe Nothing          = "Empty"
describe (Just (Left s))  = "Error: " ++ s
describe (Just (Right n)) = "Value: " ++ show n

10. 代数数据类型 (ADTs)

代数数据类型通过两种方式组合类型:积类型(AND — 记录/元组,所有字段都存在)和和类型(OR — 标签联合,恰好是其中一种变体)。ADTs 的核心优势在于"让非法状态不可表示" — 通过类型系统强制约束,使运行时错误在编译期就被捕获。

// Product type: all fields present (AND)
type User = {
  id: string;
  name: string;
  email: string;
};

// Sum type: exactly one variant (OR)
type LoadingState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

// Making illegal states unrepresentable
// BAD: both data and error could be set simultaneously
type BadState = {
  loading: boolean;
  data: string | null;
  error: Error | null;
};

// GOOD: exactly one state at a time
function renderState<T>(state: LoadingState<T>): string {
  switch (state.status) {
    case "idle":    return "Ready";
    case "loading": return "Loading...";
    case "success": return `Data: ${String(state.data)}`;
    case "error":   return `Error: ${state.error.message}`;
  }
}

Haskell ADTs

-- Sum type (OR)
data Color = Red | Green | Blue

-- Product type (AND)
data Point = Point { x :: Double, y :: Double }

-- Parameterized ADTs
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b

-- Recursive ADT (binary tree)
data Tree a = Empty | Node a (Tree a) (Tree a)

-- Pattern match on ADTs
depth :: Tree a -> Int
depth Empty          = 0
depth (Node _ l r)   = 1 + max (depth l) (depth r)

11. 实用 FP 模式与库

在真实项目中,你可能不会从头实现 Maybe 和 Either。以下是 JavaScript/TypeScript 生态中主流的函数式编程库,它们提供了经过实战检验的函数式工具。

特点适合场景
fp-tsOption, Either, Task, IO, pipe, 类型类Haskell 风格的严格 FP
Effect-TSEffect 系统, 依赖注入, 并发, 流处理大型应用的副作用管理
Ramda自动柯里化, 数据在后, 镜头 (lenses)数据转换管道
Lodash/fpLodash 的 FP 版本, 自动柯里化, 不可变已用 Lodash 的项目迁移
Immer结构共享, 可变 API → 不可变结果React 状态管理
Zod运行时类型验证, parse-don't-validateAPI 边界验证

fp-ts 实战示例

import { pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";

// Option: safe property access
const getCity = (user: User): O.Option<string> =>
  pipe(
    O.fromNullable(user.address),
    O.flatMap((addr) => O.fromNullable(addr.city))
  );

// Either: validation pipeline
const validateInput = (input: string) =>
  pipe(
    E.right(input),
    E.flatMap((s) => s.length >= 5 ? E.right(s) : E.left("Too short")),
    E.flatMap((s) => s.includes("@") ? E.right(s) : E.left("Invalid email"))
  );

12. 语言对比:TypeScript vs Haskell vs Scala

下表对比了三种语言在函数式编程支持方面的差异。Haskell 是纯函数式语言,Scala 是多范式 JVM 语言,TypeScript 是多范式但偏向 FP 友好的 JavaScript 超集。

特性TypeScriptHaskellScala
纯函数式约定,不强制强制(IO monad)可选,不强制
不可变性Readonly, as const默认不可变val, case class
类型推断局部推断Hindley-Milner局部 + 高级推断
模式匹配switch + never内置 + 穷尽检查match 表达式
柯里化手动或使用库默认自动柯里化多参数列表
类型类无(用接口模拟)一等支持given/using (Scala 3)
ADTsDiscriminated unionsdata + |sealed trait + case class
学习曲线中等

13. 高级模式:Lens、Optics 与 Transducers

Lens(透镜)是一种可组合的 getter/setter 抽象,用于在不可变嵌套数据结构中优雅地读取和更新深层字段。Optics 是更广泛的概念,包括 Prism(处理和类型)、Traversal(遍历多个目标)等。Transducer 是可组合的转换,独立于数据源。

// Lens: composable getter/setter for nested immutable data
type Lens<S, A> = {
  get: (s: S) => A;
  set: (a: A) => (s: S) => S;
};

const lensProp = <S, K extends keyof S>(key: K): Lens<S, S[K]> => ({
  get: (s) => s[key],
  set: (a) => (s) => ({ ...s, [key]: a }),
});

// Compose lenses for deep nested access
const composeLens = <A, B, C>(
  ab: Lens<A, B>, bc: Lens<B, C>
): Lens<A, C> => ({
  get: (a) => bc.get(ab.get(a)),
  set: (c) => (a) => ab.set(bc.set(c)(ab.get(a)))(a),
});

// Usage: update deeply nested field immutably
const addressLens = lensProp<Company, "address">("address");
const cityLens = lensProp<Company["address"], "city">("city");
const companyCityLens = composeLens(addressLens, cityLens);

companyCityLens.get(company);           // "NYC"
companyCityLens.set("LA")(company);     // { ...company, address: { ...city: "LA" } }

14. 递归与尾调用优化

函数式编程用递归替代循环。尾递归(递归调用是函数的最后一个操作)可以被编译器优化为常量栈空间。Haskell 和 Scala 支持尾调用优化;JavaScript 仅 Safari 引擎支持 TCO。在 JS/TS 中,可使用 trampoline 模式模拟 TCO。

// Naive recursion: O(n) stack space — can overflow
const factorial = (n: number): number =>
  n <= 1 ? 1 : n * factorial(n - 1);

// Tail-recursive version: last operation is the recursive call
const factorialTail = (n: number, acc: number = 1): number =>
  n <= 1 ? acc : factorialTail(n - 1, n * acc);

// Trampoline: simulate TCO in JS environments without it
type Thunk<T> = T | (() => Thunk<T>);

function trampoline<T>(fn: Thunk<T>): T {
  let result = fn;
  while (typeof result === "function") {
    result = (result as () => Thunk<T>)();
  }
  return result;
}

const factTrampoline = (n: number, acc = 1): Thunk<number> =>
  n <= 1 ? acc : () => factTrampoline(n - 1, n * acc);

trampoline(factTrampoline(100000)); // Works without stack overflow!

Haskell/Scala 的递归

-- Haskell: tail-recursive with accumulator
factTail :: Integer -> Integer
factTail n = go n 1
  where go 0 acc = acc
        go n acc = go (n-1) (n*acc)

// Scala: @tailrec ensures compiler optimizes tail calls
import scala.annotation.tailrec
@tailrec
def factorial(n: Long, acc: Long = 1): Long =
  if (n <= 1) acc else factorial(n - 1, n * acc)

15. 函数式编程与 React

React 的核心设计深受函数式编程影响:组件是纯函数(props → JSX),状态是不可变的(通过 setState/useReducer 更新),Hooks 是高阶函数模式,React 18/19 的 Suspense 和 Server Components 体现了 Effect 系统的思想。

// React: FP patterns everywhere

// 1. Pure component: function from props to UI
const Greeting = ({ name }: { name: string }) => <h1>Hello, {name}!</h1>;

// 2. Reducer: pure function (state, action) => newState
type Action = { type: "inc" } | { type: "dec" } | { type: "reset" };
const reducer = (state: { count: number }, action: Action) => {
  switch (action.type) {
    case "inc":   return { count: state.count + 1 };
    case "dec":   return { count: state.count - 1 };
    case "reset": return { count: 0 };
  }
};

// 3. useMemo: memoize pure computation
const result = useMemo(() => expensiveCompute(data), [data]);

// 4. Immer: mutable API → immutable result
import { produce } from "immer";
const next = produce(state, (draft) => {
  draft.todos.push(newTodo); // looks mutable, produces new object
});

16. 常见陷阱与最佳实践

常见错误
  • 在 map/filter 回调中产生副作用
  • 忘记 Object.freeze 是浅冻结
  • 过度柯里化导致可读性下降
  • 无界递归导致栈溢出
  • 滥用 point-free 风格让代码难以理解
  • 在性能敏感的循环中使用 reduce 替代 for
最佳实践
  • 优先使用 pipe 而非嵌套函数调用
  • 用 TypeScript strict 模式确保类型安全
  • 小函数 + 组合 > 大函数
  • 用 ADTs 建模业务逻辑,让非法状态不可表示
  • 在团队项目中渐进式引入 FP,不要全盘重写
  • 为纯函数编写属性测试(property-based testing)

总结

函数式编程不仅仅是一种编程范式,更是一种思维方式。它教会我们如何通过纯函数、不可变数据和可组合的抽象来构建可靠、可维护的软件。你不需要切换到 Haskell 来享受 FP 的好处 — TypeScript 搭配 fp-ts 或 Effect-TS 已经可以在日常开发中应用大部分 FP 概念。从纯函数和不可变性开始,逐步引入 Maybe/Either 进行错误处理,用 pipe 和 compose 构建数据管道,最终你会发现代码变得更加可预测、可测试和可组合。

常见问题

What is a pure function and why does it matter?

A pure function always returns the same output for the same input and produces no side effects — no mutation of external state, no I/O, no randomness. Pure functions are easier to test, reason about, memoize, and parallelize. They form the foundation of functional programming and enable referential transparency, meaning any expression can be replaced with its value without changing program behavior.

What is immutability and how do I achieve it in JavaScript/TypeScript?

Immutability means data cannot be changed after creation. In JavaScript, use Object.freeze for shallow immutability, spread operators or structuredClone for copies, and const for bindings. In TypeScript, use Readonly<T>, ReadonlyArray<T>, and as const assertions. Libraries like Immer provide convenient immutable updates with a mutable API through structural sharing.

What is the difference between currying and partial application?

Currying transforms a function that takes multiple arguments into a chain of single-argument functions: f(a, b, c) becomes f(a)(b)(c). Partial application fixes some arguments and returns a function expecting the remaining ones: partial(f, a) returns g(b, c). Currying always produces unary functions, while partial application can produce functions of any arity. Haskell curries all functions by default.

What is a Monad and why should developers learn about it?

A Monad is a design pattern for composing computations that produce wrapped values. It must implement two operations: unit (wrapping a value) and flatMap/bind (chaining computations on wrapped values). Common monads include Maybe/Option (handling nulls), Either/Result (error handling), Promise (async operations), and List (non-determinism). Understanding monads helps manage side effects, error propagation, and async flows in a composable way.

How does function composition work and what are its benefits?

Function composition combines two or more functions to create a new function where the output of one becomes the input of the next: compose(f, g)(x) = f(g(x)). Pipe works left-to-right: pipe(f, g)(x) = g(f(x)). Benefits include code reuse, readability (describing what rather than how), testability (each piece is independently testable), and the ability to build complex transformations from simple building blocks.

What are algebraic data types (ADTs) and how are they used?

Algebraic data types (ADTs) combine types using sum types (OR — tagged unions/discriminated unions) and product types (AND — tuples/records). Sum types like type Shape = Circle | Rectangle model exclusive choices. Product types like { radius: number; color: string } combine multiple fields. ADTs enable exhaustive pattern matching, making illegal states unrepresentable. TypeScript supports ADTs via discriminated unions with a tag field.

What is pattern matching and how does it compare across languages?

Pattern matching is a mechanism for checking a value against patterns and destructuring data. Haskell and Scala have built-in pattern matching with exhaustiveness checking. TypeScript achieves similar results using switch on discriminated unions with the never type for exhaustiveness. The TC39 pattern matching proposal (Stage 1) aims to bring native pattern matching to JavaScript with a match expression syntax.

How does Haskell compare to TypeScript for functional programming?

Haskell is a purely functional language with lazy evaluation, algebraic data types, type classes, and monadic I/O by default. TypeScript is a multi-paradigm language that supports functional patterns through libraries and conventions but does not enforce purity or immutability. Haskell has superior type inference (Hindley-Milner), pattern matching, and function composition syntax. TypeScript has a larger ecosystem, lower learning curve, and better industry adoption for web development.

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSTypeScript to JavaScript🔒JavaScript Obfuscator

相关文章

TypeScript 类型守卫:运行时类型检查完全指南

掌握 TypeScript 类型守卫:typeof、instanceof、in、自定义类型守卫和可辨识联合。

软件设计模式指南:创建型、结构型与行为型模式

全面的设计模式指南,涵盖工厂、建造者、单例、适配器、装饰器、代理、外观、观察者、策略、命令、状态模式,附 TypeScript 和 Python 实例。

数据结构与算法指南:数组、树、图、哈希表与大 O 表示法

全面的数据结构与算法指南。学习数组、链表、树、图、哈希表、堆、栈、队列、大 O 分析、排序和搜索算法,附 TypeScript 和 Python 代码实例。