English
A programming language is a tool, and like any tool it shapes what its user can build and how they think about building it. This section is not about any specific language, nor about the theory of programming languages (that is §3.4). It is about the practitioner’s relationship to languages: how to choose one for a task, how to learn a new one efficiently, how the paradigm a language embodies shapes the solutions its users reach for, and what it means to be fluent in several. These are practical questions that every working programmer answers, well or badly, repeatedly throughout a career.
The distinction between knowing a language and being able to program is the starting point. A person who has memorized the syntax of Python can write Python that runs; this is not the same as being able to express a clear solution to a problem in Python, to recognize when Python is the wrong tool, or to read idiomatic Python written by an expert and understand why it is structured as it is. Language knowledge is the entry fee; what matters is the judgment about how to use the language well, which idioms to reach for, and how the language’s design influences the shape of good solutions in it. A practitioner who has internalized this distinction approaches a new language by asking not “what is the syntax?” but “what does this language make easy, what does it make hard, and what does idiomatic code in it look like?”
The deeper claim, which most experienced programmers come to believe, is that the languages you know shape the solutions you can imagine. A programmer who knows only imperative languages reaches for loops and mutable state; a programmer who also knows functional languages sees the same problem as a transformation of data through a pipeline of operations. Neither view is universally correct, but the programmer who has both has more options. This is the practical argument for learning languages across paradigms: not because you will use all of them, but because each one teaches a way of decomposing problems that transfers back to the languages you use daily.
Prerequisites: Programming (§2.1) — fluency in at least one language is assumed. Programming language theory (§3.4) provides the conceptual foundations — type systems, semantics, paradigms — that this section applies practically.
The Shape of the Skill: From One Language to Many
The practitioner’s relationship to programming languages develops through recognizable stages, and understanding the stages clarifies what fluency actually consists of.
The first stage is fluency in a single language: the ability to express solutions in it without constantly consulting documentation, to read idiomatic code in it, and to know its standard library well enough to avoid reinventing what it provides. Reaching this stage in one language takes most people a year or two of regular use. It is the necessary foundation; everything else builds on having at least one language in which you think fluently rather than translating from pseudocode in your head.
The second stage is the recognition that languages embody paradigms, and that paradigms are deeper than syntax. The imperative paradigm (do this, then this, modifying state along the way) descends from the von Neumann architecture and is embodied in C, and in the imperative core of most languages. The object-oriented paradigm (organize state and the operations on it into objects that communicate) is embodied in Smalltalk, Java, and the OO features of multi-paradigm languages. The functional paradigm (compute by evaluating functions, avoid mutable state) is embodied in Haskell, in the ML family, and increasingly in the functional features added to mainstream languages. The logic paradigm (declare facts and rules, let the system derive answers) is embodied in Prolog. Each paradigm is a way of thinking about what a program is and how it should be decomposed; learning a language that embodies a paradigm purely is the way to internalize that paradigm.
The third stage is multilingual fluency: comfort across several languages and paradigms, with the judgment to choose appropriately and the ability to learn a new language quickly because most of what a new language offers is a recombination of concepts already understood. A programmer at this stage learns their fifth language in days, not months, because they are not learning to program — they already know how to program — they are learning how this particular language expresses concepts they already understand. The new syntax, the specific standard library, the idioms and conventions are the only genuinely new material; the underlying concepts (variables, functions, types, control flow, abstraction mechanisms) transfer.
The history of programming languages is, from the practitioner’s vantage point, a history of accumulating ways to manage complexity, and a few episodes of that history matter for understanding the present landscape. Assembly gave way to FORTRAN and the first high-level languages because expressing computation in terms closer to the problem rather than the machine reduced error and increased productivity. The structured programming movement of the late 1960s and 1970s (Dijkstra’s argument against goto) established that disciplined control flow made programs more reliable, and the languages that followed built in the structured constructs (loops, conditionals, functions) that made goto unnecessary. The object-oriented wave of the 1980s and 1990s offered encapsulation and polymorphism as complexity-management tools, oversold their benefits, and settled into being one useful approach among several. The static-versus-dynamic typing debate ran for decades and has substantially resolved into a synthesis: gradual typing (TypeScript over JavaScript, type hints in Python, Sorbet for Ruby) added optional static typing to dynamic languages, and the contemporary consensus is that static types pay for themselves on codebases above a certain size and team scale. Memory management evolved from manual (C) through garbage collection (Java, the majority of modern languages) to Rust’s ownership model, which provides memory safety without garbage collection through static analysis — the most significant language design development of the past fifteen years, because it demonstrated that a tradeoff long believed fundamental (safety versus control) could be partly dissolved by a better type system.
The contemporary language landscape reflects this accumulated history. The languages a working programmer is most likely to encounter — Python, JavaScript/TypeScript, Java, C#, Go, Rust, C++, Swift, Kotlin — each occupy a niche defined by tradeoffs among performance, safety, expressiveness, ecosystem, and the domains where they dominate. Python dominates data science, scripting, and increasingly AI/ML tooling because of its readability and ecosystem despite its performance limitations. JavaScript/TypeScript dominate web frontend (by necessity — it is the browser’s language) and have spread to backend through Node. Go occupies the niche of simple, fast, concurrent network services. Rust occupies the niche where memory safety and performance both matter — systems programming, embedded, performance-critical infrastructure — and is expanding into domains C and C++ previously owned. Understanding why each language occupies its niche, rather than treating language choice as a matter of preference or fashion, is part of the practitioner’s judgment.
Choosing, Learning, and Thinking in Languages
Choosing a Language for a Task
Language choice for a project is an engineering decision with consequences that play out over the project’s lifetime, and it should be made deliberately rather than by default or fashion.
The dominant factors are usually not the language’s intrinsic properties but its ecosystem and context. The single most important question is often: what libraries and frameworks does this task need, and which language has the best ones? A machine learning project goes to Python not because Python is the best-designed language for the task but because PyTorch, TensorFlow, scikit-learn, and the entire ML ecosystem are in Python. A web frontend goes to JavaScript/TypeScript because that is what runs in browsers. A project that must integrate with an existing codebase goes to whatever language that codebase is in. These contextual factors usually dominate the intrinsic properties of the language.
When the ecosystem does not force the choice, the intrinsic tradeoffs become relevant. Performance requirements: does the task need the speed of a compiled, low-level language (C, C++, Rust, Go) or is the productivity of a higher-level language (Python, Ruby) more valuable than raw speed? Safety requirements: does the task’s failure cost justify the stronger guarantees of a statically typed, memory-safe language, or is the flexibility of a dynamic language more valuable? Concurrency requirements: does the task involve heavy concurrency, where Go’s goroutines or Rust’s async or Erlang’s actor model provide real advantages? Team familiarity: a language the team knows well is usually a better choice than a theoretically superior language the team would have to learn, because the productivity cost of learning is real and immediate while the theoretical superiority is speculative.
The cost of a wrong language choice is asymmetric and grows over time. Early in a project, switching languages is cheap; once a codebase has grown, accumulated libraries, and trained a team, switching is enormously expensive. This asymmetry argues for taking the initial choice seriously and for preferring established languages with strong ecosystems and hiring pools over novel languages whose advantages are real but whose risks (immature ecosystem, small hiring pool, uncertain longevity) compound over the project’s life.
Learning a New Language Efficiently
Learning the nth language (for n greater than two or three) is a different activity from learning the first. The concepts transfer; only the specifics are new. An efficient approach exploits this.
The most efficient path starts by mapping the new language onto concepts already known: how does this language do variables, functions, types, control flow, error handling, modules, the abstraction mechanisms? For most of these, the new language is a variation on patterns already understood, and the learning is recognizing the variation. The genuinely new material — the concepts the language introduces that the learner does not already have — deserves the most attention. Learning Rust, the new material is the ownership and borrowing system; everything else is familiar. Learning Haskell, the new material is purity, laziness, and the type-class-based abstraction; the rest maps onto known concepts. Learning Go, the new material is the goroutine and channel concurrency model; the language is otherwise deliberately minimal. Identifying what is genuinely new and concentrating on it, rather than treating the whole language as new, is what makes experienced programmers learn languages fast.
Idiom is the hardest and most neglected part of learning a language. Every language has idiomatic ways of expressing common patterns, and code that is syntactically correct but non-idiomatic marks its author as a novice in the language and is often harder to read for experts in it. Writing Java in Python (using getters and setters, deep class hierarchies, verbose patterns where Python idioms are concise) produces code that runs but fights the language. Learning idiom requires reading idiomatic code — the standard library, well-regarded open-source projects, the language’s official style guide — and writing enough code to internalize the patterns. The tooling helps: linters and formatters (gofmt, Black, rustfmt, Prettier, clippy) encode idiomatic conventions and enforce them automatically, and using them from the start of learning a language teaches its conventions.
The AI-assisted era has changed language learning in a specific way: a language model can translate code between languages, explain idioms, and answer “how do I do X in language Y” questions instantly. This accelerates the early stages of learning a new language dramatically — the friction of looking up syntax and standard-library functions is largely gone. But it introduces a risk: a programmer who relies on AI translation without internalizing the new language’s idioms produces code that works but is not idiomatic, and never develops the fluency that comes from thinking in the language. The productive use of AI for language learning is to accelerate the lookup of specifics while still doing the work of reading idiomatic code and internalizing the patterns, not to bypass that work.
How Paradigms Shape Thought
The strongest reason to learn languages across paradigms is that each paradigm provides a way of decomposing problems that transfers back to whatever language you use daily.
The functional paradigm teaches decomposition by data transformation: a problem is solved by a pipeline of pure functions, each transforming its input to its output without side effects. Once internalized, this way of thinking improves code in imperative languages too — the programmer who has learned functional thinking writes clearer code even in Python or JavaScript, reaching for map/filter/reduce and immutable data where it clarifies, avoiding the tangled mutable state that imperative programmers default to. The functional concepts that have migrated into mainstream languages — first-class functions, closures, immutability, higher-order functions, algebraic data types and pattern matching — are functional thinking made available everywhere.
The object-oriented paradigm teaches decomposition by responsibility: a problem is solved by a set of objects, each responsible for some data and behavior, communicating through well-defined interfaces. Its insights about encapsulation (hiding internal state behind an interface) and polymorphism (treating different types uniformly through a shared interface) are valuable even in non-OO contexts. Its overreach — deep inheritance hierarchies, the attempt to model everything as objects, the ceremony that OO languages sometimes impose — is the part that the field has learned to use more sparingly.
The concurrent paradigms teach decomposition by independent activity: the actor model (Erlang, Akka), communicating sequential processes (Go’s goroutines and channels), and async/await (Rust, JavaScript, Python) each provide a different mental model for structuring programs that do many things at once. These models matter increasingly as concurrency becomes unavoidable, and understanding more than one provides options for structuring concurrent systems that a programmer who knows only threads-and-locks does not have.
The practical payoff of paradigm diversity is options. A programmer who thinks only imperatively sees one kind of solution; a programmer fluent across paradigms sees several and can choose. This is not an argument for using every paradigm in every program — that produces incoherent code — but for having the paradigms available as ways of seeing, so that the solution you reach for is chosen from a range rather than being the only one you can imagine.
What Changes With Mastery
Fluency across languages and paradigms changes how a practitioner approaches problems and tools.
The first change is that language stops being a constraint on thinking. The programmer who knows one language thinks in that language’s terms and is limited by them; the programmer who knows several thinks about the problem and then expresses the solution in whichever language fits, choosing the language to suit the problem rather than forcing the problem into the only language they know. This is liberating: the problem is approached on its own terms, and the language is selected as the appropriate tool.
The second change is the ability to learn new languages and tools quickly, which compounds over a career. Technology changes; the dominant languages of one decade are displaced or supplemented in the next. A practitioner whose skill is tied to a specific language is vulnerable to that language’s decline; a practitioner who has internalized the concepts that transfer across languages adapts, learning the new language as a variation on what they already understand. In a field where the specific tools change continuously, the transferable conceptual skill is what has durable value.
The third change is better code even in a single language. The programmer who has learned functional thinking writes clearer imperative code; the programmer who understands type systems writes better-structured dynamically typed code; the programmer who has used several concurrency models structures concurrent code more clearly. The cross-pollination from multiple paradigms improves code in every language, because the insights from each paradigm are not confined to the languages that embody it purely.
The fourth change is calibration about language debates. Programming language discussions — static versus dynamic typing, this language versus that one, paradigm wars — generate strong opinions and substantial heat. The practitioner who has worked seriously across languages and paradigms holds these debates more lightly: they understand that each approach has genuine strengths and genuine costs, that the right choice depends on context, and that the strongly held position that one language or paradigm is universally superior is usually a sign of limited experience rather than deep insight.
Resources
Books and Texts
Bruce Tate’s Seven Languages in Seven Weeks (Pragmatic Bookshelf, 2010) and its successor Seven More Languages in Seven Weeks are the most direct route to paradigm breadth. Each chapter introduces a language chosen to illustrate a different way of thinking — Ruby, Io, Prolog, Scala, Erlang, Clojure, Haskell — with enough depth to convey the paradigm without the years of use required for fluency. The point is not to become proficient in seven languages but to experience seven ways of thinking about programming. For a practitioner who knows one or two mainstream languages, this is the fastest way to discover that there are other ways to decompose problems.
Abelson and Sussman’s Structure and Interpretation of Computer Programs (2nd ed., MIT Press, 1996, free at mitpress.mit.edu/sicp) — referenced in §2.1 — remains the deepest treatment of how programming languages and the act of programming relate. Its progression through functional programming, state and objects, and the implementation of interpreters and compilers reveals that the boundaries between “using a language” and “implementing a language” are more porous than they appear. The book uses Scheme, but its subject is the structure of programs in any language.
Michael Scott’s Programming Language Pragmatics (4th ed., Morgan Kaufmann, 2015) is the comprehensive reference connecting language features to their implementation. For the practitioner who wants to understand why languages have the features they do — how closures are implemented, what garbage collection costs, how type systems are checked — it provides the implementation-grounded understanding that makes language tradeoffs concrete rather than abstract.
Bjarne Stroustrup’s writing on C++ design, the Rust Book (free at doc.rust-lang.org/book), Rob Pike’s writing on Go’s design philosophy, and the design rationale documents of major languages are valuable primary sources: they explain, from the designers’ perspective, what tradeoffs each language makes and why. Reading the design rationale of a language clarifies what it is for in a way that tutorials do not.
| Book | Role | Type |
|---|---|---|
| Tate, Seven Languages in Seven Weeks (+ Seven More) | Paradigm breadth | Entry |
| Abelson & Sussman, SICP (free) | Deep treatment of programs and languages | Depth |
| Scott, Programming Language Pragmatics (4th ed.) | Features-to-implementation reference | Practice |
| Sebesta, Concepts of Programming Languages (12th ed.) | Comparative language concepts textbook | Reference |
| The Rust Book (free) | Model language design rationale | Entry |
Courses and Lectures
Programming Languages (Dan Grossman, University of Washington, free on Coursera) is the best structured course on the comparative study of programming languages, taught across three languages (ML, Racket, Ruby) chosen to illustrate functional, dynamic functional, and dynamic object-oriented paradigms, with explicit attention to the concepts that transfer and the tradeoffs each design makes. It teaches the comparative perspective that this section advocates, with rigor.
Exercism (exercism.org, free) provides programming exercises in over fifty languages with mentorship feedback focused specifically on writing idiomatic code in each language. It is the most effective free resource for the idiom-learning that is the hardest part of learning a new language well — the mentor feedback specifically addresses whether code is idiomatic, not just whether it works.
| Course | Platform | Type |
|---|---|---|
| Grossman, Programming Languages (free) | Coursera | Entry |
| Exercism (free) | exercism.org | Practice |
Practice, Tools, and Projects
The most effective practice for paradigm breadth is implementing the same non-trivial program in languages from different paradigms — a small interpreter, a text-processing pipeline, a concurrent web scraper — and observing how the solution’s shape changes. The functional version, the object-oriented version, and the concurrent version of the same program reveal what each paradigm makes natural and what it makes awkward, in a way that reading about paradigms cannot.
Language-specific formatters and linters — gofmt, rustfmt and clippy, Black and ruff for Python, Prettier and ESLint for JavaScript/TypeScript — encode and enforce idiomatic conventions. Using them from the first day of learning a language teaches its idioms automatically, by showing what idiomatic code looks like and flagging deviations. They are the most efficient idiom teachers available.
Compiler Explorer (godbolt.org, §4.7 reference) is valuable for understanding what languages compile to: comparing how the same logic compiles in C, Rust, and Go reveals the cost of each language’s abstractions and makes the performance tradeoffs concrete.
Rosetta Code (rosettacode.org, free) presents solutions to the same tasks across hundreds of languages, providing a direct comparison of how different languages express the same computation — useful for seeing idiom differences and for orienting in an unfamiliar language by comparison to a familiar one.
| Resource | Platform | Type |
|---|---|---|
| Multi-paradigm reimplementation exercise | Local | Practice |
| Formatters and linters (gofmt, clippy, Black, Prettier) | Language-specific | Practice |
| Compiler Explorer (free) | godbolt.org | Practice |
| Rosetta Code (free) | rosettacode.org | Reference |
| Effective Go (free) | go.dev | Reference |
| Advent of Code (free; donations optional) | adventofcode.com | Practice |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| The one-language career | A programmer who knows only one language, however well, has their thinking shaped entirely by that language’s paradigm and is vulnerable to that language’s decline. They reach for the solutions their language makes easy and cannot see the solutions it makes hard, because they have no contrast. The limitation is invisible from inside: the one-language programmer does not know what they are not seeing. | Learn at least one language from each major paradigm — an imperative/OO language, a functional language, and a language with a distinctive concurrency model. The goal is not to use all of them professionally but to internalize the ways of thinking they teach. The functional paradigm in particular transfers back to improve code in every other language. |
| Confusing syntax knowledge with language fluency | Knowing a language’s syntax is the entry fee, not fluency. A programmer who can write syntactically correct code in a language but does not know its idioms, its standard library, or its conventions writes code that works but marks them as a novice and is harder for experts to read and maintain. | Learn idiom deliberately: read the standard library source, read well-regarded open-source projects in the language, use the language’s formatter and linter from the start, and seek feedback (from Exercism mentors, code review, or experienced colleagues) specifically on whether your code is idiomatic. Idiom is the difference between writing in a language and writing the language. |
| Choosing languages by fashion | Programming languages have fashions, and the temptation to adopt the currently exciting language for a serious project is real. A language’s novelty is not a reason to use it; an immature ecosystem, a small hiring pool, and uncertain longevity are real costs that compound over a project’s life. The exciting language of this year may be unmaintained in five. | Choose languages for projects based on the ecosystem the task needs, the team’s familiarity, and the language’s maturity and longevity — not on novelty. Reserve experimentation with new languages for personal projects and learning, where the risks are contained. For production systems with long lifetimes, established languages with strong ecosystems are usually the right choice. |
| Treating AI translation as a substitute for learning | AI language models can translate code between languages and answer syntax questions instantly, which accelerates the early stages of learning a new language. A programmer who relies on this without internalizing the new language’s idioms produces working but non-idiomatic code and never develops fluency — they are perpetually translating from a language they know rather than thinking in the new one. | Use AI to accelerate the lookup of specifics — syntax, standard-library functions, how to do a known thing in a new language — while still doing the work that builds fluency: reading idiomatic code, internalizing the language’s patterns, and writing enough code in the language to think in it. AI removes the friction of the lookup; it does not remove the need to internalize the language. |
| Paradigm zealotry | Programmers who discover a paradigm — most often functional programming, but the pattern applies to any — sometimes become evangelists who apply it everywhere, writing rigidly functional code in languages and contexts where it fights the grain, or dismissing other paradigms as inferior. This produces code that is harder to read than a pragmatic mixture would be and signals ideology over judgment. | Hold paradigms as tools, not identities. Each paradigm has contexts where it excels and contexts where it is awkward. The mature use of paradigm knowledge is to apply the right paradigm to each problem and to mix them pragmatically within the idioms of the language being used, not to impose one paradigm everywhere as a matter of principle. |
中文
编程语言是一种工具;像任何工具一样,它会塑造使用者能够构建什么,以及他们如何思考构建这件事。本节不是讨论某一种具体语言,也不是讨论编程语言理论——那是 §3.4 的内容。本节讨论的是实践者与语言的关系:如何为一项任务选择语言,如何高效学习一门新语言,一门语言所体现的范式如何塑造使用者会自然想到的解决方案,以及熟练掌握多门语言意味着什么。这些都是每个职业程序员在整个职业生涯中会反复回答的问题,只是回答得有好有坏。
区分“知道一门语言”和“能够编程”,是起点。一个人记住了 Python 语法,就可以写出能运行的 Python;但这并不等于他能够用 Python 清楚表达一个问题的解法,也不等于他能判断 Python 什么时候不是合适工具,更不等于他能读懂专家写出的地道 Python,并理解为什么代码要这样组织。语言知识只是入场券;真正重要的是如何良好使用这门语言的判断力:该使用哪些惯用法,以及这门语言的设计如何影响其中优秀解法的形状。真正内化这一区分的实践者,在面对一门新语言时,不会先问“语法是什么?”,而会问:“这门语言让什么事情变得容易?让什么事情变得困难?地道代码看起来是什么样?”
更深的主张是:你知道哪些语言,会塑造你能想象哪些解法。多数有经验的程序员最终都会相信这一点。只懂命令式语言的程序员,往往会自然想到循环和可变状态;同时懂函数式语言的程序员,则会把同一个问题看作数据穿过一组操作管道而发生的变换。两种视角都不是普遍正确的,但同时拥有两者的程序员拥有更多选择。这就是跨范式学习语言的实践理由:不是因为你会使用所有这些语言,而是因为每一门语言都会教你一种分解问题的方式,而这种方式会迁移回你日常使用的语言中。
前置知识:编程(§2.1)——假定已经至少熟练掌握一门语言。编程语言理论(§3.4)提供概念基础——类型系统、语义、范式——本节则把这些内容落实到实践中。
这项能力的形状:从一门语言到多门语言
实践者与编程语言的关系,会经历一些可识别的阶段。理解这些阶段,有助于澄清所谓“熟练”到底包含什么。
第一阶段,是熟练掌握一门语言:能够不总是查文档就用它表达解决方案,能够阅读其中的地道代码,并且足够了解它的标准库,以避免重复造已有的轮子。多数人要达到这个阶段,需要一到两年的规律使用。这是必要基础;后面的一切,都建立在至少有一门语言你能流畅思考,而不是在脑子里从伪代码翻译过来。
第二阶段,是认识到语言体现着范式,而范式比语法更深。命令式范式——先做这件事,再做那件事,并在过程中修改状态——继承自冯·诺伊曼架构,体现在 C 中,也体现在大多数语言的命令式核心中。面向对象范式——把状态以及作用于这些状态的操作组织为相互通信的对象——体现在 Smalltalk、Java,以及多范式语言的面向对象特性中。函数式范式——通过求值函数进行计算,避免可变状态——体现在 Haskell、ML 家族,以及越来越多被加入主流语言的函数式特性中。逻辑范式——声明事实和规则,让系统推导答案——体现在 Prolog 中。每一种范式都是一种关于程序是什么、程序应该如何被分解的思考方式;学习一门纯粹体现某种范式的语言,是内化这种范式的方式。
第三阶段,是多语言熟练:能够舒适地跨越多门语言和多种范式,具备合适选择的判断力,也能够快速学习一门新语言,因为新语言提供的大部分内容,都是已经理解过的概念的重新组合。达到这个阶段的程序员,学习第五门语言只需要几天,而不是几个月,因为他们不是在学习如何编程——他们已经知道如何编程——他们是在学习这门特定语言如何表达他们已经理解的概念。真正新的材料只有新语法、具体标准库、惯用法和约定;底层概念,例如变量、函数、类型、控制流、抽象机制,都会迁移。
从实践者角度看,编程语言史就是不断积累复杂性管理方式的历史;其中有几个阶段对理解当代语言格局尤其重要。汇编语言让位于 FORTRAN 和最早的一批高级语言,是因为用更接近问题、而不是更接近机器的方式表达计算,可以减少错误并提高生产力。1960 年代末和 1970 年代的结构化编程运动——Dijkstra 反对 goto 的论证——确立了一个认识:有纪律的控制流会让程序更可靠;随后出现的语言把结构化构造内置进去,例如循环、条件、函数,使 goto 变得不再必要。1980 年代和 1990 年代的面向对象浪潮,把封装和多态作为复杂性管理工具推广出来;它曾被过度宣传,后来则稳定为几种有用方法之一。静态类型与动态类型之争持续数十年,后来很大程度上走向综合:渐进类型让动态语言也获得可选的静态类型,例如 JavaScript 之上的 TypeScript、Python 中的类型提示、Ruby 的 Sorbet;当代较普遍的共识是,当代码库和团队规模超过一定程度后,静态类型通常值得其成本。内存管理则从手动管理(C)发展到垃圾回收(Java 以及多数现代语言),再到 Rust 的所有权模型。Rust 通过静态分析在没有垃圾回收的情况下提供内存安全,这是过去十五年最重要的语言设计发展,因为它证明了一个长期被认为根本性的取舍——安全与控制之间的取舍——可以部分地由更好的类型系统化解。
当代语言格局反映了这段积累的历史。职业程序员最可能遇到的语言——Python、JavaScript/TypeScript、Java、C#、Go、Rust、C++、Swift、Kotlin——各自占据一个由性能、安全性、表达力、生态系统以及主导领域共同定义的位置。Python 主导数据科学、脚本编写,并且越来越主导 AI/ML 工具链,这是因为它可读性强、生态系统庞大,尽管性能有限。JavaScript/TypeScript 主导 Web 前端,因为浏览器只能运行它;并且通过 Node 扩展到后端。Go 占据简单、快速、并发网络服务的生态位。Rust 占据内存安全和性能都重要的生态位——系统编程、嵌入式、性能关键基础设施——并正在进入过去属于 C 和 C++ 的领域。理解为什么每门语言会占据自己的生态位,而不是把语言选择看成偏好或潮流问题,是实践者判断力的一部分。
选择、学习和用语言思考
为任务选择语言
为一个项目选择语言,是一项会在项目整个生命周期中持续产生后果的工程决策。因此,它应该被有意识地做出,而不是靠默认选择或流行趋势决定。
主导因素通常不是语言的内在性质,而是它的生态系统和语境。最重要的问题常常是:这项任务需要什么库和框架?哪门语言拥有最好的相关生态?机器学习项目会选择 Python,并不是因为 Python 是这项任务上设计最好的语言,而是因为 PyTorch、TensorFlow、scikit-learn 以及整个机器学习生态都在 Python 中。Web 前端会选择 JavaScript/TypeScript,因为浏览器运行的就是它。必须与既有代码库集成的项目,通常会选择既有代码库使用的语言。这些语境因素通常压过语言本身的内在属性。
当生态系统没有强制选择时,语言的内在取舍才变得重要。性能要求:这项任务是否需要编译型、低层语言的速度,例如 C、C++、Rust、Go?还是高层语言的生产力,例如 Python、Ruby,比原始速度更有价值?安全要求:任务失败的代价是否足以证明使用静态类型、内存安全语言的强保证是值得的?还是动态语言的灵活性更重要?并发要求:任务是否涉及大量并发,Go 的 goroutine、Rust 的 async,或 Erlang 的 actor 模型是否会提供真实优势?团队熟悉度:团队熟悉的语言,通常比理论上更优但团队必须重新学习的语言更好,因为学习的生产力成本真实且立即发生,而理论上的优越性则是推测性的。
错误语言选择的成本是不对称的,而且会随时间增长。项目早期切换语言成本较低;一旦代码库变大,积累了库,团队也围绕这门语言训练起来,切换就会变得极其昂贵。这种不对称性说明,初始选择值得认真对待;对于有长期寿命的项目,也更应该优先选择生态系统和招聘池都强的成熟语言,而不是选择那些优势真实、但风险会在项目生命周期中不断累积的新语言,例如生态不成熟、招聘池小、寿命不确定。
高效学习一门新语言
学习第 n 门语言,其中 n 大于二或三,与学习第一门语言是完全不同的活动。概念会迁移;真正新的只是细节。高效学习应当利用这一点。
最高效的路径,是先把新语言映射到已经知道的概念上:这门语言如何处理变量、函数、类型、控制流、错误处理、模块和抽象机制?对于其中大多数,新语言都只是已知模式的一个变体;学习就是识别这个变体。真正值得集中注意力的是这门语言引入、而学习者尚未掌握的新概念。学习 Rust 时,真正新的材料是所有权和借用系统;其他大多数东西都是熟悉的。学习 Haskell 时,真正新的材料是纯函数、惰性求值,以及基于类型类的抽象;其他部分可以映射到已知概念。学习 Go 时,真正新的材料是 goroutine 和 channel 的并发模型;除此之外,这门语言有意保持极简。识别什么是真正新的,并把注意力集中在那里,而不是把整门语言都当成全新的东西,正是有经验程序员能够快速学习语言的原因。
惯用法是学习一门语言中最困难、也最容易被忽视的部分。每门语言都有表达常见模式的地道方式;语法正确但不地道的代码,会显示出作者仍是这门语言的新手,而且专家读起来往往更困难。用 Python 写 Java 风格代码——使用 getter 和 setter、深层类继承、在 Python 惯用法可以很简洁的地方使用冗长模式——会得到能运行但与语言作对的代码。学习惯用法需要阅读地道代码:标准库、评价良好的开源项目、官方风格指南;也需要写足够多的代码,内化其中模式。工具会提供帮助:linter 和 formatter,例如 gofmt、Black、rustfmt、Prettier、clippy,会编码并自动执行惯用约定;从学习一门语言的第一天就使用它们,等于让工具教你这门语言的约定。
AI 辅助时代以一种特定方式改变了语言学习:语言模型可以在语言之间翻译代码,可以解释惯用法,也可以即时回答“在 Y 语言中如何做 X”这类问题。这会大幅加速新语言学习的早期阶段——查找语法和标准库函数的摩擦基本消失了。但它也引入一种风险:如果程序员依赖 AI 翻译,却没有内化新语言的惯用法,就会写出能工作但不地道的代码,并且永远发展不出用这门语言思考所带来的流利度。AI 在语言学习中的有效用法,是加速具体细节的查找,同时仍然完成阅读地道代码和内化模式的工作,而不是绕过这些工作。
范式如何塑造思考
跨范式学习语言的最强理由是:每一种范式都提供一种分解问题的方式,而这种方式会迁移回你日常使用的任何语言中。
函数式范式教会的是通过数据变换来分解问题:一个问题由一组纯函数组成的管道来解决,每个函数都把输入转化为输出,并且不产生副作用。一旦内化,这种思考方式也会改善命令式语言中的代码——学过函数式思维的程序员,即使写 Python 或 JavaScript,也会写出更清楚的代码:在有助于清晰表达时使用 map/filter/reduce 和不可变数据,避免命令式程序员容易默认使用的纠缠在一起的可变状态。已经迁移到主流语言中的函数式概念——一等函数、闭包、不可变性、高阶函数、代数数据类型和模式匹配——正是到处可用的函数式思维。
面向对象范式教会的是通过责任来分解问题:一个问题由一组对象解决,每个对象负责某些数据和行为,并通过定义良好的接口彼此通信。它关于封装的洞见——把内部状态隐藏在接口之后——以及关于多态的洞见——通过共享接口统一处理不同类型——即使在非面向对象语境中也有价值。它的过度扩张,例如深层继承结构、试图把一切都建模为对象,以及某些面向对象语言强加的仪式感,则是这个领域已经学会更谨慎使用的部分。
并发范式教会的是通过独立活动来分解问题:actor 模型(Erlang、Akka)、通信顺序进程(Go 的 goroutine 和 channel),以及 async/await(Rust、JavaScript、Python),分别提供了不同的心智模型,用来组织同时做许多事情的程序。随着并发越来越不可避免,这些模型也越来越重要。理解不止一种并发模型,可以为组织并发系统提供更多选项,而只知道线程和锁的程序员不具备这些选项。
范式多样性的实践收益,就是选项。只用命令式方式思考的程序员,只会看见一种解法;跨范式流利的程序员,会看见几种解法,并能够选择。这并不是主张在每个程序中使用所有范式——那会制造不连贯的代码——而是主张让这些范式成为可用的观察方式。这样,你自然想到的解法,就是从一系列可能性中选择出来的,而不是你唯一能够想象的东西。
熟练之后会发生什么变化
跨语言和跨范式的流利度,会改变实践者面对问题和工具的方式。
第一种变化,是语言不再限制思考。只懂一门语言的程序员,会用这门语言的术语来思考,并被它们限制;懂多门语言的程序员,则会先思考问题本身,再用适合的语言表达解法。他会选择语言来适配问题,而不是把问题塞进自己唯一熟悉的语言中。这是一种解放:问题可以按照自身方式被处理,语言则作为合适工具被选择。
第二种变化,是快速学习新语言和新工具的能力,而这种能力会在整个职业生涯中复利增长。技术会变化;某一年代的主流语言,会在下一年代被取代或补充。如果一个实践者的能力绑定在某一种具体语言上,那么当这门语言衰落时,他就会脆弱;如果他已经内化了能跨语言迁移的概念,就能够适应,把新语言当成已知内容的一个变体来学习。在具体工具不断变化的领域中,可迁移的概念能力才具有耐久价值。
第三种变化,是即使只写一门语言,代码也会更好。学过函数式思维的程序员会写出更清楚的命令式代码;理解类型系统的程序员会写出结构更好的动态类型代码;用过多种并发模型的程序员,会更清楚地组织并发代码。多范式之间的交叉授粉,会改善每一门语言中的代码,因为每种范式的洞见并不局限于那些纯粹体现它的语言。
第四种变化,是对语言争论的校准。编程语言讨论——静态类型与动态类型、这门语言与那门语言、范式战争——会产生强烈观点和大量争执。认真跨语言、跨范式工作过的实践者,会更轻地持有这些争论:他们理解每种方法都有真实优势和真实成本,正确选择依赖语境,而那种坚称某一语言或某一范式普遍更优的立场,通常更多说明经验有限,而不是洞见深刻。
资源
书籍与文本
Bruce Tate 的 Seven Languages in Seven Weeks(Pragmatic Bookshelf,2010)及其后续 Seven More Languages in Seven Weeks,是获得范式宽度的最直接路径。每一章介绍一门被选来说明一种不同思维方式的语言——Ruby、Io、Prolog、Scala、Erlang、Clojure、Haskell——其深度足以传达范式本身,但不要求多年使用才能达到熟练。重点不是精通七门语言,而是体验七种关于编程的思考方式。对于已经掌握一两门主流语言的实践者来说,这是最快发现“还有其他分解问题方式”的路径。
Abelson 和 Sussman 的 Structure and Interpretation of Computer Programs(第 2 版,MIT Press,1996,可在 mitpress.mit.edu/sicp 免费获取)——§2.1 中也提到过——仍然是关于编程语言与编程行为之间关系的最深处理。它依次讲解函数式编程、状态与对象,以及解释器和编译器的实现,揭示“使用一门语言”和“实现一门语言”之间的边界远比表面上更可渗透。它使用 Scheme,但它的主题是任何语言中程序的结构。
Michael Scott 的 Programming Language Pragmatics(第 4 版,Morgan Kaufmann,2015)是把语言特性与其实现连接起来的综合参考书。对于想理解语言为什么有这些特性的实践者——闭包如何实现,垃圾回收有什么成本,类型系统如何检查——它提供了基于实现的理解,使语言取舍变得具体,而不是抽象。
Bjarne Stroustrup 关于 C++ 设计的写作、Rust Book(可在 doc.rust-lang.org/book 免费获取)、Rob Pike 关于 Go 设计哲学的写作,以及主要语言的设计理由文档,都是有价值的一手来源:它们从设计者视角解释每门语言做了什么取舍,以及为什么这么做。阅读一门语言的设计理由,比教程更能澄清这门语言究竟是为什么而存在。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Tate,Seven Languages in Seven Weeks(以及 Seven More) | 范式宽度 | 入门 |
| Abelson & Sussman,SICP(免费) | 对程序和语言的深入处理 | 深入 |
| Scott,Programming Language Pragmatics(第 4 版) | 从特性到实现的参考书 | 实践 |
| Sebesta,Concepts of Programming Languages(第 12 版) | 比较性语言概念教材 | 参考 |
| The Rust Book(免费) | 语言设计理由的范例 | 入门 |
课程与讲座
Programming Languages(Dan Grossman,University of Washington,可在 Coursera 免费学习)是关于编程语言比较研究的最佳结构化课程。它横跨三门语言:ML、Racket、Ruby,分别用来展示函数式、动态函数式和动态面向对象范式,并且明确关注可迁移概念以及每种设计做出的取舍。它以严谨方式教授本节所主张的比较视角。
Exercism(exercism.org,免费)提供超过五十门语言的编程练习,并带有导师反馈,重点尤其放在用每门语言写出地道代码。它是学习一门新语言时最困难的部分——惯用法学习——最有效的免费资源;导师反馈关注的不只是代码能不能工作,而是它是否地道。
| 课程 | 平台 | 类型 |
|---|---|---|
| Grossman,Programming Languages(免费) | Coursera | 入门 |
| Exercism(免费) | exercism.org | 实践 |
实践、工具与项目
获得范式宽度最有效的练习,是用不同范式的语言实现同一个非平凡程序——一个小型解释器、一个文本处理管道、一个并发网页爬虫——并观察解法形状如何变化。同一个程序的函数式版本、面向对象版本和并发版本,会直接展示每种范式让什么变得自然、让什么变得别扭;这种理解不是单靠阅读范式介绍能获得的。
语言专用 formatter 和 linter——gofmt、rustfmt 和 clippy,Python 的 Black 和 ruff,JavaScript/TypeScript 的 Prettier 和 ESLint——会编码并执行惯用约定。从学习一门语言的第一天开始使用它们,会自动教你这门语言的惯用法:它们会展示地道代码应该是什么样,并标出偏离惯用法的地方。它们是目前最有效的惯用法教师。
Compiler Explorer(godbolt.org,§4.7 参考)对于理解语言会编译成什么非常有价值:比较同一段逻辑在 C、Rust 和 Go 中编译出来的结果,可以揭示每门语言抽象的成本,并使性能取舍变得具体。
Rosetta Code(rosettacode.org,免费)展示了数百门语言对同一任务的解法,提供了一个直接比较不同语言如何表达同一计算的窗口——这对于观察惯用法差异,以及用熟悉语言对照陌生语言进行定位,都很有用。
| 资源 | 平台 | 类型 |
|---|---|---|
| 多范式重新实现练习 | 本地 | 实践 |
| Formatter 和 linter(gofmt、clippy、Black、Prettier) | 各语言专用 | 实践 |
| Compiler Explorer(免费) | godbolt.org | 实践 |
| Rosetta Code(免费) | rosettacode.org | 参考 |
| Effective Go(免费) | go.dev | 参考 |
| Advent of Code(免费;可选捐赠) | adventofcode.com | 实践 |
陷阱
| 陷阱 | 为什么会误导 | 更好的回应 |
|---|---|---|
| 单语言职业生涯 | 一个只懂一门语言的程序员,即使对这门语言很熟,也会让自己的思考完全受这门语言的范式塑造,并且容易受到这门语言衰落的影响。他会自然选择这门语言让它容易想到的解法,却看不见这门语言让它难以想到的解法,因为他没有对照。这种限制在内部是不可见的:单语言程序员并不知道自己看不见什么。 | 至少学习每种主要范式中的一门语言——一门命令式/面向对象语言,一门函数式语言,以及一门具有独特并发模型的语言。目标不是把它们都用于职业工作,而是内化它们所教的思考方式。尤其是函数式范式,会迁移回来改善任何其他语言中的代码。 |
| 把语法知识误认为语言流利度 | 知道一门语言的语法只是入场券,不是流利。程序员如果能写出语法正确的代码,却不知道这门语言的惯用法、标准库和约定,那么他的代码可以工作,但会显示出新手痕迹,而且对专家来说更难阅读和维护。 | 有意识地学习惯用法:阅读标准库源码,阅读这门语言中评价良好的开源项目,从一开始就使用 formatter 和 linter,并专门寻求关于代码是否地道的反馈,例如 Exercism 导师、代码审查或有经验同事的反馈。惯用法,就是“用一门语言写代码”和“写这门语言本身”之间的差别。 |
| 按潮流选择语言 | 编程语言会有潮流,把当下令人兴奋的新语言用于严肃项目,这种诱惑是真实的。但一门语言新颖,并不是使用它的理由;生态不成熟、招聘池小、寿命不确定,都是会在项目生命周期中不断累积的真实成本。今年令人兴奋的语言,五年后可能无人维护。 | 为项目选择语言时,应基于任务所需生态、团队熟悉度,以及语言的成熟度和长期生命力,而不是基于新奇感。把新语言实验留给个人项目和学习场景,在那里风险是可控的。对于寿命较长的生产系统,拥有强生态的成熟语言通常才是正确选择。 |
| 把 AI 翻译当作学习替代品 | AI 语言模型可以在语言之间翻译代码,并即时回答语法问题,这会加速学习新语言的早期阶段。但如果程序员依赖它而不内化新语言的惯用法,就会写出能工作但不地道的代码,也永远发展不出真正的流利度——他会一直从自己已知的语言进行翻译,而不是在新语言中思考。 | 使用 AI 来加速具体细节查找:语法、标准库函数、如何在新语言中完成一件已知事情;但仍然要完成建立流利度所需的工作:阅读地道代码,内化语言模式,并写足够多的代码,直到能用这门语言思考。AI 消除了查找的摩擦,但并不消除内化语言的必要。 |
| 范式狂热 | 程序员在发现一种范式之后——最常见的是函数式编程,但这种模式适用于任何范式——有时会变成传教士,把它到处套用,在并不适合的语言和场景中写出僵硬的函数式代码,或者把其他范式斥为低劣。这会产生比实用混合更难阅读的代码,也显示出意识形态压过判断力。 | 把范式当作工具,而不是身份。每种范式都有擅长的语境,也有别扭的语境。成熟使用范式知识,意味着为每个问题选择合适范式,并在所用语言的惯用法之内务实混合,而不是出于原则把一种范式强加到所有地方。 |