本文首发于:我的个人博客
Haskell 是一门风格极其独特的语言。网络上我们往往能够看到各式各样对其函数式特性的评价,我相信不少人对其的印象就是一门“函数式纯度高、学术风格浓厚、学习曲线陡峭”的语言。
最近机缘巧合下我尝试了一下这个语言。从我个人的感觉而言,这门语言其实并没有想象中那么难,很多特性的引入不仅在我看来很自然,对于代码抽象程度和表达能力的提升也是肉眼可见的。
这篇文章我将以一个有其他函数式语言经验的 Haskell 初学者视角,谈谈我的看法。
如果你还没有决定是否要学这门有趣的语言,或者刚刚开始你的 Haskell 旅程,我想看完本文以及计划中的系列文章后,你会有一些全新的想法。
1. 为什么要学 Haskell?
对我而言,理由其实很简单:之前接触了不少语言,有些是混合范式语言中具备不少函数式风格,有些虽然是函数式语言,但由于各种设计上的考量最后还是引入了一些没那么函数式的特性。随着对函数式风格理解的逐渐深入,最终接触最纯粹的函数式语言 Haskell 几乎是必然的。
当然,除了对我自己,我始终觉得对于任何一个开发人员而言,学习掌握函数式风格语言独特的抽象方式都是有价值的。
1.1 有助于学习其他新语言
首先,学习掌握函数式语言,特别是 Haskell,对于深入理解其他语言在新版本中引入的特性是有很大帮助的。
比如,掌握了模式匹配、匿名函数等概念,在写 Python 的时候就会发现,新版本引入的模式匹配和匿名函数语法糖其实还是很有用的,合理使用可以提高代码的表达能力和可读性。
再比如,理解 Haskell 的高阶函数以及应用场景,会发现可以在 JavaScript 等很多语言用高阶函数简化层层嵌套的循环结构。
许多主流语言在近十年间积极吸收函数式语言的发展成果,这使得学习函数式语言,客观上也达到了学习其他语言创新成果的目的。而且,由于 Haskell 的纯粹性和体系性,这些函数式风格的威力和精妙之处,往往在 Haskell 编码实践中才能得到最深刻的体会。
总而言之,即便你以极其功利的目的,学习 Haskell 也不亏。从来没有浪费的学习。
1.2 有助于获得全新的抽象思维
其次,由于函数式风格和传统面向对象的差异很大,使用的抽象思维方式也大相径庭。学习 Haskell 可以让我们在面临同样的工程需求时用截然不同的抽象视角来完成工作。
面向对象需要工程师思考类之间的职责划分、继承层次和对象交互;而函数式编程则依赖代数数据结构的设计、不同函数的组合以及副作用的严格隔离等。
在一些工程中的重要问题上,Haskell 的函数式特性也带来了和其他语言间的巨大差距。例如,全局不可变性和无副作用函数彻底规避了共享状态竞争,设计并发程序兼具简洁性和工业级效率;基于代数数据类型和 Monad 抽象的 Maybe/Either 等类型,将错误处理编码进类型系统,通过严格的编译期检查覆盖所有分支,避免了许多运行时崩溃。
显然,二者所需的工程思维和架构方法是完全不同的,对于同一个现实问题,掌握函数式编程的人就可以使用一种风格迥异、许多时候表达能力更强的方式将问题转化成逻辑和代码语言。
1.3 坚若磐石的稳定性
进一步来说,我觉得即便你不喜欢上述所有提到的特性,有一点是必须说明的:Haskell 通过严格的类型系统使得所有能编译运行的代码有相当的质量保障。
Haskell 的严格编译期检查,有一些人可能不太喜欢,会觉得编译期检查影响了编写代码的速度。
实际上,这些检查的逻辑在于把很多运行时测试才可能发现的问题提前到编译期一起解决掉,确保可编译代码的质量,极大提升了软件上线后的稳定性和可维护性。
实际上,如果我们充分对比动态语言软件和 Haskell 在测试、维护、重构与拓展等多阶段的情况,我们很可能发现后者的严格检查虽然在编译期需要更多时间处理,但从全局来看反而是提高了效率的。
当然,Haskell 社区内流传的传说“编译通过即正确”固然是夸张的说法,毕竟编译期无法验证软件业务逻辑是否正确,该有单元测试、集成测试一样都不能少。然而,编译通过意味着代码排除了所有编译期涵盖的错误类别(如类型不匹配、分支覆盖不完备、副作用处理不当等)。这种编译期给开发者带来的安全感和对核心正确性的信心,是其他语言无法比拟的。
从这个角度而言,我个人觉得唯一可以和 Haskell 实现类似“把错误提早到编译期”理念的语言就是 Rust。不过 Rust 为了实现底层内存操作,引入了所有权、生命周期等概念。如果你觉得某个项目不追求手动控制内存带来的极致性能,可以接受由 GC 来回收内存垃圾带来的便利性和开销,那么 Haskell 是在更高抽象层次上践行把问题提前解决的极佳选择。
1.4 极高的 AI 驱动编程契合度
注:放心往下读,我这里没有一块钱 AI 课程广告~
顺着上一点往下说,我一直觉得强调严格编译期检查的语言,特别适合 AI 辅助编程,乃至完全由 AI 智能体驱动的开发。
原因很简单:编译期错误信息通常是确定性的、局部的,并且直接指向代码中违反语言规则的具体位置。大语言模型可以相对容易地通过上下文和详细报错信息,尝试推断问题根源并修复错误。
对于 Haskell 和 Rust 而言,如果代码有问题,可以直接向大语言模型提供详尽的编译器报错信息,让 AI 根据报错和上下文自主排查错误根源,排除类型错误。
反之,如果是 Python 的代码,可能有些时候看似正确的代码其实是有问题的,只有进入运行时才能够被发现。运行时排查问题,除非这个软件本身就有完善的日志系统可以清晰向 AI 提供相关信息和问题复现条件,很多时候解决问题还得靠人类开发者。
当然,这不是说 Haskell 生成的代码就一定比 Python 好。实际上,由于 Python 训练语料更多,一般而言 Python 代码的生成质量是不错的。
但即便 Python 的代码是正确的,我在使用前往往需要大量测试才敢用;对于 Haskell,AI 如果能生成可编译的代码并且人工复核业务逻辑无误,那么代码的基础健壮性就有了一定的保证,显著降低 AI 生成代码引入隐蔽运行时错误的可能性。
2. 核心概念一览表 (中英对照)
学习任何具备一定体系性的知识,首先都需要把“要学什么”的路径图规划清楚。
Haskell 作为一种语法设计高度统一的语言,把握住正确的学习方向和路径就尤为重要。而把学习方向的第一步,就是要对核心概念有一个基本的印象。
由于 Haskell 中文社区很多大佬的文章往往会混用一些中文表达的概念和英文表达的概念,我觉得有必要提供一个简单的中英文概念对照。很多时候让你一头雾水的某个概念,可能换一个语言环境解释就清楚了。
这里,我提供一个中英文对照的概念表,以及一个非常简略的介绍。我认为初学阶段需要掌握的概念包括:
| 中文概念 | 英文概念 | 说明 |
|---|---|---|
| 函数定义 | Function Definition | 使用 = 定义函数、参数、返回值 |
| 基本类型 | Basic Types | Int, Char, Bool, String/List/Tuple 等 |
| 模式匹配 | Pattern Matching | 可以代替大量嵌套 if...else... 结构,舒适定义代码的控制流 |
| 匿名函数 | Anonymous Function | \x->x + 1,不需要命名就可以使用的函数 |
| 高阶函数 | Higher-Order Function | 接受函数作为参数,或者返回函数的函数,如 map/filter/foldl |
| 柯里化 | Currying | 所有函数本质上只有一个参数,(a,b)->c 等价于 a->b->c |
| 部分应用 | Partial Application | 只提供函数部分参数,与柯里化高度相关 |
| 代数数据类型 | Algebraic Data Type (ADT) | data 关键字定义类型,可以是枚举、结构体或它们的组合 |
| Maybe 类型 | Maybe Type | Maybe a = Just a | Nothing |
| 类型类 | Typeclass | 类似接口,定义一组函数,类型通过实现函数来声明支持该行为 |
| 惰性求值 | Lazy Evaluation | Haskell 一大语法特点 |
| 函子 | Functor | 可被映射的数据容器或上下文 |
| 应用函子 | Applicative | 一种承载了函数的容器,可被应用于 Funtor |
| 单子 | Monad | 代表顺序计算链,用于处理副作用、可选值、错误等上下文中的计算 |
| do 语法 | do Notation | 好用的编写 Monad 计算的语法糖 |
这个列表大体上遵循了从上到下由易到难,如果你还没有开始学 Haskell,我觉得完全可以按图索骥一步一步掌握语言的核心思想。
掌握这个列表中的概念,是学习 Haskell 语言和阅读社区代码的关键基础。当然,实际项目中还需要理解更多细节和工具链的使用,但把握住这些概念就具备了构建复杂软件的基本能力。
3. Haskell 怎么学
我觉得 Haskell 学习过程中最重要的一点,就是多找教程交叉比对。
我之前专门写过一个回答论证“你学不明白不见得是你的问题,可能是没找对教程的问题”,我觉得理解这一点在学习 Haskell 中十分重要。
Haskell 语言很多高级抽象工具,像是 Monad,想要单凭一篇文章讲明白其实是很困难的,但如果你读了二三十篇技术博客谈这一个概念,即便仍然不能完全理解,起码多篇不同侧重点的文章建立起对一个概念的立体理解。
总而言之,找到适合自己理解的教程是相当重要的,而唯一能确保这一点的就是先浏览大量材料,再从中挑选契合的文章重点精研。
此外,我还想说一点就是实践优先,在做中学。
Haskell 本身的语言设计可能有不少学术成分,网络上一些人也有论及 Haskell 必谈抽象代数、范畴论的习惯。
我不是说理解背后的深层次数学思想无用,只是如果你对这些数学工具了解不多,大可不必被吓到。实际上,Haskell 选择这些语言特性更多是出于实践因素的考量,只有不断在项目中实践,才能够真正理解深层次的设计理念。
须知,不必深入理解数学思想,也可以高效运用 Haskell 解决问题。理解底层数学原理有助于揣摩设计动机,但绝不是使用 Haskell 的门槛。
最后,如果你对这个语言感兴趣,欢迎关注我,我将在未来的一段时间持续讲解编写 Haskell 的心得体会和实践经验。