



&& 和 || 是语法形式,而非运算符;它们右边的表达式并不总是会被求值。将一个大程序拆分为若干小块,并分别构建。
通过函数调用,无需任何循环语法就可以构建迭代结构。
参见应用序求值
关键在于每个函数都要完成一项可识别的任务,并且能够作为模块在定义其他函数时加以使用。(如何完成最大的任务?将其分解成小块;如何完成这些小块?…)
{} 内声明所有变量和函数,使边界更加清晰(_词法作用域_,封装)。线性递归(shape):一条延迟操作的链,解释器跟踪稍后要执行的操作。链的长度为 n。时间复杂度 O(n),空间复杂度 O(n)。
线性迭代(linear iterative):无需增长和收缩。状态可以由固定数量的_状态变量_概括,以及描述状态如何从一个状态更新到另一个状态的固定规则,以及一个(可选的)指定终止条件的结束测试。时间复杂度 O(n),空间复杂度 O(1)。
区别:迭代过程不需要解释器存储某些”隐藏”状态,所有描述都由状态变量提供。而递归过程借助解释器存储状态,链越长,解释器需要维护的状态就越多。
递归函数(recursive function):指在语法上引用自身的函数,与递归过程无关。递归函数也可以是迭代过程。
递归过程(recursive process):函数的一种演化方式,将状态留给解释器,并调用下一个函数。
尾递归(tail-recursive):常见语言即使在迭代过程的递归函数中也会消耗空间。但 JavaScript 具有尾递归优化,消耗常数空间。借助尾递归实现,迭代可以使用普通的函数调用机制来表达,特殊的迭代构造只作为语法糖存在。
函数调用自身两次会形成一棵二叉树。我们可以用树状图来分析时间复杂度。(本质上是关于如何在其他函数之上演化函数。)
树形递归对于树状结构数据或图来说非常有用,且比迭代过程更直观。
改为迭代过程:从下往上思考,f(n) = f(n-1) + f(n-2),所以 f(n) + f(n-1) = f(n+1),我们需要两个状态来存储 n 和 n-1,以及一个限制条件计数。
1 | function fib(n){ |
我们能用多少种不同的方式找开 1 美元(100 美分),已知有半美元、25 美分、10 美分、5 美分和 1 美分(即 50 美分、25 美分、10 美分、5 美分和 1 美分)?
1 | // problem: given 100 or n, count the ways of splitting it in |
1 | // recursive |
1 | // consider p[i][j] |
1 | // fast expt: given b and n, calculate b^n |
1 | // GCD(a, b) = GCD(a, r) r = a % b |
1 | function is_prime(n) { |
1 | // if n is prime, then a^n mod n = a mod n, if 0 < a < n. O(time*logn) |
1 | // for testing the time cost of the function, we make a high order function |
考虑以下三个函数:
1 | function sum_integers(a, b) { |
我们可以将大部分相同部分提取为一个新函数,将不同之处留作具体函数 term 和 next:
这是数学中的求和:
1 | function summate(a, b, f, next) { |
1 | function product(a, b, f, next) { |
1 | // use same thought to extract common partern of summate and product function |
x => x + 4 的无名称形式。便于作为参数传递给函数。当有一些小表达式不需要声明为函数时使用,特别是那些永远不会被复用的函数。给定 f(x),求 f(x) = 0 的根。同时给定边界 a、b,根在 [a, b] 之间。
解法:
1 | function find_root(f, r, a, b) { |
1 | // example 1: make a damp to a function for instance x -> x * x |
这是一种表达能力的增强:没有函数作为参数,我们无法表达对其他函数进行操作的抽象;没有函数作为返回值,我们无法表达对函数进行包装/升级的抽象。
1 | // deriv is a new function base on g(x), so we can use fn as return value to |
我们在 1.3 节开始时观察到,复合函数是一种关键的抽象机制,因为它们允许我们将通用计算方法表达为编程语言中的显式元素。现在我们已经看到了高阶函数如何让我们操纵这些通用方法,以创建更进一步的抽象。
我们应该始终保持警觉,寻找在程序中识别底层抽象的机会,并创建更强大的抽象——但这并不意味着我们应该总是以最抽象的方式编写程序。专家程序员知道如何选择适合其任务的抽象层次。但重要的是要能够用这些抽象进行思考,以便在新的情境中能够随时应用它们。
一等元素(first-class elements):编程语言对其操纵施加最少限制的元素。
JavaScript 中函数作为一等元素:表达能力更强,但高效实现是一个挑战。
练习 1.40
1 | // given param, generate a function |
1 | function double(f) { |
1 | // abstraction of compose |
1 | // given a function and n times, modify the function to repeat itself n times |
1 | // smoothing function, times |
迭代改进:为了计算结果,我们先从一个初始猜测值开始,然后判断是否足够好,若不是,则重复该过程。
1 | function iterative_improve(improve, is_good_enough) { |
这一切都是关于提升你的表达能力。
sicpjs — Apr 25, 2022
用 ❤ 和 制作于 Earth.