十分钟魔法练习TS版学习笔记(1)
代数数据类型 (Algebraic Data Type) 、 广义代数数据类型 (Generalized Algebriac Data Type) 、余代数数据类型 (Coalgebraic Data Type)
https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/ADT.md
https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/GADT.md
https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/CoData.md
入门三篇篇,感觉还算能理解,就是理解起来有点烧脑(应该是我太菜了,嗯没错),不过多解释了
单位半群 (Monoid)
https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/Monoid.md
到这开始有点难理解了,这里我掏出我的学习小助手DS R1问一下什么是半群(Semigroup):
好的!咱们用大白话来解释「半群」这个听起来挺唬人的数学概念:
你可以把它想象成一个 「遵守特定组合规则的流水线」:
你有一个「车间」:这个车间就是集合
A
。里面装着你所有的「原材料」(元素),比如零件、数字、字母等等。车间里有一台固定的「组合机器」:这台机器就叫做
<>
(你可以把它想象成任何你喜欢的机器名,比如「搅拌机」、「黏贴器」、「连接器」)。- 这台机器怎么用? 每次只能放进 两件 车间里的原材料 (比如
a
和b
)。 - 它吐出来什么? 一定会吐出来另外一件仍然属于你车间的原材料(比如
a <> b
)。这就是 封闭性:机器不会做出跑到车间外面的东西。
- 这台机器怎么用? 每次只能放进 两件 车间里的原材料 (比如
这台机器有个重要特点:「顺序无所谓,效果一样」:这就是 结合律。想象一下:
- 你 先 把原料
a
和b
扔进机器,得到一个中间产品(a <> b)
。 - 然后你 再 把这个中间产品
(a <> b)
和下一个原料c
一起扔进机器,得到最终产品(a <> b) <> c
。 - 或者,你也可以换种顺序:先把
b
和c
扔进机器,得到一个不同的中间产品(b <> c)
。 - 然后你 再 把原料
a
和这个中间产品(b <> c)
一起扔进机器,得到最终产品a <> (b <> c)
。 - 关键在于:这台机器设计的非常好,不管你先组合哪两个(只要原材料的顺序
a b c
不变),最终产品竟然是完全一样的! 也就是(a <> b) <> c === a <> (b <> c)
。
- 你 先 把原料
所以,半群就是这样一个东西:
- 一个装满东西的盒子(集合
A
)。 - 一个专门加工这个盒子里任意两样东西的「两两组合器」(运算
<>
),组合完的结果也必须在这个盒子里(封闭性)。 - 这个组合器有个神奇特性:组合顺序可以随意搭配,只要原材料的大排队顺序不变(从左到右还是
a, b, c
),最终组合结果保证一样(结合律)。
几个通俗例子让你感受一下:
整数加法车间:
- 车间
A
: 所有整数 (… -2, -1, 0, 1, 2, …) - 组合器
<>
: 加号+
- 两两组合规则:
a + b
- 封闭性: 两个整数加一起肯定还是整数(还在车间里)。
- 结合律:
(1 + 2) + 3 = 3 + 3 = 6
,1 + (2 + 3) = 1 + 5 = 6
。结果一样!
- 车间
非空字符串连接车间:
- 车间
A
: 所有由字母组成的非空字符串 (“a”, “hello”, “cat”, …) - 组合器
<>
: 字符串连接.
(比如 Python/Javascript里的+
用于字符串) - 两两组合规则:
"hello" <> "world" = "helloworld"
- 封闭性: 连接两个非空字符串,肯定得到另一个非空字符串(还在车间里)。
- 结合律:
("hello" <> " ") <> "world" = "hello " <> "world" = "hello world"
,"hello" <> (" " <> "world") = "hello" <> " world" = "hello world"
。结果一样!
- 车间
什么情况下不是半群?
- 减法车间(整数): 满足结合律吗?
(10 - 5) - 2 = 5 - 2 = 3
,10 - (5 - 2) = 10 - 3 = 7
。3 不等于 7!所以不满足结合律。 - 整数除法车间: 既不封闭(1除以2等于0.5,不是整数!),也不满足结合律。
总结:
半群就是一个集合(一群东西)配上一个规矩(一个只能操作两样东西的组合器)。这个规矩有两个核心要求:1) 组合结果不能跑出这个集合(不能跑偏);2) 组合的操作顺序可以灵活安排,只要原材料的先后次序不变,最终结果保证不变(组合顺序影响不了大局)。
记住「流水线」和「顺序无所谓,效果一样」这两个关键点,半群就没那么神秘了!它就是数学里用来描述一些具有稳定组合行为的系统的最基本结构。
嗯经过ai这么一说就很通俗易懂了,谢谢你,DS。
下一段是讲单位半群,说的也不是很清楚,那么更通俗的讲解一下单位半群的事情就拜托你了DS!
好的,我们把单位半群(Monoid)拆解得更通俗一点,继续用之前的比喻:
回忆一下半群:
想象一个车间(集合 A
) 和一台只能同时加工两件原料的组合机器(运算 <>
)。这台机器有两个规矩:
- 封闭性:扔进去两件车间的原料,吐出来的成品一定还在车间里。
- 结合律:加工顺序可以随意组合(先做
a
和b
,再和c
做;或者先做b
和c
,再和a
做),只要原料的排队顺序(a
然后b
然后c
)不变,最终成品保证一模一样。
现在,单位半群(Monoid)在半群的基础上,增加了一个超级神奇的东西:一个「隐形原料」或者说「万能催化剂」——单位元(Identity Element)
这个「单位元」有什么神奇之处?
想象一下,在车间 A
里,存在一件特殊的原料,我们叫它 e
(或者 identity
)。它神奇的地方在于:
- 当你把
e
和车间里的任何其他原料
x
一起扔进组合机器<>
时,无论e
是在左边还是右边,机器吐出来的成品,都完全等于
那个x
本身!
用公式表示就是:
x <> e = x
(x
和e
组合,结果还是x
)e <> x = x
(e
和x
组合,结果还是x
)
这个 e
就是单位元!
为什么叫「单位元」?
因为它就像数学乘法里的 1
,或者加法里的 0
。任何数乘以 1
还是它本身 (x * 1 = x
),任何数加上 0
也还是它本身 (x + 0 = x
)。它不改变其他元素。
所以,单位半群(Monoid)就是:
- 一个半群(车间
A
+ 组合机器<>
,满足封闭性和结合律)。 - 外加一个「隐形原料」/「万能催化剂」——单位元
e
。这个e
在组合操作中,对任何元素都像「透明」的一样,不会改变对方。
通俗比喻升级:
想象你在车间里做拼积木(组合机器 <>
就是拼积木的动作):
半群: 你有各种积木块(集合
A
),你可以把任意两块积木拼在一起(a <> b
),拼完的结果还是一块更大的积木(封闭性)。而且,拼积木的顺序可以灵活安排(结合律),比如:- 先拼
a
和b
得到ab
,再把ab
和c
拼成abc
。 - 或者先拼
b
和c
得到bc
,再把a
和bc
拼成abc
。 - 最终
abc
是一样的。
- 先拼
单位半群: 除了各种积木块,车间里还有一个超级特殊的「隐形积木」
e
。这个隐形积木的神奇之处在于:- 如果你把
e
拼在任何积木块
x
的 左边 (e <> x
),x
看起来就像完全没被拼过一样,还是x
。 - 如果你把
e
拼在任何积木块
x
的 右边 (x <> e
),x
看起来也像完全没被拼过一样,还是x
。 - 这个
e
就像空气,或者一个完全透明的、没有任何体积的积木块。拼它等于没拼!
- 如果你把
你举的例子非常棒:
{加法+, 自然数集} 是单位半群:
- 车间: 所有自然数
0, 1, 2, 3, ...
- 组合机器:
+
(加法) - 单位元
e
:0
- 为什么?
x + 0 = x
且0 + x = x
。加0
等于没加!
- 车间: 所有自然数
{乘法, 自然数集} 是单位半群:*
- 车间: 所有自然数
1, 2, 3, ...
(注意,自然数集是否包含0
有争议,如果包含0
,0 * x = 0
而不是x
,所以单位元是1
,但0
会破坏单位元性质。通常这里指正整数集或包含1
的集合)。 - 组合机器:
*
(乘法) - 单位元
e
:1
- 为什么?
x * 1 = x
且1 * x = x
。乘1
等于没乘!
- 车间: 所有自然数
{连接+, 字符串集} 是单位半群:
- 车间: 所有字符串 (
"a"
,"hello"
,"cat"
,""
, …) (通常包含空串) - 组合机器:
+
或concat
(字符串连接,例如"a" + "b" = "ab"
) - 单位元
e
:""
(空字符串) - 为什么?
"hello" + "" = "hello"
且"" + "hello" = "hello"
。连接一个空字符串等于没连接!
- 车间: 所有字符串 (
TypeScript 代码解释:
你提供的 TypeScript 接口 MonoidAcc<T>
描述了一个对象,它封装了一个 Monoid 的值和操作:
value: T;
: 这就是当前这个对象所代表的值,属于集合A
(类型T
)。concat: (v: T) => MonoidAcc<T>
: 这就是组合操作<>
。它接受另一个属于集合A
的值v
(类型T
),将当前对象的value
和传入的v
进行组合(执行this.value <> v
),然后返回一个新的MonoidAcc<T>
对象,其value
就是组合的结果。
要成为一个真正的 Monoid,这个 concat
操作必须:
满足结合律:
(a.concat(b)).concat(c)
必须等价于a.concat(b.concat(c))
(这里a
,b
,c
是MonoidAcc<T>
实例,.concat
返回新实例,.value
是组合结果)。存在单位元: 必须存在一个特殊的
MonoidAcc<T>
实例,通常叫做empty
或identity
,它的value
就是单位元e
。对于这个empty
实例和任何其他实例x
:empty.concat(x).value === x.value
x.concat(empty).value === x.value
总结:
单位半群(Monoid)就是一个自带「隐形元素」的半群。这个隐形元素(单位元)在进行组合操作时,就像不存在一样,不会改变和它组合的那个元素。常见的例子有:数字加法里的 0
,数字乘法里的 1
,字符串连接里的空串 ""
,列表连接里的空列表 []
。它在数学和编程(尤其是函数式编程)中非常重要,因为它提供了一种安全、可预测的“组合”基础。
这下就通俗易懂了,比如举例的Optional
1 | class SumAcc implements MonoidAcc<number> { |
是不是和 ?.
或者 ??
||
很像
高阶类型
https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/HKT.md
文中给的是案例代码好像是java的,这里用ai写一段真正ts代码(还得是Gemini2.5pro,ds-r1写的代码一直有问题)
1 | // --- 第1步 (核心修复):创建一个“类型注册表”接口 --- |
让ai更通俗易懂的介绍一下HKT:
HKT 的核心用途
抽象类型操作
为不同容器类型(Array
,Option
,Either
)统一实现:map
(映射)flatMap
(链式操作)ap
(应用函子)
类型安全泛型编程
写出适用于任意类型构造器的泛型函数:1
2
3function doubleValues<F>(F: Functor<F>, values: Type<F & HKT<F, number>>) {
return F.map(values, x => x * 2);
}函数式模式
实现函数式库中的关键抽象:- Functor(
map
) - Monad(
flatMap
) - Applicative(
ap
)
- Functor(
实际应用场景
函数式库(如 fp-ts)
fp-ts 使用 HKT 模拟实现完整的函数式编程工具:1
2
3
4
5
6import * as O from 'fp-ts/Option';
import * as A from 'fp-ts/Array';
// 统一操作 Option 和 Array
O.map(Some(3), x => x * 2); // Some(6)
A.map([1, 2], x => x * 2); // [2, 4]领域特定抽象
例如为 API 响应统一处理容器类型:1
2
3
4
5
6
7
8
9
10
11
12
13
14type ApiResponse<T> = {
data: T;
error: string | null
};
// 定义 ApiResponse 的 HKT
interface ApiResponseHKT extends HKT<'ApiResponse', any> {
type: ApiResponse<this['_A']>;
}
// 实现 Functor
const apiFunctor: Functor<'ApiResponse'> = {
map: (fa, f) => ({ ...fa, data: f(fa.data) }),
};
这里还有个htk的实现:https://github.com/Snowflyt/hkt-core
就不过多介绍了,不然感觉又可以独立写一篇文章了。。。