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核心本质的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 顶层配置是一个递归积类型
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 : FieldPosition,
y : FieldPosition,
color : FieldColor,
...
}

为什么这个例子要用haskell,而不是用typescript呢?
这个问题触及了思维工具工程工具的本质区别。

我的选择基于一个核心原则:用数学语言思考,用工程语言实现


一、类型系统的能力差异

Haskell:代数数据类型(Algebraic Data Types)

Haskell的类型是构造式的,能精确表达数学结构:

haskell

复制

1
2
3
4
5
6
7
8
-- 这是一个"和类型"(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

复制

1
2
3
4
5
6
7
8
// 这是"鸭子类型":只要结构对得上就行
type Chart = {
type: 'line' | 'bar' | 'scatter';
series: Series;
}

// 无法直接表达"和类型"与"积类型"的严格数学区分
// 也无法在类型层面进行"计算"

关键局限:TypeScript的类型系统是为了辅助开发,而非形式化证明。它的类型是”描述”,不是”定义”。


二、为什么形式化方法需要Haskell?

1. 定理证明的表达能力

在Haskell中,我可以写:

haskell

复制

1
2
3
4
-- 这不仅是类型声明,更是**待证明的命题**
Theorem coord_consistency :: Option -> Bool
coord_consistency opt =
length (grid opt) > 0 ==> length (xAxis opt) == length (yAxis opt)

这个定理可以被Coq/Isabelle等证明助手验证。而TypeScript的类型无法上升到这个抽象层次。

2. λ演算的直接对应

Haskell表达式与数学λ演算几乎一一对应:

haskell

复制

1
2
-- 数学: λd. (x(d), y(d))
encode = \d -> (x d, y d)

TypeScript则需要引入运行时语义:

TypeScript

复制

1
2
// 这是"函数",不是纯粹的λ项
const encode = (d: Data) => ({ x: d.x, y: d.y });

3. 心智模型的纯净性

学习ECharts的本质时,Haskell能剥离所有副作用,只保留纯粹的映射关系

haskell

复制

1
2
3
4
5
-- 纯粹的、无副作用的数学映射
data -> visual = encode(data)

-- 渲染是另一个独立过程
render(visual) = dom

TypeScript的代码则混杂了类型、值、副作用

TypeScript

复制

1
2
3
4
5
// 类型、数据、副作用混在一起
const option = { // 这是值
series: data.map(d => encode(d)) // 运行时计算
}
chart.setOption(option); // 副作用:渲染

三、实战时如何切换到TypeScript?

**Haskell是”思维草稿纸”,TypeScript是”实现蓝图”**。

思维过程(Haskell):

haskell

复制

1
2
3
4
5
6
-- 第一步:在脑中用Haskell推导
myEncode :: StockData -> (Time, Price, Size)
myEncode d = (date d, price d, sqrt (volume d))

-- 第二步:验证类型正确性
-- :t myEncode -- 编译通过,证明逻辑正确

实现过程(TypeScript):

TypeScript

复制

1
2
3
4
5
6
// 第三步:将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

复制

1
2
3
4
5
6
7
// 看起来熟悉,但丢失了关键信息
type Option = {
title?: Title;
legend?: Legend;
grid?: Grid[];
// ... 无穷无尽的嵌套
}

问题

  1. 噪音太多?表示可选,但数学上”可选”是什么?(Haskell用Maybe显式表达)

  2. 无法抽象:无法表达”所有Series都是某种态射”这个通用模式

  3. 依赖记忆:你只能看到”这个字段有什么”,看不到”这个配置能推导什么”

你会陷入API文档沼泽,而非第一性原理的清晰结构


五、核心结论:思维的分层

复制

1
2
3
4
5
6
7
8
9
10
11
抽象层次

| Haskell层:形式化规约、定理证明、λ演算
| (思考"可视化是什么")

| TypeScript层:具体实现、类型检查、工程实践
| (思考"如何用ECharts实现")

| JavaScript层:运行时、副作用、DOM操作
| (思考"代码怎么跑起来")

**Haskell不是为了”运行”,而是为了”理解”**。

就像学物理时,我们先用数学公式(F=ma)理解原理,再用计算机模拟实现计算。前者是思维工具,后者是工程工具。


六、给你的实践建议

学习时

  1. 用Haskell/伪代码在纸上推导数据→视觉的映射函数

  2. 画出BNF文法树,理解配置的递归结构

  3. 在脑中用类型签名验证正确性

编码时

  1. 将推导出的λ表达式翻译成TypeScript函数

  2. 代数数据类型翻译成接口和联合类型

  3. 形式化规约写成单元测试

验证时

TypeScript

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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,而是用最适合抽象思维的语言来思考,用最适合工程实现的语言来编码。两者不是对立,而是思维-实现的完整闭环。