Haskell学习笔记
基本概念
/ˈhæskəl/
Haskell 是一种纯函数式 (purely functional) 编程语言。
可以把它想象成一种“数学家”设计的语言。在大多数语言中(如 Python, Java, C++),你是在给计算机下达一步一步的指令(“先做 A,再修改 B,然后循环 C”)。
但在 Haskell 中,你是在“描述事物是什么”。你编写的程序更像是一系列数学函数的定义,程序的核心就是对这些函数进行求值。
Haskell 的核心特点(它为何如此特别)
Haskell 之所以独特,主要基于三大支柱:
1. 纯函数式 (Purely Functional)
这是它最核心的灵魂。
-
无副作用 (No Side Effects): 在 Haskell 中,一个函数除了根据输入计算并返回一个结果外,不能做任何其他事情。它不能修改一个全局变量,不能打印到屏幕,也不能读取文件。(别担心,它有特殊的机制来处理这些“不纯”的操作)。
-
引用透明 (Referential Transparency): 这意味着一个函数,只要输入相同,永远返回相同的结果。例如,
add(2, 3)无论在何时何地调用,都必须返回5。这使得代码非常容易推理、测试和并行化。
2. 惰性求值 (Lazy Evaluation)
Haskell 是一个“懒惰”的语言。
-
按需计算: 它只会在“绝对必要”时才去计算一个表达式的值。例如,如果你定义了一个包含 10 亿个元素的列表,但你最终只需要访问它的前 3 个元素,Haskell 就只会计算那 3 个元素,而不会浪费资源去生成整个列表。
-
处理无限数据: 这使得 Haskell 可以优雅地定义和处理“无限”的数据结构(比如一个包含所有偶数的无限列表)。
3. 强静态类型 (Strong, Static Typing)
Haskell 对“类型”极其严格。
-
静态类型: 在程序运行(编译)之前,所有变量的类型都必须是已知的。
-
强类型: 语言不允许你“乱来”。你不能把一个数字当作一个字符串来用,除非你显式地进行了转换。
-
类型推导 (Type Inference): 这是它强大的地方。虽然它很严格,但你不必像在 Java 中那样总是写明类型。Haskell 的编译器非常聪明,能自动推导出你函数和变量的类型。
总结一下:
Haskell 是一种高抽象、数学感极强的语言。它通过强制你写“纯净”的代码,来换取极高的可靠性、可维护性和并发性。
Haskell 适合用来做什么?
由于其对“正确性”的极致追求,Haskell 在那些“绝对不能出错”的领域中备受青睐:
-
学术界与科研: 它是编程语言理论研究的“活实验室”。
-
金融与量化交易: 用于编写复杂的算法和模型,因为“纯函数”能保证计算的精确和可复现性。
-
编译器与工具链: 许多其他编程语言的编译器或解释器是用 Haskell 编写的。
-
数据分析与处理: 尤其擅长构建复杂的数据转换管道(ETL)。
-
高并发系统: 由于其“不可变性”(Immutability)的特性,Haskell 能非常容易地编写出没有“竞态条件”等常见并发 bug 的程序。
学习它意味着什么?
-
学习曲线陡峭: 对于习惯了命令式编程(如 Python/Java)的开发者来说,Haskell 的思维方式(函数式思维)是一个巨大的转变。
-
极高回报: 即使你以后不用 Haskell 工作,学习它也会彻底改变你对“编程”的看法,让你在用其他语言时也能写出更简洁、更可靠的代码。
Haskell VS. TypeScript
这是一个描述ECharts核心本质的例子。
-- 顶层配置是一个递归积类型
type EChartsOption = {
title : Maybe Title,
legend : Maybe Legend,
grid : List Grid,
xAxis : List Axis,
yAxis : List Axis,
series : List Series, -- 核心:数据映射规则
tooltip : Maybe Tooltip,
...
}
-- 每个Series是一个态射(数据 → 视觉)
type Series = {
type : ChartType, -- 几何原语选择器
data : List Data, -- 定义域
encode : Encoding, -- 映射函数
style : VisualMap -- 样式函数
}
-- 编码函数本质是一个λ演算
type Encoding = {
x : Field → Position,
y : Field → Position,
color : Field → Color,
...
}
为什么这个例子要用haskell,而不是用typescript呢? 这个问题触及了思维工具与工程工具的本质区别。
我的选择基于一个核心原则:用数学语言思考,用工程语言实现。

一、类型系统的能力差异
Haskell:代数数据类型(Algebraic Data Types)
Haskell的类型是构造式的,能精确表达数学结构:
haskell
复制
-- 这是一个"和类型"(Sum Type),表示"要么是A,要么是B"
type Chart = LineChart Series | BarChart Series | ScatterChart Series
-- 这是一个"积类型"(Product Type),表示"同时有A和B"
type Encoding = { x :: Field, y :: Field }
-- 这是高阶类型(Type Constructor),相当于类型层面的函数
type Series a = { encode :: a -> VisualMark }
关键特性:Haskell的类型系统本身是一个形式化规约系统,类型检查就是定理证明。
TypeScript:结构性类型(Structural Types)
TypeScript的类型是描述式的,更注重实用性:
TypeScript
复制
// 这是"鸭子类型":只要结构对得上就行
type Chart = {
type: 'line' | 'bar' | 'scatter';
series: Series;
}
// 无法直接表达"和类型"与"积类型"的严格数学区分
// 也无法在类型层面进行"计算"
关键局限:TypeScript的类型系统是为了辅助开发,而非形式化证明。它的类型是”描述”,不是”定义”。
二、为什么形式化方法需要Haskell?
1. 定理证明的表达能力
在Haskell中,我可以写:
haskell
复制
-- 这不仅是类型声明,更是**待证明的命题**
Theorem coord_consistency :: Option -> Bool
coord_consistency opt =
length (grid opt) > 0 ==> length (xAxis opt) == length (yAxis opt)
这个定理可以被Coq/Isabelle等证明助手验证。而TypeScript的类型无法上升到这个抽象层次。
2. λ演算的直接对应
Haskell表达式与数学λ演算几乎一一对应:
haskell
复制
-- 数学: λd. (x(d), y(d))
encode = \d -> (x d, y d)
TypeScript则需要引入运行时语义:
TypeScript
复制
// 这是"函数",不是纯粹的λ项
const encode = (d: Data) => ({ x: d.x, y: d.y });
3. 心智模型的纯净性
学习ECharts的本质时,Haskell能剥离所有副作用,只保留纯粹的映射关系:
haskell
复制
-- 纯粹的、无副作用的数学映射
data -> visual = encode(data)
-- 渲染是另一个独立过程
render(visual) = dom
TypeScript的代码则混杂了类型、值、副作用:
TypeScript
复制
// 类型、数据、副作用混在一起
const option = { // 这是值
series: data.map(d => encode(d)) // 运行时计算
}
chart.setOption(option); // 副作用:渲染
三、实战时如何切换到TypeScript?
Haskell是”思维草稿纸”,TypeScript是”实现蓝图”。
思维过程(Haskell):
haskell
复制
-- 第一步:在脑中用Haskell推导
myEncode :: StockData -> (Time, Price, Size)
myEncode d = (date d, price d, sqrt (volume d))
-- 第二步:验证类型正确性
-- :t myEncode -- 编译通过,证明逻辑正确
实现过程(TypeScript):
TypeScript
复制
// 第三步:将Haskell签名"编译"到TypeScript
const myEncode: Encoding<StockData> = {
x: (d) => d.date.getTime(), // 对应 (Time, ...)
y: (d) => d.price, // 对应 (..., Price, ...)
size: (d) => Math.sqrt(d.volume) // 对应 (..., Size)
}
转换规则:
-
Haskell的函数类型
a -> b→ TypeScript的函数(a: A) => B -
Haskell的记录
{x: A, y: B}→ TypeScript的接口interface { x: A, y: B } -
Haskell的代数类型
A | B→ TypeScript的联合类型A | B
四、如果全用TypeScript会怎样?
假设我一开始就用TypeScript:
TypeScript
复制
// 看起来熟悉,但丢失了关键信息
type Option = {
title?: Title;
legend?: Legend;
grid?: Grid[];
// ... 无穷无尽的嵌套
}
问题:
-
噪音太多:
?表示可选,但数学上”可选”是什么?(Haskell用Maybe显式表达) -
无法抽象:无法表达”所有Series都是某种态射”这个通用模式
-
依赖记忆:你只能看到”这个字段有什么”,看不到”这个配置能推导什么”
你会陷入API文档沼泽,而非第一性原理的清晰结构。
五、核心结论:思维的分层
复制
抽象层次
↑
| Haskell层:形式化规约、定理证明、λ演算
| (思考"可视化是什么")
↓
| TypeScript层:具体实现、类型检查、工程实践
| (思考"如何用ECharts实现")
↓
| JavaScript层:运行时、副作用、DOM操作
| (思考"代码怎么跑起来")
↓
Haskell不是为了”运行”,而是为了”理解”。
就像学物理时,我们先用数学公式(F=ma)理解原理,再用计算机模拟实现计算。前者是思维工具,后者是工程工具。
六、给你的实践建议
学习时:
-
用Haskell/伪代码在纸上推导数据→视觉的映射函数
-
画出BNF文法树,理解配置的递归结构
-
在脑中用类型签名验证正确性
编码时:
-
将推导出的λ表达式翻译成TypeScript函数
-
将代数数据类型翻译成接口和联合类型
-
将形式化规约写成单元测试
验证时:
TypeScript
复制
// TypeScript中模拟Haskell的定理检查
type AssertCoordConsistency<O extends Option> =
O['grid'] extends { length: infer L }
? L extends 0
? true
: O['xAxis'] extends { length: infer X }
? O['yAxis'] extends { length: infer Y }
? X extends Y ? true : never // 相等则通过,否则never
: never
: never
: never;
// 使用
type MyOption = { grid: [...], xAxis: [...], yAxis: [...] };
type Check = AssertCoordConsistency<MyOption>; // 若xy不等,这里会是never
TypeScript的类型体操虽然能模拟部分形式化能力,但语法噪音大、心智负担重。Haskell让形式化思维自然涌现,TypeScript让实现可运行。
最终答案:我用Haskell不是抛弃TypeScript,而是用最适合抽象思维的语言来思考,用最适合工程实现的语言来编码。两者不是对立,而是思维-实现的完整闭环。