English
Programming connects an intended task to an executable description. Between the two are decisions about representation, permitted inputs, state changes, and failure. A program can compile and run while answering the wrong question: an average computed correctly is still misleading if missing observations were silently recorded as zeros. Machines follow the behavior of an implementation; they do not check whether that behavior matches our purpose.
A programming language provides both notation and rules for interpreting it. Those rules need not determine a single execution: concurrency, randomness, and interaction can admit multiple behaviors. Precision means knowing what the rules permit, not assuming that every program makes every assumption explicit. Languages make different relationships convenient to express—transformations, mutable objects, constraints, or concurrent processes—and this influences the solutions we notice.
The useful starting point is a small loop: describe a task, choose a representation, implement it, examine the result, and revise the description. Variables, branches, loops, functions, and basic data structures provide enough material to begin. Mathematical reasoning and programming can then develop together; mastery of all the mathematics in this chapter is not an admission requirement for writing useful programs.
From Ada Lovelace to the Software Crisis
Programming began before working electronic computers. Babbage’s Analytical Engine separated a store of values from a mechanism for operating on them and used cards to direct operations. Lovelace’s 1843 notes included a detailed procedure for Bernoulli numbers and considered how such a machine might work on things represented symbolically, beyond quantities alone. The complete engine was never built in their lifetimes. The useful lesson is the separation of a procedure, its representation, and the physical device that might execute it; a contested title such as “the first programmer” teaches less than that separation.
Electronic computing made the cost of preparing and debugging instructions an immediate practical problem. Subroutine libraries let programmers reuse work. Hopper’s A-0 system, operating in 1952, assembled programs from existing routines; its use of the word compiler predates today’s narrower association with translating a high-level language. Fortran’s team subsequently had to demonstrate that mathematical notation could be translated into machine code efficient enough for scientific users to adopt. Abstraction succeeded partly by reducing programming effort while respecting the cost of execution.
As software systems grew, coordination and comprehension became as troublesome as instruction coding. The 1968 NATO software engineering conference made these difficulties conspicuous, but neither large-project failures nor systematic programming began there. Structured programming, associated especially with Dijkstra, sought control structures whose behavior people could reason about locally. Floyd and Hoare developed ways to attach assertions to programs and justify how execution preserves them. A loop became something one could understand through an invariant, rather than by imagining every iteration.
These responses branched. Functional programming emphasizes composition and, in its pure forms, explicit treatment of effects; object-oriented programming groups behavior with state; modules and abstract data types protect representation choices. They overlap and coexist. Their historical value is a recurring design question: what must a programmer understand at once, and what can a dependable boundary allow them to set aside?
Meaning, Abstraction, and Correctness
Three questions organize the subject: what behavior does this program specify, what can a component’s users rely on, and what evidence supports that reliance? Semantics, abstraction, and correctness address these questions from different directions. They become easier to learn when applied to the same small program.
What a Program Means
Consider a function that sums a list of integers. An operational account describes states and transitions: initialize an accumulator, read an element, add it, advance the index. A denotational account relates a list to its mathematical sum. An axiomatic account connects assertions: after processing the first i elements, the accumulator equals their sum. The last assertion is a loop invariant; when i reaches the list’s length, it yields the desired result.
These accounts agree only after the relevant assumptions are stated. Mathematical integers have no fixed bound; machine integers may wrap, trap, or have language-dependent behavior on overflow. Floating-point addition introduces rounding and depends on grouping. An input-output function also leaves out observable effects unless the model includes them. Asking “what does this program mean?” therefore includes asking which arithmetic, effects, and execution rules are being modeled.
Abstraction as the Central Design Activity
An abstraction makes selected properties available without requiring its users to track the entire implementation. A stack promises last-in, first-out behavior. It may use an array or linked nodes, provided its operations preserve that promise. The underlying representation needs its own invariant—for example, an array stack’s size stays between zero and its capacity—and each operation must maintain it.
An interface is useful when its promises match the decisions its clients must make. If bounded latency matters, documenting only the returned value is insufficient. If iteration order is unspecified, a client must not silently depend on it. This is how abstraction supports change: it identifies what implementations may vary and what clients may assume. Adding a class or a wrapper without clarifying either side does not necessarily improve the design.
Correctness: Specifications, Evidence, and Limits
Correctness is a relation between a program and a specification under stated assumptions. Partial correctness says that if execution terminates, its result meets the specification. Total correctness adds termination. There is no algorithm that decides termination for every program in a sufficiently expressive general-purpose language; that does not prevent proving termination and correctness for particular programs.
Tests examine selected executions; property-based testing generates cases from stated properties; types rule out classes of errors according to the language’s guarantees; proofs establish claims within a model. These forms of evidence complement one another. A sorting test should check both order and preservation of the input elements: a function that returns an empty list always produces a sorted result, but fails the task.
Large proofs are practical in some settings. The seL4 microkernel’s 2009 functional-correctness verification used Isabelle/HOL, establishing a relationship between its C implementation and an abstract specification under explicit assumptions. It did not establish that every system containing the kernel meets every security requirement. The enduring lesson is to state the property and the boundary of the claim. See the project’s explanation of its proofs and assumptions.
Even a fully proved implementation can satisfy an inadequate specification. Requirements review asks whether we chose the right behavior; verification asks whether the implementation has the specified behavior. Learning to move between these questions is part of programming competence.
What Studying This Changes
The practical change is learning to locate uncertainty. Before coding a parser, identify accepted inputs and error behavior. Before refactoring a module, identify which observations must remain unchanged. During debugging, separate a mistaken requirement, a faulty algorithm, a representation error, and an implementation defect; they need different repairs.
Reading, tracing, executing, testing, and proving each reveal something different. A productive exercise is to implement a small component, write its contract, explain its invariant, and deliberately search for a counterexample. Reimplementing it with a different representation then tests whether the abstraction was understood. The aim is not to avoid experimentation, but to make experiments answer explicit questions.
Resources
Books and Texts
The canonical entry to programming as conceptual practice has been, for forty years, Abelson and Sussman’s Structure and Interpretation of Computer Programs (SICP, 2nd ed., 1996, free online). Its distinctive feature is that it frontloads conceptual substance — recursion, higher-order functions, data abstraction, interpreters — before any conventional language features appear. Scheme is used not because Scheme matters but because its syntax is minimal enough to vanish, leaving the concepts unobscured. SICP’s reputation for producing programmers who understand what they are doing has held for four decades. The cost is real: it expects mathematical maturity and sustained engagement with abstract material. Learners who are not prepared find it discouraging.
Felleisen, Findler, Flatt, and Krishnamurthi’s How to Design Programs (HtDP, 2nd ed., free online) covers much of the same conceptual ground with a different emphasis: a systematic design methodology — from problem statement to data definition to examples to function template to implementation to testing — that makes the structure of good programs explicit. Where SICP is organized around concepts, HtDP is organized around a design process. The two complement each other; reading both is not redundant. Many learners who struggled with SICP found HtDP the right starting point; many who mastered HtDP found SICP the natural next step.
Kernighan and Ritchie’s The C Programming Language (K&R, 2nd ed., 1988) introduces C’s imperative style, pointers, and explicit memory management. It is useful for reading C systems code, but C is an abstract language model rather than a literal description of every machine, and the book predates later language standards. The Rust Programming Language (free online) offers a complementary route to systems programming through ownership and borrowing. Its discussion of unsafe code explains where responsibility for maintaining safety shifts to the programmer; a language guarantee still depends on respecting its boundary.
Van Roy and Haridi’s Concepts, Techniques, and Models of Computer Programming (CTM, 2004) is the most ambitious attempt to unify the paradigms under a single framework, presenting functional, imperative, object-oriented, logic, concurrent, and dataflow programming as configurations of a small set of underlying concepts. The book’s reception has been uneven — the unconventional language choice limited adoption — but its central thesis is correct and stated more clearly here than anywhere else: paradigms are not competing theories but different choices about which concepts to make primitive.
The three books on programming practice with the most durable insight are The Pragmatic Programmer (Hunt and Thomas, 20th anniversary ed., 2019), Refactoring (Fowler, 2nd ed., 2018), and The Mythical Man-Month (Brooks, anniversary ed., 1995): the first on the habits of individual practitioners, the second on systematic code improvement, the third on the organizational and human dynamics of software projects at scale. All three are discussed at more length in Chapter 7, where their concerns fit more naturally.
| Book | Role | Type |
|---|---|---|
| Abelson & Sussman, SICP (free) | Canonical conceptual entry | Entry |
| Felleisen et al., How to Design Programs (free) | Systematic design methodology | Entry |
| Kernighan & Ritchie, The C Programming Language | Imperative programming at machine level | Depth |
| The Rust Programming Language (free) | Modern systems programming | Depth |
| Van Roy & Haridi, Concepts, Techniques, and Models | Unified paradigm framework | Depth |
| Hunt & Thomas, The Pragmatic Programmer | Practitioner habits and craft | Auxiliary |
| Fowler, Refactoring (2nd ed.) | Systematic code improvement | Auxiliary |
| Brooks, The Mythical Man-Month | Software project dynamics | Auxiliary |
| Seibel, Coders at Work | How mature programmers actually think | Auxiliary |
Courses and Lectures
Berkeley CS 61A (current version) is the most refined free course on programming as conceptual practice. Built on SICP’s principles but using Python and providing substantial pedagogical scaffolding, it is paired with the free textbook Composing Programs (John DeNero). For learners who find SICP too abrupt as a first encounter, CS 61A covers the same destination more gradually. Full lecture recordings, assignments, and exams are freely available.
MIT 6.009 (Fundamentals of Programming) covers programming concepts — recursion, data structures, functional programming, interpreters — in Python with emphasis on implementation. Less conceptually ambitious than CS 61A but more practice-oriented; useful as a complement or alternative depending on how a learner engages with abstract material.
Nand to Tetris (Shimon Schocken and Noam Nisan) connects programming to the layers that execute it. Its project sequence builds an educational computer from a supplied NAND-gate abstraction, then develops an assembler, virtual machine, compiler, and basic operating-system services. It explains the path from logic gates to high-level programs; transistor physics and production-system complexity are outside that model.
Harvard CS50x (free on the CS50 site and edX, with an optional paid certificate) is the most polished broad introduction to computer science for beginners. It is less conceptually focused than CS 61A or SICP, but its lectures, projects, and production quality are excellent for learners who need motivation, breadth, and a first pass through C, Python, SQL, and web programming before returning to deeper conceptual work.
| Course | Platform | Type |
|---|---|---|
| Berkeley CS 61A + Composing Programs (free) | Berkeley / course site | Entry |
| MIT 6.009 Fundamentals of Programming (free) | MIT OCW | Entry |
| Nand to Tetris Parts I & II (free) | nand2tetris.org / Coursera | Depth |
| Harvard CS50x (free; optional paid certificate) | Harvard / edX / CS50 site | Auxiliary |
Practice, Tools, and Code
Python Tutor (pythontutor.com) visualizes program execution step by step, showing the call stack, heap, and variable bindings at each moment. Especially useful for understanding recursion and for debugging the gap between what a programmer thinks a program does and what it actually does. Free for Python, JavaScript, C, C++, and Java.
Bret Victor’s “Inventing on Principle” (YouTube, 2012) is a 54-minute talk arguing that the gap between writing code and seeing its effect impedes thinking. It demonstrates live-coding environments where changes are immediately reflected in the running program — tools that were prototypes, but that change how programmers think about the relationship between notation and execution.
Reading an existing codebase provides practice in recovering contracts and design decisions from an implementation. Start with a bounded path through a system: how a small utility processes input, how Redis handles a command, how SQLite executes a query, or how CPython evaluates an expression. Repository size alone is a poor guide to difficulty. For SQLite, distinguish its modular development sources from the generated single-file amalgamation; the latter is convenient for building and distribution, not evidence that the system was designed as one undivided source file. Follow tests and documentation alongside the code.
| Resource | Platform | Type |
|---|---|---|
| Python Tutor (free) | pythontutor.com | Practice |
| Bret Victor, “Inventing on Principle” (free) | YouTube | Depth |
| Open-source codebases: Unix utils, Redis, SQLite, CPython | GitHub / distributed | Practice |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Equating compilation with correctness | Syntax and type checks cover only particular properties. | State the intended result and test cases that distinguish it from plausible wrong results. |
| Learning only syntax or frameworks | Familiar commands do not explain representation, state, or contracts. | Build small components and explain what remains true across their operations. |
| Treating one paradigm as universally superior | Different organizations of code expose different costs and dependencies. | Solve a small problem in two styles and compare what becomes easier to change or verify. |
| Calling every wrapper an abstraction | A new layer may add indirection without a useful promise. | Identify the property clients rely on and the detail the boundary allows them to ignore. |
| Demanding proof before any experimentation | Exploring examples can reveal the specification and the invariant. | Alternate experiments and arguments; distinguish a conjecture from an established guarantee. |
| Delegating the whole learning task to an assistant | Accepting a solution does not exercise design or error detection. | Use help for hints or review, then explain, modify, and test a nearby problem independently. |
中文
编程把我们想完成的任务连接到可执行的描述。两者之间有一系列选择:怎样表示数据,允许什么输入,状态如何变化,失败时怎么办。程序即使能编译、能运行,也可能回答了错误的问题:如果把缺失的观测值悄悄记成零,平均数算得再准确,也可能产生误导。机器遵循实现所规定的行为,并不会检查这些行为是否符合我们的目的。
编程语言既提供记号,也规定如何解释这些记号。这些规则不一定只允许一种执行过程:并发、随机性和交互都可能使程序具有多种行为。精确意味着知道规则允许什么,而不是假定每个程序都已明确写出全部前提。不同语言使不同关系更容易表达,例如数据变换、可变对象、约束或并发进程;这会影响我们容易想到哪些解法。
合适的起点是一个小循环:描述任务,选择表示,实现它,检查结果,再修正描述。变量、分支、循环、函数和基本数据结构已经足以开始。数学推理与编程可以相互促进;不必先学完本章所有数学,才有资格编写有用的程序。
从 Ada Lovelace 到软件危机
编程早于能够工作的电子计算机。Babbage 的分析机把存放数值的机构与执行运算的机构分开,并用卡片控制操作。Lovelace 在 1843 年的笔记中给出了计算伯努利数的详细过程,也思考过:只要事物能用符号表示,机器就可能处理数量以外的对象。他们在世时,完整的分析机始终没有建成。这段历史最有用的启发,是区分过程、过程的表示和可能执行它的物理装置;相比之下,“第一位程序员”这种有争议的称号提供的理解要少得多。
电子计算使编写指令和排查错误的成本成为紧迫的实际问题。子程序库让人们能够复用已有工作。Hopper 的 A-0 系统在 1952 年投入运行,用已有例程组合程序;它所使用的 compiler 一词,早于今天主要指高级语言翻译器的含义。此后,Fortran 团队必须证明:数学记号可以被翻译成足够高效的机器代码,值得科学计算用户采用。抽象之所以被接受,一部分原因是它在照顾执行成本的同时,降低了编程成本。
软件系统扩大后,协作与理解逐渐成为和编写指令同样棘手的问题。1968 年的 NATO 软件工程会议使这些困难受到集中关注,但大型项目的失败与系统化的编程方法都不是从那时才开始。以 Dijkstra 为重要代表的结构化编程,追求能让人局部推理的控制结构。Floyd 和 Hoare 则发展了给程序附加断言、论证执行如何保持这些断言的方法。于是,理解循环可以依靠不变式,而不必在脑中逐次模拟所有迭代。
这些回应发展成了不同方向。函数式编程强调组合,并在纯函数式形式中明确处理副作用;面向对象编程把行为与状态组织在一起;模块和抽象数据类型保护内部表示的选择。它们相互交叉,也长期共存。理解其历史价值,可以抓住一个反复出现的设计问题:程序员必须同时理解多少东西,又能凭借可靠的边界暂时忽略哪些细节?
意义、抽象与正确性
三个问题可以组织这一领域:程序规定了什么行为,组件的使用者可以依赖什么,我们有什么证据支持这种依赖?语义、抽象与正确性分别从不同方向回答这些问题。把它们用于同一个小程序,比孤立地记忆术语更容易理解。
程序意味着什么
考虑一个求整数列表之和的函数。操作语义通过状态与转移描述它:初始化累加器,读取一个元素,相加,移动索引。指称语义把列表对应到它的数学求和值。公理语义则连接断言:处理完前 i 个元素后,累加器等于这些元素的和。最后这个断言就是循环不变式;当 i 到达列表长度时,它便导出所需结果。
只有说明相关前提,这几种描述才会一致。数学整数没有固定的大小上限;机器整数溢出时可能回绕、触发异常,或具有依语言而异的行为。浮点加法会舍入,分组方式也会影响结果。单纯的输入输出函数还会遗漏可观察的副作用,除非模型明确包含它们。因此,问“程序意味着什么”,也包括问:这里建模的是哪一种算术、副作用和执行规则?
抽象是核心设计活动
抽象让使用者能够依赖选定的性质,而不必跟踪实现的全部细节。栈承诺后进先出的行为;它可以用数组,也可以用链式节点实现,只要操作保持这一承诺。底层表示也需要自身的不变式,例如数组栈的元素个数始终介于零和容量之间,每个操作都必须维护它。
接口是否有用,取决于其承诺是否覆盖调用方必须作出的决策。如果延迟上界很重要,只说明返回什么值就不够。如果接口没有规定遍历顺序,调用方就不应暗中依赖某种顺序。抽象由此支持变化:它划清实现可以改变什么、使用者可以假定什么。只增加一个类或一层包装,却没有澄清这两边,并不必然改善设计。
正确性:规格、证据与边界
正确性是程序与规格说明在给定前提下的一种关系。部分正确性表示:如果执行终止,结果就符合规格。完全正确性还要求执行终止。对于表达能力足够强的通用语言,不存在能判定所有程序是否终止的算法;这并不妨碍我们证明某个具体程序会终止并且正确。
测试检查选定的执行;基于性质的测试根据所声明的性质生成用例;类型系统依其语言保证排除某些错误;证明则在模型内建立结论。这些证据可以相互补充。测试排序函数时,应同时检查顺序和输入元素是否完整保留:一个总返回空列表的函数,其结果总是有序,却没有完成排序任务。
在一些场景中,大规模证明已经可行。seL4 微内核在 2009 年完成的功能正确性验证使用的是 Isabelle/HOL,它在明确前提下建立了 C 实现与抽象规格之间的对应关系。这不等于证明所有包含该内核的系统都满足全部安全要求。值得学习的是如何说清所证明的性质及其边界。项目对证明及其前提有进一步说明。
即使实现已经得到完整证明,也可能忠实地满足了一份不合适的规格。需求审查关心我们是否选择了恰当的行为;验证关心实现是否具有规定的行为。能够在这两个问题之间切换,也是编程能力的一部分。
学习这一部分会改变什么
实际的变化,是学会定位不确定性。写解析器前,先确定接受哪些输入、错误如何处理。重构模块前,先确定哪些可观察行为必须保持。调试时,要区分需求判断错误、算法错误、表示错误与实现缺陷,因为它们需要不同的修复方式。
阅读、跟踪、运行、测试和证明各自能揭示不同问题。一个有用的练习是:实现一个小组件,写出契约,解释不变式,再主动寻找反例。随后换一种表示重新实现,检验自己是否理解了抽象。目标不是避免试验,而是让试验回答明确的问题。
资源
书籍与文本
四十年来,把编程作为概念实践来学习的经典入口,一直是 Abelson 和 Sussman 的 Structure and Interpretation of Computer Programs(SICP,第 2 版,1996,可免费在线获取)。它的独特之处在于,它把概念实质放在最前面——递归、高阶函数、数据抽象、解释器——而不是先讲传统语言特性。书中使用 Scheme,并不是因为 Scheme 本身重要,而是因为它的语法极简,几乎可以隐退到背景中,让概念不被遮蔽。SICP 培养“知道自己在做什么的程序员”的名声已经维持了四十年。代价也是真实的:它要求数学成熟度和对抽象材料的持续投入。没有准备好的学习者会觉得它令人沮丧。
Felleisen、Findler、Flatt 和 Krishnamurthi 的 How to Design Programs(HtDP,第 2 版,可免费在线获取)覆盖了许多相同的概念地带,但强调方式不同:它提供一套系统性的设计方法论——从问题陈述,到数据定义,到例子,到函数模板,到实现,再到测试——把好程序的结构显式化。SICP 围绕概念组织,HtDP 围绕设计过程组织。二者相互补充,并不重复。许多在 SICP 中受挫的学习者,会发现 HtDP 才是正确起点;许多掌握 HtDP 的学习者,也会发现 SICP 是自然的下一步。
Kernighan 和 Ritchie 的 The C Programming Language(K&R,第 2 版,1988)介绍 C 的命令式风格、指针和显式内存管理,适合帮助阅读 C 系统代码。但 C 规定的是抽象语言模型,并不直接描述每一种机器;这本书也早于后来的语言标准。The Rust Programming Language(可免费在线获取)通过所有权与借用,提供一条互补的系统编程路线。其关于 unsafe 代码的讨论说明,哪些地方维护安全的责任转移给了程序员;语言保证仍以遵守其边界为前提。
Van Roy 和 Haridi 的 Concepts, Techniques, and Models of Computer Programming(CTM,2004)是最雄心勃勃的范式统一尝试。它在一个框架中呈现函数式、命令式、面向对象、逻辑、并发和数据流编程,把它们视为少数底层概念的不同配置。这本书的接受度并不均衡——不常规的语言选择限制了它的采用——但它的核心论点是正确的,而且没有其他地方比这里说得更清楚:范式不是彼此竞争的理论,而是关于哪些概念应被设为原语的不同选择。
关于编程实践,最具有持久洞见的三本书是 The Pragmatic Programmer(Hunt 和 Thomas,20 周年版,2019)、Refactoring(Fowler,第 2 版,2018)和 The Mythical Man-Month(Brooks,周年版,1995):第一本讨论个体实践者的习惯,第二本讨论系统性的代码改进,第三本讨论大规模软件项目中的组织与人类动态。这三本书会在第七章中更详细讨论,因为它们的关注点在那里更自然。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Abelson & Sussman, SICP(免费) | 经典概念入口 | 入门 |
| Felleisen et al., How to Design Programs(免费) | 系统性设计方法论 | 入门 |
| Kernighan & Ritchie, The C Programming Language | 机器层面的命令式编程 | 深入 |
| The Rust Programming Language(免费) | 现代系统编程 | 深入 |
| Van Roy & Haridi, Concepts, Techniques, and Models | 统一范式框架 | 深入 |
| Hunt & Thomas, The Pragmatic Programmer | 实践者习惯与技艺 | 辅助 |
| Fowler, Refactoring(第 2 版) | 系统性代码改进 | 辅助 |
| Brooks, The Mythical Man-Month | 软件项目动态 | 辅助 |
| Seibel, Coders at Work | 成熟程序员实际如何思考 | 辅助 |
课程与讲座
Berkeley CS 61A(当前版本)是关于编程作为概念实践的最精炼免费课程。它建立在 SICP 原则之上,但使用 Python,并提供大量教学脚手架;它与免费教材 Composing Programs(John DeNero)配套。对于觉得 SICP 作为第一接触过于陡峭的学习者,CS 61A 会以更渐进的方式抵达同一目的地。完整讲课录像、作业和考试都可免费获取。
MIT 6.009(Fundamentals of Programming)用 Python 覆盖编程概念——递归、数据结构、函数式编程、解释器——并强调实现。它不如 CS 61A 在概念上雄心勃勃,但更偏实践;根据学习者如何处理抽象材料,它可以作为补充,也可以作为替代。
Nand to Tetris(Shimon Schocken 和 Noam Nisan)把编程与执行程序的各个层次连接起来。其项目序列从给定的 NAND 门抽象构建教学计算机,再开发汇编器、虚拟机、编译器和基本操作系统服务。它解释从逻辑门到高级程序的路径;晶体管物理与生产系统的复杂性不在这个模型之内。
Harvard CS50x(可在 CS50 官网和 edX 免费学习,付费证书可选)是面向初学者最精致的广义计算机科学入门课程。它不如 CS 61A 或 SICP 那样集中于概念,但它的讲座、项目和制作质量非常适合需要动机、广度,以及先接触 C、Python、SQL 和 Web 编程,再回到更深概念工作的学习者。
| 课程 | 平台 | 类型 |
|---|---|---|
| Berkeley CS 61A + Composing Programs(免费) | Berkeley / 课程网站 | 入门 |
| MIT 6.009 Fundamentals of Programming(免费) | MIT OCW | 入门 |
| Nand to Tetris Parts I & II(免费) | nand2tetris.org / Coursera | 深入 |
| Harvard CS50x(免费;付费证书可选) | Harvard / edX / CS50 官网 | 辅助 |
实践、工具与代码
Python Tutor(pythontutor.com)会逐步可视化程序执行,展示每一刻的调用栈、堆和变量绑定。它尤其适合理解递归,也适合调试程序员以为程序会做什么与程序实际做什么之间的差距。它免费支持 Python、JavaScript、C、C++ 和 Java。
Bret Victor 的 “Inventing on Principle”(YouTube,2012)是一场 54 分钟的演讲,主张写代码与看见代码效果之间的间隔会阻碍思考。它展示了一些实时编码环境:修改会立刻反映到正在运行的程序中。这些工具当时只是原型,但它们会改变程序员理解记号与执行之间关系的方式。
阅读已有代码库,可以练习从实现中恢复契约与设计决策。先限定一条执行路径:小工具怎样处理输入,Redis 怎样处理一条命令,SQLite 怎样执行查询,或 CPython 怎样求值表达式。仅凭仓库大小很难判断阅读难度。对 SQLite,应区分模块化的开发源码与生成的单文件合并版本;后者方便构建和分发,并不表示系统原本就按一个不可分割的源文件设计。阅读时应同时追踪测试与文档。
| 资源 | 平台 | 类型 |
|---|---|---|
| Python Tutor(免费) | pythontutor.com | 实践 |
| Bret Victor, “Inventing on Principle”(免费) | YouTube | 深入 |
| 开源代码库:Unix utils、Redis、SQLite、CPython | GitHub / 分发源码 | 实践 |
陷阱
| 陷阱 | 为什么会误导 | 更好的做法 |
|---|---|---|
| 把编译通过等同于正确 | 语法和类型检查只覆盖特定性质。 | 说明预期结果,并设计能把它与貌似合理的错误结果区分开的用例。 |
| 只学语法或框架 | 熟悉命令并不等于理解表示、状态与契约。 | 构建小组件,解释其操作前后有哪些性质保持成立。 |
| 认为某种范式普遍优越 | 不同代码组织方式会显露不同的成本与依赖。 | 用两种风格解决小问题,比较哪些修改或验证变得容易。 |
| 把所有包装都叫作抽象 | 新的一层可能只增加间接性,没有提供有用承诺。 | 指出调用方依赖什么性质,以及这个边界允许它忽略什么细节。 |
| 要求先有证明才允许试验 | 研究实例本身有助于发现规格与不变式。 | 交替使用试验和论证,区分猜想与已建立的保证。 |
| 把整个学习任务交给助手 | 接受解答并没有练习设计与识别错误。 | 用辅助工具获取提示或审阅,然后独立解释、修改并测试一个相近问题。 |