mcpField
F
Functional Programming Fundamentals

Functional Programming Fundamentals

github.com/whatiskadudoing开发14121 次安装39 周前更新
代码仓库

关于 Functional Programming Fundamentals

面向学习函数式编程的开发者,讲解纯函数、柯里化、组合、无点风格和一等函数等基础概念,并以 TypeScript 示例说明其应用。

主要功能

  1. 01纯函数与引用透明性
  2. 02柯里化与偏应用
  3. 03函数组合与管道
  4. 04无点风格
  5. 05一等函数与高阶函数

适用场景

  1. 01理解函数式编程核心概念
  2. 02在 TypeScript 中编写可组合函数
  3. 03学习 fp-ts 类型前打好基础

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/whatiskadudoing/fp-ts-skills --skill functional-programming-fundamentals

安装后在 Claude Code 中输入/functional-programming-fundamentals确认已加载。

详细介绍

函数式编程基础

本技能涵盖函数式编程的基础概念。在学习 fp-ts 中的 Option、Either 和 Task 等类型之前,请先掌握这些概念。这些概念适用于各种函数式编程语言和库。

为什么要使用函数式编程?

函数式编程具有以下优点:

  • 可预测性:纯函数对相同的输入始终产生相同的输出
  • 可测试性:无需模拟全局状态或依赖项
  • 可组合性:小函数可以组合成复杂行为
  • 可维护性:独立的部分更容易理解和修改
  • 并行化:纯函数可以安全地并发运行

1. 纯函数与引用透明性

什么是纯函数?

纯函数有两个特性:

  1. 相同输入,相同输出:给定相同的参数,它始终返回相同的结果
  2. 无副作用:它不会修改外部状态、执行 I/O 或造成可观察到的变化
为什么这很重要

纯函数具有以下优点:

  • 可缓存:由于结果具有确定性,因此可以进行记忆化
  • 可移植:不依赖外部状态中的隐藏依赖项
  • 可测试:无需设置或清理全局状态
  • 可并行化:可以安全地并发运行,不会出现竞态条件
  • 易于局部推理:无需了解整个系统,就能理解它们
示例
// PURE: Same input always gives same output
const add = (x: number, y: number): number => x + y

const greet = (name: string): string => `Hello, ${name}!`

const head = <T>(arr: readonly T[]): T | undefined => arr[0]

// IMPURE: Depends on external state
let counter = 0
const incrementCounter = (): number => {
  counter += 1  // Mutates external state
  return counter
}

// IMPURE: Different output for same input
const randomNumber = (): number => Math.random()

// IMPURE: Performs I/O
const logMessage = (msg: string): void => {
  console.log(msg)  // Side effect: writes to console
}

// IMPURE: Depends on mutable external state
const config = { apiUrl: 'https://api.example.com' }
const getApiUrl = (): string => config.apiUrl  // Could return different values
引用透明性

如果一个表达式可以替换为它的值而不改变程序行为,那么该表达式就具有引用透明性。

// Referentially transparent
const double = (x: number): number => x * 2
const result = double(5) + double(5)
// Can be replaced with: 10 + 10 = 20

// NOT referentially transparent
let count = 0
const incrementAndGet = (): number => {
  count += 1
  return count
}
const result2 = incrementAndGet() + incrementAndGet()
// Cannot replace with values: first call returns 1, second returns 2
// Result is 3, not 1 + 1 = 2
如何让非纯函数变为纯函数
// IMPURE: Modifies input
const addItem = (arr: string[], item: string): void => {
  arr.push(item)  // Mutates the original array
}

// PURE: Returns new array
const addItem = (arr: readonly string[], item: string): readonly string[] =>
  [...arr, item]

// IMPURE: Depends on Date
const getAge = (birthYear: number): number =>
  new Date().getFullYear() - birthYear

// PURE: Accept date as parameter (dependency injection)
const getAge = (birthYear: number, currentYear: number): number =>
  currentYear - birthYear

// IMPURE: Reads from database
const getUser = async (id: string): Promise<User> =>
  await database.find(id)

// Functional approach: Return a description of the effect
// (This is the concept behind Task and IO in fp-ts)
const getUser = (id: string): Task<User> =>
  () => database.find(id)
常见错误
// MISTAKE 1: Mutating arguments
const sortArray = (arr: number[]): number[] => {
  return arr.sort((a, b) => a - b)  // sort() mutates the original!
}

// FIX: Create a copy first
const sortArray = (arr: readonly number[]): number[] =>
  [...arr].sort((a, b) => a - b)

// MISTAKE 2: Using Date, Math.random, or other non-deterministic APIs
const generateId = (): string =>
  `id-${Date.now()}-${Math.random()}`

// FIX: Accept dependencies as parameters
const generateId = (timestamp: number, random: number): string =>
  `id-${timestamp}-${random}`

// MISTAKE 3: Hidden dependencies via closure
const multiplier = 2
const double = (x: number): number => x * multiplier  // Captures external variable

// FIX: Make dependencies explicit
const multiply = (factor: number) => (x: number): number => x * factor
const double = multiply(2)

2. 柯里化与部分应用

什么是柯里化?

柯里化会将一个接受多个参数的函数转换为一系列每次只接受一个参数的函数。

// Uncurried: takes all arguments at once
const add = (x: number, y: number, z: number): number => x + y + z
add(1, 2, 3)  // 6

// Curried: takes one argument at a time
const addCurried = (x: number) => (y: number) => (z: number): number => x + y + z
addCurried(1)(2)(3)  // 6
为什么这很重要

柯里化可以实现:

  • 部分应用:通过提供部分参数来创建专用函数
  • 组合:单参数函数可以轻松组合
  • 复用:根据一个定义创建一系列相关函数
  • 延迟计算:在参数可用时再提供它们
实践中的柯里化
// Curried function definition
const multiply = (x: number) => (y: number): number => x * y

// Create specialized functions through partial application
const double = multiply(2)
const triple = multiply(3)

double(5)  // 10
triple(5)  // 15

// Curried comparison helpers
const greaterThan = (threshold: number) => (value: number): boolean =>
  value > threshold

const isAdult = greaterThan(17)
const isExpensive = greaterThan(100)

isAdult(25)      // true
isExpensive(50)  // false
数组方法中的柯里化
// Curried filter predicate
const filterBy = <T>(predicate: (item: T) => boolean) =>
  (items: readonly T[]): T[] => items.filter(predicate)

// Create reusable filters
const keepPositive = filterBy((n: number) => n > 0)
const keepEven = filterBy((n: number) => n % 2 === 0)

keepPositive([-1, 2, -3, 4])  // [2, 4]
keepEven([1, 2, 3, 4, 5, 6])  // [2, 4, 6]

// Curried map transformer
const mapWith = <A, B>(fn: (a: A) => B) =>
  (items: readonly A[]): B[] => items.map(fn)

const doubleAll = mapWith((n: number) => n * 2)
const stringify = mapWith(String)

doubleAll([1, 2, 3])  // [2, 4, 6]
stringify([1, 2, 3])  // ['1', '2', '3']
部分应用与柯里化
// Partial application: fixing some arguments of a function
const greet = (greeting: string, name: string): string =>
  `${greeting}, ${name}!`

// Using bind for partial application (uncurried)
const sayHello = greet.bind(null, 'Hello')
sayHello('Alice')  // "Hello, Alice!"

// Curried version is naturally partially applicable
const greetCurried = (greeting: string) => (name: string): string =>
  `${greeting}, ${name}!`

const sayHello = greetCurried('Hello')
const sayGoodbye = greetCurried('Goodbye')

sayHello('Alice')    // "Hello, Alice!"
sayGoodbye('Bob')    // "Goodbye, Bob!"
参数顺序很重要
// BAD: Data first prevents easy partial application
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] => arr.map(fn)

// Must use anonymous function to specialize
const doubleAll = (arr: number[]) => map(arr, n => n * 2)

// GOOD: Data last enables clean partial application
const map = <A, B>(fn: (a: A) => B) => (arr: readonly A[]): B[] => arr.map(fn)

// Specialize by supplying the function
const doubleAll = map((n: number) => n * 2)
const stringify = map(String)

// This is why fp-ts uses data-last curried functions
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'

pipe(
  [1, 2, 3],
  A.map(n => n * 2),      // A.map takes fn first, returns function expecting array
  A.filter(n => n > 2)     // Same pattern
)
常见错误
// MISTAKE 1: Inconsistent argument order
const process = (config: Config) => (data: Data) => (options: Options) => ...
const handle = (data: Data) => (config: Config) => ...  // Inconsistent!

// FIX: Establish convention - typically: config -> options -> data
const process = (config: Config) => (options: Options) => (data: Data) => ...
const handle = (config: Config) => (data: Data) => ...

// MISTAKE 2: Over-currying simple functions
const add = (a: number) => (b: number) => (c: number) => (d: number) =>
  a + b + c + d

add(1)(2)(3)(4)  // Tedious to call

// FIX: Curry only to the level needed for your use case
const add = (a: number, b: number, c: number, d: number) => a + b + c + d
// Or group related parameters
const addPairs = (a: number, b: number) => (c: number, d: number) =>
  a + b + c + d

// MISTAKE 3: Forgetting to curry when using with pipe
const formatPrice = (currency: string, amount: number): string =>
  `${currency}${amount.toFixed(2)}`

pipe(
  100,
  formatPrice('$', ???)  // Can't use in pipe - needs both args!
)

// FIX: Curry with data last
const formatPrice = (currency: string) => (amount: number): string =>
  `${currency}${amount.toFixed(2)}`

pipe(
  100,
  formatPrice('$')  // Works!
)  // "$100.00"

3. 函数组合

什么是函数组合?

函数组合会将两个或多个函数合并为一个新函数。一个函数的输出会成为下一个函数的输入。

// Mathematical composition: (f . g)(x) = f(g(x))
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B) =>
  (x: A): C => f(g(x))

const addOne = (x: number): number => x + 1
const double = (x: number): number => x * 2

const addOneThenDouble = compose(double, addOne)
addOneThenDouble(5)  // double(addOne(5)) = double(6) = 12
为什么这很重要

组合可以实现:

  • 用简单部分构建复杂行为:小巧、专注的函数可以组合成强大的转换
  • 复用:组合后的函数还可以继续组合
  • 可读性:每一步都有清晰且单一的目的
  • 测试:可以单独测试各个小部分,组合在数学上是有保证的
Pipe 与 Flow(从左到右组合)

传统的数学组合从右向左读取,可能令人困惑。pipe 和 flow 提供了从左到右的组合方式。

import { pipe, flow } from 'fp-ts/function'

const addOne = (x: number): number => x + 1
const double = (x: number): number => x * 2
const toString = (x: number): string => `Value: ${x}`

// pipe: Execute immediately with a value
const result = pipe(
  5,
  addOne,     // 6
  double,     // 12
  toString    // "Value: 12"
)

// flow: Create a reusable function
const transform = flow(
  addOne,
  double,
  toString
)

transform(5)   // "Value: 12"
transform(10)  // "Value: 22"
多类型组合
// Functions don't need matching types, just compatible connections
const parseNumber = (s: string): number => parseInt(s, 10)
const isEven = (n: number): boolean => n % 2 === 0
const toYesNo = (b: boolean): string => b ? 'Yes' : 'No'

const isInputEven = flow(
  parseNumber,  // string -> number
  isEven,       // number -> boolean
  toYesNo       // boolean -> string
)

isInputEven('4')  // "Yes"
isInputEven('7')  // "No"
构建管道
import { pipe, flow } from 'fp-ts/function'

interface User {
  name: string
  email: string
  age: number
}

// Small, focused functions
const getName = (user: User): string => user.name
const toUpperCase = (s: string): string => s.toUpperCase()
const addGreeting = (name: string): string => `Hello, ${name}!`
const addExcitement = (s: string): string => `${s}!!!`

// Compose into a pipeline
const greetUser = flow(
  getName,
  toUpperCase,
  addGreeting,
  addExcitement
)

greetUser({ name: 'alice', email: '[email protected]', age: 30 })
// "Hello, ALICE!!!!"

// Or use pipe for one-off transformations
const greeting = pipe(
  { name: 'bob', email: '[email protected]', age: 25 },
  getName,
  toUpperCase,
  addGreeting
)
// "Hello, BOB!"
组合的结合律

组合满足结合律:(f . g) . h = f . (g . h)

const f = (x: number): number => x + 1
const g = (x: number): number => x * 2
const h = (x: number): number => x - 3

// These are equivalent:
const way1 = flow(flow(h, g), f)  // (f . g) . h
const way2 = flow(h, flow(g, f))  // f . (g . h)
const way3 = flow(h, g, f)        // Direct composition

way1(10)  // 15
way2(10)  // 15
way3(10)  // 15
常见错误
// MISTAKE 1: Composing functions with incompatible types
const toString = (n: number): string => String(n)
const double = (n: number): number => n * 2

const bad = flow(toString, double)  // Type error! double expects number, gets string

// MISTAKE 2: Trying to compose multi-argument functions
const add = (a: number, b: number): number => a + b
const double = (n: number): number => n * 2

const bad = flow(add, double)  // Won't work - add takes 2 args

// FIX: Curry first
const add = (a: number) => (b: number): number => a + b
const addFiveThenDouble = flow(add(5), double)
addFiveThenDouble(3)  // 16

// MISTAKE 3: Long unreadable pipelines
const result = pipe(
  data,
  fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8, fn9, fn10  // What does this do?
)

// FIX: Group related transformations and name them
const parseInput = flow(fn1, fn2, fn3)
const validateData = flow(fn4, fn5)
const formatOutput = flow(fn6, fn7, fn8, fn9, fn10)

const process = flow(parseInput, validateData, formatOutput)

4. Pointfree 风格

什么是 Pointfree 风格?

Pointfree(或称 tacit)编程在定义函数时不会显式提及其参数。“点”指的就是这些参数。

// Pointed: explicitly names the argument
const double = (x: number): number => x * 2

// Pointfree: no explicit argument
const double = multiply(2)  // Assuming multiply is curried
为什么这很重要

Pointfree 风格:

  • 减少冗余:专注于转换,而不是数据传递
  • 鼓励组合:自然适用于 flow/pipe
  • 揭示模式:让计算结构一目了然
  • 避免错误:无需担心变量名拼写错误或变量遮蔽
实践中的 Pointfree
import { pipe, flow } from 'fp-ts/function'
import * as A from 'fp-ts/Array'

// Pointed style
const getActiveUserNames = (users: User[]): string[] =>
  users
    .filter(user => user.isActive)
    .map(user => user.name)

// Pointfree style
const isActive = (user: User): boolean => user.isActive
const getName = (user: User): string => user.name

const getActiveUserNames = flow(
  A.filter(isActive),
  A.map(getName)
)

// Both work the same:
getActiveUserNames(users)
构建 Pointfree 函数
// Utility functions enable pointfree style
const prop = <T, K extends keyof T>(key: K) => (obj: T): T[K] => obj[key]
const equals = <T>(target: T) => (value: T): boolean => value === target
const gt = (threshold: number) => (value: number): boolean => value > threshold

interface Product {
  name: string
  price: number
  category: string
}

// Pointed
const getExpensiveElectronics = (products: Product[]): Product[] =>
  products.filter(p => p.category === 'electronics' && p.price > 100)

// Pointfree with helpers
const isElectronics = flow(prop<Product, 'category'>('category'), equals('electronics'))
const isExpensive = flow(prop<Product, 'price'>('price'), gt(100))
const both = <T>(f: (t: T) => boolean, g: (t: T) => boolean) =>
  (t: T): boolean => f(t) && g(t)

const getExpensiveElectronics = A.filter(both(isElectronics, isExpensive))
Pointfree 风格适用的场景
// GOOD: Simple transformations
const doubleAll = A.map((n: number) => n * 2)  // Semi-pointfree
const keepPositive = A.filter((n: number) => n > 0)

// BETTER: With named predicates
const double = (n: number): number => n * 2
const isPositive = (n: number): boolean => n > 0

const doubleAll = A.map(double)      // Fully pointfree
const keepPositive = A.filter(isPositive)

// Compose them
const processNumbers = flow(
  keepPositive,
  doubleAll
)
何时避免 Pointfree
// AVOID: When it obscures meaning
const mystery = flow(
  A.map(flow(prop('x'), add(1))),
  A.filter(flow(prop('y'), gt(5))),
  A.reduce(0, flow(([acc, item]) => acc + item.z))
)

// PREFER: Clear variable names when logic is complex
const incrementX = (item: Item): Item => ({ ...item, x: item.x + 1 })
const hasLargeY = (item: Item): boolean => item.y > 5
const sumZ = (items: Item[]): number =>
  items.reduce((acc, item) => acc + item.z, 0)

const process = flow(
  A.map(incrementX),
  A.filter(hasLargeY),
  sumZ
)

// AVOID: Pointfree gymnastics
const weird = flip(compose(flip(map), filter))(isEven)(double)

// PREFER: Readable code
const doubleEvens = flow(
  A.filter(isEven),
  A.map(double)
)
常见错误
// MISTAKE 1: Forcing pointfree when it's unclear
const process = flow(
  A.map(flow(
    juxt([prop('a'), prop('b')]),
    apply(add)
  ))
)

// FIX: Use pointed style when clearer
const process = A.map(({ a, b }) => a + b)

// MISTAKE 2: Missing type annotations in pointfree
const double = A.map(n => n * 2)  // n is 'unknown' without context

// FIX: Add type annotation
const double = A.map((n: number) => n * 2)

// MISTAKE 3: Mixing pointed and pointfree awkwardly
const process = (arr: number[]) => flow(
  A.filter(n => n > 0),
  A.map(n => n * 2)
)(arr)

// FIX: Choose one style
// Pointfree:
const process = flow(
  A.filter((n: number) => n > 0),
  A.map(n => n * 2)
)

// Or pointed:
const process = (arr: number[]): number[] =>
  pipe(arr, A.filter(n => n > 0), A.map(n => n * 2))

5. 一等函数

什么是一等函数?

在 JavaScript/TypeScript 中,函数是一等公民。它们可以:

  • 赋值给变量
  • 作为参数传递
  • 由其他函数返回
  • 存储在数据结构中
为什么这很重要

一等函数支持:

  • 高阶函数:接收或返回函数的函数
  • 回调:将行为作为数据传递
  • 闭包:捕获其环境的函数
  • 函数组合:通过组合函数构建程序
避免包装函数

一个常见的反模式是不必要地包装函数。

// BAD: Unnecessary wrapper function
const numbers = [1, 2, 3, 4, 5]

// Don't do this:
numbers.map(x => double(x))
numbers.filter(x => isPositive(x))
numbers.forEach(x => console.log(x))

// GOOD: Pass the function directly
numbers.map(double)
numbers.filter(isPositive)
numbers.forEach(console.log)

// The wrapper adds nothing but noise
何时需要包装函数
// NEEDED: When you need to modify arguments
const users = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]

// getName expects a User, but we want just the index
users.map((user, index) => `${index}: ${user.name}`)  // Wrapper needed

// NEEDED: When you need to ignore arguments
const fetchAll = (urls: string[]) =>
  urls.map(url => fetch(url))  // fetch has many optional params

// If we did urls.map(fetch), TypeScript might complain about
// the (value, index, array) signature of map's callback

// NEEDED: When changing arity
const add = (a: number, b: number): number => a + b
[1, 2, 3].map(n => add(n, 10))  // Must wrap since map passes 3 args

// BETTER: Curry the function
const add = (b: number) => (a: number): number => a + b
[1, 2, 3].map(add(10))  // [11, 12, 13]
高阶函数
// Function that returns a function
const multiply = (x: number) => (y: number): number => x * y
const double = multiply(2)
const triple = multiply(3)

// Function that takes a function
const applyTwice = <T>(fn: (t: T) => T) => (value: T): T =>
  fn(fn(value))

const addOne = (n: number): number => n + 1
const addTwo = applyTwice(addOne)
addTwo(5)  // 7

// Function that does both
const compose = <A, B, C>(f: (b: B) => C) => (g: (a: A) => B) => (x: A): C =>
  f(g(x))

const increment = (n: number): number => n + 1
const toString = (n: number): string => String(n)

const incrementThenStringify = compose(toString)(increment)
incrementThenStringify(5)  // "6"
方法引用
// Be careful with method references and 'this'

// PROBLEM: Losing 'this' context
const obj = {
  value: 42,
  getValue() {
    return this.value
  }
}

const getValue = obj.getValue
getValue()  // undefined or error - 'this' is lost

// SOLUTION 1: Bind the method
const getValue = obj.getValue.bind(obj)
getValue()  // 42

// SOLUTION 2: Arrow function wrapper
const getValue = () => obj.getValue()
getValue()  // 42

// SOLUTION 3: Use static functions (FP approach)
const getValue = (obj: { value: number }): number => obj.value
getValue({ value: 42 })  // 42
常见错误
// MISTAKE 1: Unnecessary wrappers
const names = users.map(user => getName(user))
// FIX:
const names = users.map(getName)

// MISTAKE 2: Wrapping to match types incorrectly
const numbers = ['1', '2', '3'].map(s => parseInt(s))  // Seems fine...
// Actually problematic: parseInt takes (string, radix)
// map passes (value, index, array)
// parseInt('2', 1) is NaN!

// FIX: Explicit wrapper when semantics differ
const numbers = ['1', '2', '3'].map(s => parseInt(s, 10))
// Or use Number:
const numbers = ['1', '2', '3'].map(Number)

// MISTAKE 3: Not leveraging first-class functions
const process = (items: Item[], transform: (item: Item) => Item) => {
  const result = []
  for (const item of items) {
    result.push(transform(item))
  }
  return result
}
// FIX: Just use map
const process = (items: Item[], transform: (item: Item) => Item) =>
  items.map(transform)

// Or even simpler - don't wrap map at all:
items.map(transform)

// MISTAKE 4: Immediately invoking returned functions
const createAdder = (x: number) => (y: number) => x + y

// Wrong - this calls the inner function immediately with undefined
const add5 = createAdder(5)()  // NaN

// Right - store the function for later use
const add5 = createAdder(5)
add5(3)  // 8

实践练习

练习 1:纯函数

找出这些函数不纯的原因,并将它们重写为纯函数。

// 1a. Makes HTTP call
const fetchUser = async (id: string) => {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// 1b. Uses current time
const isExpired = (expiryDate: Date) => {
  return expiryDate < new Date()
}

// 1c. Mutates input
const addToCart = (cart: CartItem[], item: CartItem) => {
  cart.push(item)
  return cart
}

// 1d. Depends on global config
const config = { taxRate: 0.08 }
const calculateTotal = (subtotal: number) => {
  return subtotal * (1 + config.taxRate)
}
答案
// 1a. Return a description of the effect (Task pattern)
type FetchUser = (id: string) => () => Promise<User>
const fetchUser: FetchUser = (id) => () =>
  fetch(`/api/users/${id}`).then(r => r.json())

// 1b. Accept current time as parameter
const isExpired = (expiryDate: Date, now: Date): boolean =>
  expiryDate < now

// 1c. Return new array instead of mutating
const addToCart = (cart: readonly CartItem[], item: CartItem): CartItem[] =>
  [...cart, item]

// 1d. Accept config as parameter
const calculateTotal = (taxRate: number) => (subtotal: number): number =>
  subtotal * (1 + taxRate)

// Or group them
interface TaxConfig { taxRate: number }
const calculateTotal = (config: TaxConfig, subtotal: number): number =>
  subtotal * (1 + config.taxRate)
练习 2:柯里化和偏应用

将这些函数转换为柯里化形式,并创建专用版本。

// 2a. Convert to curried form
const formatDate = (locale: string, options: Intl.DateTimeFormatOptions, date: Date): string =>
  date.toLocaleDateString(locale, options)

// Create: formatUSDate, formatShortDate

// 2b. Convert to curried form
const clamp = (min: number, max: number, value: number): number =>
  Math.max(min, Math.min(max, value))

// Create: clampPercentage (0-100), clampByte (0-255)

// 2c. Convert to curried form with good argument order
const hasProperty = (obj: object, key: string): boolean =>
  key in obj

// Should work with: users.filter(hasProperty(???))
答案
// 2a.
const formatDate = (locale: string) =>
  (options: Intl.DateTimeFormatOptions) =>
    (date: Date): string =>
      date.toLocaleDateString(locale, options)

const formatUSDate = formatDate('en-US')
const formatShortDate = formatUSDate({ month: 'short', day: 'numeric' })

formatShortDate(new Date())  // "Jan 29"

// 2b.
const clamp = (min: number) => (max: number) => (value: number): number =>
  Math.max(min, Math.min(max, value))

const clampPercentage = clamp(0)(100)
const clampByte = clamp(0)(255)

clampPercentage(150)  // 100
clampByte(-10)        // 0

// 2c. Reorder arguments: key first (config), object last (data)
const hasProperty = (key: string) => (obj: object): boolean =>
  key in obj

const hasEmail = hasProperty('email')
const hasId = hasProperty('id')

users.filter(hasEmail)  // Users with email property
练习 3:函数组合

使用 flow 和 pipe 构建这些管道。

import { pipe, flow } from 'fp-ts/function'
import * as A from 'fp-ts/Array'

interface Product {
  name: string
  price: number
  category: string
  inStock: boolean
}

// 3a. Create a pipeline that:
// - Filters to products in stock
// - Filters to products under $50
// - Maps to product names
// - Sorts alphabetically

// 3b. Create a reusable pipeline that:
// - Takes a string
// - Trims whitespace
// - Converts to lowercase
// - Replaces spaces with hyphens
// - Prefixes with "slug-"

// 3c. Create a number processing pipeline that:
// - Filters out negative numbers
// - Doubles each number
// - Sums all numbers
// - Returns "Total: X"
答案
// 3a.
const getAffordableInStockNames = (products: Product[]): string[] =>
  pipe(
    products,
    A.filter(p => p.inStock),
    A.filter(p => p.price < 50),
    A.map(p => p.name),
    A.sort((a, b) => a.localeCompare(b))
  )

// Or as a reusable function:
const getAffordableInStockNames = flow(
  A.filter((p: Product) => p.inStock),
  A.filter(p => p.price < 50),
  A.map(p => p.name),
  A.sort<string>((a, b) => a.localeCompare(b))
)

// 3b.
const slugify = flow(
  (s: string) => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/\s+/g, '-'),
  s => `slug-${s}`
)

slugify('  Hello World  ')  // "slug-hello-world"

// 3c.
const isPositive = (n: number): boolean => n >= 0
const double = (n: number): number => n * 2
const sum = (numbers: number[]): number =>
  numbers.reduce((acc, n) => acc + n, 0)
const formatTotal = (n: number): string => `Total: ${n}`

const processNumbers = flow(
  A.filter(isPositive),
  A.map(double),
  sum,
  formatTotal
)

processNumbers([-1, 2, 3, -4, 5])  // "Total: 20"
练习 4:Pointfree 风格

在适当的情况下,将这些函数重构为 Pointfree 风格。

// 4a. Refactor to pointfree
const getAdultNames = (users: User[]): string[] =>
  users
    .filter(user => user.age >= 18)
    .map(user => user.name)

// 4b. Refactor to pointfree
const sumPrices = (products: Product[]): number =>
  products.reduce((total, product) => total + product.price, 0)

// 4c. Decide: Should this be pointfree or not? Why?
const formatUserForDisplay = (user: User): string =>
  `${user.name} (${user.email}) - ${user.isActive ? 'Active' : 'Inactive'}`
答案
// 4a. Good candidate for pointfree
const isAdult = (user: User): boolean => user.age >= 18
const getName = (user: User): string => user.name

const getAdultNames = flow(
  A.filter(isAdult),
  A.map(getName)
)

// 4b. Partially pointfree - the reducer is clearer pointed
const prop = <T, K extends keyof T>(key: K) => (obj: T): T[K] => obj[key]
const sum = (numbers: number[]): number =>
  numbers.reduce((a, b) => a + b, 0)

const sumPrices = flow(
  A.map(prop<Product, 'price'>('price')),
  sum
)

// Or keep it simple:
const getPrice = (p: Product): number => p.price
const sumPrices = flow(A.map(getPrice), sum)

// 4c. Keep pointed - the string template is clearer with explicit access
const formatUserForDisplay = (user: User): string =>
  `${user.name} (${user.email}) - ${user.isActive ? 'Active' : 'Inactive'}`

// Pointfree version would be overly complex:
const formatUserForDisplay = flow(
  user => [user.name, user.email, user.isActive] as const,
  ([name, email, isActive]) =>
    `${name} (${email}) - ${isActive ? 'Active' : 'Inactive'}`
)
// This is worse - no benefit, harder to read
练习 5:一等函数

修复这些不必要的包装,并改进代码。

// 5a. Remove unnecessary wrappers
const results = items
  .map(item => processItem(item))
  .filter(result => isValid(result))
  .forEach(result => logResult(result))

// 5b. Fix the parseInt issue
const numbers = ['1', '2', '3', '10', '11'].map(parseInt)
// Current result: [1, NaN, NaN, 3, 4] - Why? Fix it.

// 5c. Create a safe version of this
const handlers = {
  onClick: (e: Event) => handleClick(e),
  onSubmit: (e: Event) => handleSubmit(e),
  onHover: (e: Event) => handleHover(e),
}
// Too repetitive - can we do better?
答案
// 5a. Pass functions directly
const results = items
  .map(processItem)
  .filter(isValid)
  .forEach(logResult)

// 5b. parseInt receives (value, index, array) from map
// parseInt('2', 1) interprets '2' as base-1 (invalid) = NaN
// parseInt('3', 2) interprets '3' as base-2 (invalid) = NaN
// parseInt('10', 3) interprets '10' as base-3 = 3
// parseInt('11', 4) interprets '11' as base-4 = 5

// Fix: Use wrapper with explicit radix
const numbers = ['1', '2', '3', '10', '11'].map(s => parseInt(s, 10))
// Or use Number (doesn't have radix parameter)
const numbers = ['1', '2', '3', '10', '11'].map(Number)
// Result: [1, 2, 3, 10, 11]

// 5c. If handlers match the expected signature, just use them directly
const handlers = {
  onClick: handleClick,
  onSubmit: handleSubmit,
  onHover: handleHover,
}

// Or if you need to create them dynamically:
const createHandlers = <T extends Record<string, (e: Event) => void>>(
  handlerMap: T
): T => handlerMap

const handlers = createHandlers({
  onClick: handleClick,
  onSubmit: handleSubmit,
  onHover: handleHover,
})

总结

概念核心思想优点
纯函数相同输入 = 相同输出,无副作用可预测、可测试、可缓存
柯里化将多参数函数转换为单参数函数链偏应用、函数组合
组合将小函数组合成更大的函数可复用、模块化
Pointfree定义函数时不命名参数减少冗余,揭示结构
一等函数函数作为值高阶函数、回调

后续步骤

掌握这些基础知识后,你可以继续学习:

  1. fp-ts Option 和 Either:函数式错误处理
  2. fp-ts Pipe 和 Flow:高级组合模式
  3. fp-ts Task 和 TaskEither:异步函数式编程
  4. 单子和函子:fp-ts 背后的代数结构

请记住:函数式编程旨在用简单、可组合的部分构建复杂行为。从小处着手,练习组合,并随着高级模式在代码中证明其价值,逐步采用它们。

评论

支持 Markdown