JavaScript 轻量级函数式编程

coderljw 2024-10-13 大约 9 分钟

# 1. 管理函数的输入

  • 偏函数:偏函数严格来讲是一个减少函数参数个数的过程。
function partial(fn, ...presetArgs) {
  return function partiallyApplied(...laterArgs) {
    return fn(...presetArgs, ...laterArgs)
  }
}

function add(x, y) {
  return x + y
}

[1, 2, 3, 4, 5].map(partial(add, 3)) // => [4, 5, 6, 7, 8]
1
2
3
4
5
6
7
8
9
10
11
  • 将实参顺序颠倒。

    只有在传两个实参(匹配到 x 和 y 形参)调用 f(..) 函数时,"z:last" 这个值才能被赋给函数的形参 z。在其他的例子里,不管左边有多少个实参,"z:last" 都被传给最右的实参。

    function reverseArgs(fn) {
      return function argsReversed(...args) {
        return fn(...args.reverse())
      }
    }
    
    function partialRight(fn, ...presetArgs) {
      return reverseArgs(partial(reverseArgs(fn), ...presetArgs.reverse()))
    }
    
    function foo(x, y, z) {
      var rest = [].slice.call(arguments, 3)
      console.log(x, y, z, rest)
    }
    
    var f = partialRight(foo, 'z:last')
    
    f(1, 2) // => 1 2 'z:last' []
    
    f(1) // => 1 'z:last' undefined []
    
    f(1, 2, 3) // => 1 2 3 ['z:last']
    
    f(1, 2, 3, 4) // => 1 2 3 [4,'z:last']
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
  • 柯里化:柯里化将一个多参数(higher-arity)函数拆解为一系列的单元链式函数。

  • 柯里化和偏应用相似,每个类似偏应用的连续柯里化调用都把另一个实参应用到原函数,一直到所有实参传递完毕。不同之处在于,curriedAjax(..) 函数会明确地返回一个期望只接收下一个实参 data 的函数(我们把它叫做 curriedGetPerson(..)),而不是那个能接收所有剩余实参的函数(像此前的 getPerson(..) 函数)。

    • 严格柯里化(一次处理 {1, 1} 个参数)。
    function curry(fn, arity = fn.length) {
      return (function nextCurried(prevArgs) {
        return function curried(nextArg) {
          var args = prevArgs.concat([nextArg])
    
          if (args.length >= arity) {
            return fn(...args)
          } else {
            return nextCurried(args)
          }
        }
      })([])
    }
    
    function add(x, y) {
      return x + y
    }
    
    [1, 2, 3, 4, 5].map(curry(add)(3)) // => [4, 5, 6, 7, 8]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    • 松散柯里化(一次处理 {0, } 个参数)。
    function looseCurry(fn, arity = fn.length) {
      return (function nextCurried(prevArgs) {
        return function curried(...nextArgs) {
          var args = prevArgs.concat(nextArgs)
    
          if (args.length >= arity) {
            return fn(...args)
          } else {
            return nextCurried(args)
          }
        }
      })([])
    }
    
    function add(x, y, z) {
      return x + y + z
    }
    
    looseCurry(add)(2)(3, 5) // => 10
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • 反柯里化:拿到一个柯里化后的函数,却想要它柯里化之前的版本 —— 这本质上就是想将类似 f(1)(2)(3) 的函数变回类似 g(1,2,3) 的函数。

function uncurrying(fn) {
  return function uncurried(...args) {
    var ret = fn

    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i])
    }

    return ret
  }
}

function sum(...args) {
  var sum = 0
  for (let i = 0; i < args.length; i++) {
    sum += args[i]
  }
  return sum
}

var curriedSum = curry(sum, 5)
var uncurriedSum = uncurry(curriedSum)

curriedSum(1)(2)(3)(4)(5) // => 15

uncurriedSum(1, 2, 3, 4, 5) // => 15
uncurriedSum(1, 2, 3)(4)(5) // => 15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 只要一个实参:强制把一个函数处理成单参数函数(unary)。
function unary(fn) {
  return function onlyOneArg(arg) {
    return fn(arg)
  }
}

[10, 10, 10, 10, 10].map(parseInt) // => [10, NaN, 2, 3, 4]
[10, 10, 10, 10, 10].map(unary(parseInt)) // => [10, 10, 10, 10, 10]
1
2
3
4
5
6
7
8

# 2. 组合函数

  • 函数组合是一种定义函数的模式,它能将一个函数调用的输出路由到另一个函数的调用上,然后一直进行下去。

  • 因为 JS 函数只能返回单个值,这个模式本质上要求所有组合中的函数(可能第一个调用的函数除外)是一元的,当前函数从上一个函数输出中只接收一个输入。

  • 相较于在我们的代码里详细列出每个调用,函数组合使用 compose(..) 实用函数来提取出实现细节,让代码变得更可读,让我们更关注组合完成的是什么,而不是它具体做什么。

  • 组合 —— 声明式数据流 —— 是支撑函数式编程其他特性的最重要的工具之一。

  • 之前学习过组合函数,阅读本书组合函数时与现存理解相仿!

# 3. 副作用

  • 当我们在阅读程序的时候,能够清晰明确的识别每一个起因和每一个结果是非常重要的。在某种程度上,通读程序但不能看到因果的直接关系,程序的可读性就会降低。
function foo(x) {
  y = x * 2
}

var y

foo(3)
1
2
3
4
5
6
7
  • 编程中的幂等:幂等的面向程序的定义也是类似的,但不太正式。编程中的幂等仅仅是 f(x); 的结果与 f(x); f(x) 相同而不是要求 f(x) === f(f(x))。换句话说,之后每一次调用 f(x) 的结果和第一次调用 f(x) 的结果没有任何改变。
// 幂等的:
obj.count = 2
a[a.length - 1] = 42
person.name = upper(person.name)

// 非幂等的:
obj.count++
a[a.length] = 42
person.lastUpdated = Date.now()
1
2
3
4
5
6
7
8
9
  • 纯粹的快乐:没有副作用的函数称为纯函数。在编程的意义上,纯函数是一种幂等函数,因为它不可能有任何副作用。

  • 表达一个函数的纯度的另一种常用方法是:给定相同的输入(一个或多个),它总是产生相同的输出。

function add(x, y) {
  return x + y
}
1
2
3
  • 副作用对代码的可读性和质量都有害,因为它们使您的代码难以理解。副作用也是程序中最常见的错误原因之一,因为很难应对他们。幂等是通过本质上创建仅有一次的操作来限制副作用的一种策略。

  • 避免副作用的最优方法是使用纯函数。纯函数给定相同输入时总返回相同输出,并且没有副作用。引用透明更近一步的状态是 —— 更多的是一种脑力运动而不是文字行为 —— 纯函数的调用是可以用它的输出来代替,并且程序的行为不会被改变。

# 4. 闭包 VS 对象

  • 德高望重的大师 Qc Na 曾经和他的学生 Anton 一起散步。Anton 希望引导大师到一个讨论里,说到:大师,我曾听说对象是一个非常好的东西,是这样么?Qc Na 同情地看着他的学生回答到, “愚笨的弟子,对象只不过是可怜人的闭包”

  • 被批评后,Anton 离开他的导师并回到了自己的住处,致力于学习闭包。他认真的阅读整个“匿名函数:终极……”系列论文和它的姐妹篇,并且实践了一个基于闭包系统的小的 Scheme 解析器。他学了很多,盼望展现给他导师他的进步。

  • 当他下一次与 Qc Na 一同散步时,Anton 试着提醒他的导师,说到 “导师,我已经勤奋地学习了这件事,我现在明白了对象真的是可怜人的闭包。” ,Qc Na 用棍子戳了戳 Anton 回应到,“你什么时候才能学会,闭包才是可怜人的对象”。在那一刻, Anton 明白了什么。

# 5. 列表操作

  • 映射。

    mapperFn, arr 的参数顺序,乍一看像是在倒退。但是这种方式在函数式编程类库中非常常见。因为这样做,可以让这些实用函数更容易被组合。

function map(mapperFn, arr) {
  var newList = []

  for (let idx = 0; idx < arr.length; idx++) {
    newList.push(mapperFn(arr[idx], idx, arr))
  }

  return newList
}
1
2
3
4
5
6
7
8
9
  • 函子:函子是采用运算函数有效用操作的值。

  • 如果问题中的值是复合的,意味着它是由单个值组成,就像数组中的情况一样。例如,函子在每个单独的值上执行操作函数。函子实用函数创建的新值是所有单个操作函数执行的结果的组合。

    字符串函子是一个字符串加上一个实用函数,这个实用函数在字符串的所有字符上执行某些函数操作,返回包含处理过的字符的字符串。

    function uppercaseLetter(c) {
      var code = c.charCodeAt(0)
    
      // 小写字母?
      if (code >= 97 && code <= 122) {
        // 转换为大写!
        code = code - 32
      }
    
      return String.fromCharCode(code)
    }
    
    function stringMap(mapperFn, str) {
      return [...str].map(mapperFn).join('')
    }
    
    stringMap(uppercaseLetter, 'Hello World!') // 'HELLO WORLD!'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 过滤器。

function filter(predicateFn, arr) {
  var newList = []

  for (let idx = 0; idx < arr.length; idx++) {
    if (predicateFn(arr[idx], idx, arr)) {
      newList.push(arr[idx])
    }
  }

  return newList
}
1
2
3
4
5
6
7
8
9
10
11
  • Reduce。
function reduce(reducerFn, initialValue, arr) {
  var acc, startIdx

  if (arguments.length == 3) {
    acc = initialValue
    startIdx = 0
  } else if (arr.length > 0) {
    acc = arr[0]
    startIdx = 1
  } else {
    throw new Error('Must provide at least one value.')
  }

  for (let idx = startIdx; idx < arr.length; idx++) {
    acc = reducerFn(acc, arr[idx], idx, arr)
  }

  return acc
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 6. 递归

  • 相互递归:如果在一个递归循环中,出现两个及以上的函数相互调用,则称之为相互递归。

    实现斐波纳契数列。

    function fib_(n) {
      if (n == 1) return 1
      else return fib(n - 2)
    }
    
    function fib(n) {
      if (n == 0) return 0
      else return fib(n - 1) + fib_(n)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 递归的优点是它更具声明性,因此通常更易于阅读。缺点通常是性能方面,但是相比执行速度,更多的限制在于内存方面。

# 7. Transducing (opens new window)

  • Transduce 就是通过减少来转换。更具体点,transduer 是可组合的 reducer。

  • 我们使用转换来组合相邻的 map(..)、filter(..) 和 reduce(..) 操作。我们首先将 map(..) 和 filter(..) 表示为 reduce(..),然后抽象出常用的组合操作来创建一个容易组合的一致的 reducer 生成函数。

  • transducing 主要提高性能,如果在延迟序列(异步 observables)中使用,则这一点尤为明显。

  • 但是更广泛地说,transducing 是我们针对那些不能被直接组合的函数,使用的一种更具声明式风格的方法。否则这些函数将不能直接组合。如果使用这个技术能像使用本书中的所有其他技术一样用的恰到好处,代码就会显得更清晰,更易读! 使用 transducer 进行单次 reduce(..) 调用比追踪多个 reduce(..) 调用更容易理解。

# 8. Monad

  • Monad 的核心思想是,它必须对所有的值都是有效的,不能对值做任何检查 —— 甚至是空值检查。

  • Monad 是一个值类型,一个接口,一个有封装行为的对象数据结构。

  • 但是这些定义中没有一个是有用的。这里尝试做一个更好的解释:Monad 是一个用更具有声明式的方式围绕一个值来组织行为的方法。

  • 阮一峰 (opens new window):简单说,Monad 就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

以父之名
周杰伦.mp3