F 代数和 F 代数是有助于推理归纳类型(或递归类型)的数学结构。
F-代数
我们首先从 F 代数开始。我会尽量简单。
我想你知道什么是递归类型。例如,这是一个整数列表类型:
data IntList = Nil | Cons (Int, IntList)
很明显它是递归的——事实上,它的定义是指它自己。它的定义由两个数据构造函数组成,具有以下类型:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
请注意,我将Nil 的类型写为() -> IntList,而不仅仅是IntList。从理论上讲,这些实际上是等价的类型,因为() 类型只有一个居民。
如果我们以更集合论的方式编写这些函数的签名,我们将得到
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
其中1 是一个单元集(包含一个元素),A × B 运算是两个集合A 和B 的叉积(即,一对集合(a, b) 其中a遍历A 的所有元素,b 遍历B 的所有元素。
两个集合A 和B 的不相交并集是集合A | B,它是集合{(a, 1) : a in A} 和{(b, 2) : b in B} 的并集。本质上它是来自A 和B 的所有元素的集合,但是每个元素都被“标记”为属于A 或B,所以当我们从A | B 中选择任何元素时,我们将立即知道该元素是来自A 还是来自B。
我们可以“加入”Nil 和 Cons 函数,因此它们将形成一个处理集合 1 | (Int × IntList) 的单个函数:
Nil|Cons :: 1 | (Int × IntList) -> IntList
确实,如果Nil|Cons 函数应用于() 值(显然属于1 | (Int × IntList) 集合),那么它的行为就好像它是Nil;如果Nil|Cons 应用于(Int, IntList) 类型的任何值(这些值也在集合1 | (Int × IntList) 中,它的行为类似于Cons。
现在考虑另一种数据类型:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
它有以下构造函数:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
也可以合并成一个函数:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
可以看出,这两个joined 函数的类型相似:它们看起来都像
f :: F T -> T
其中F 是一种转换,它接受我们的类型并给出更复杂的类型,它由x 和| 操作、T 的用法和可能的其他类型组成。例如,IntList 和 IntTree F 如下所示:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
我们可以立即注意到任何代数类型都可以用这种方式编写。事实上,这就是为什么它们被称为“代数”的原因:它们由许多其他类型的“和”(并)和“积”(叉积)组成。
现在我们可以定义 F 代数了。 F-代数 只是一对(T, f),其中T 是某种类型,f 是类型f :: F T -> T 的函数。在我们的示例中,F 代数是 (IntList, Nil|Cons) 和 (IntTree, Leaf|Branch)。但是请注意,尽管 f 函数的类型对于每个 F 都是相同的,但 T 和 f 本身可以是任意的。例如,(String, g :: 1 | (Int x String) -> String) 或 (Double, h :: Int | (Double, Double) -> Double) 对于某些 g 和 h 也是对应 F 的 F 代数。
之后我们可以引入F-代数同态和初始F-代数,它们具有非常有用的性质。事实上,(IntList, Nil|Cons) 是一个初始 F1 代数,(IntTree, Leaf|Branch) 是一个初始 F2 代数。我不会给出这些术语和属性的确切定义,因为它们比需要的更复杂和抽象。
尽管如此,例如(IntList, Nil|Cons) 是 F 代数这一事实允许我们在此类型上定义类似 fold 的函数。如您所知,折叠是一种将某些递归数据类型转换为一个有限值的操作。例如,我们可以将整数列表折叠成单个值,该值是列表中所有元素的总和:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
可以在任何递归数据类型上推广此类操作。
以下是foldr函数的签名:
foldr :: ((a -> b -> b), b) -> [a] -> b
请注意,我使用大括号将前两个参数与最后一个参数分开。这不是真正的foldr 函数,但它是同构的(也就是说,您可以轻松地从另一个中获取一个,反之亦然)。部分应用的foldr 将具有以下签名:
foldr ((+), 0) :: [Int] -> Int
我们可以看到这是一个接受整数列表并返回单个整数的函数。让我们根据 IntList 类型来定义这样的函数。
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
我们看到这个函数由两部分组成:第一部分定义了这个函数在Nil 部分IntList 上的行为,第二部分定义了函数在Cons 部分上的行为。
现在假设我们不是在使用 Haskell 编程,而是使用某种允许在类型签名中直接使用代数类型的语言(嗯,从技术上讲,Haskell 允许通过元组和 Either a b 数据类型使用代数类型,但这会导致不必要的冗长)。考虑一个函数:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
可以看出reductor是F1 Int -> Int类型的函数,就像F-代数的定义一样!事实上,(Int, reductor) 是一个 F1 代数。
因为IntList 是一个初始的 F1 代数,对于每个类型 T 和每个函数 r :: F1 T -> T 都存在一个函数,称为 catamorphism 对于 r,它转换 @987654411 @ to T,这样的功能是独一无二的。实际上,在我们的示例中,reductor 的变质是sumFold。请注意reductor 和sumFold 的相似之处:它们的结构几乎相同! reductor定义中s参数用法(类型对应T)对应sumFold xs定义中sumFold xs计算结果的用法。
只是为了更清楚并帮助您了解模式,这里是另一个示例,我们再次从生成的折叠函数开始。考虑append 函数,它将其第一个参数附加到第二个参数:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
这是它在我们的IntList 上的样子:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
再次,让我们尝试写出减速器:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold 是appendReductor 的变态,它将IntList 转换为IntList。
因此,从本质上讲,F 代数允许我们在递归数据结构上定义“折叠”,即将我们的结构减少到某个值的操作。
F-代数
F 代数是 F 代数的所谓“对偶”项。它们允许我们为递归数据类型定义unfolds,即一种从某个值构造递归结构的方法。
假设你有以下类型:
data IntStream = Cons (Int, IntStream)
这是一个无限的整数流。它唯一的构造函数有以下类型:
Cons :: (Int, IntStream) -> IntStream
或者,就集合而言
Cons :: Int × IntStream -> IntStream
Haskell 允许您在数据构造函数上进行模式匹配,因此您可以在 IntStreams 上定义以下函数:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
您可以自然地将这些函数“加入”为IntStream -> Int × IntStream 类型的单个函数:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
请注意函数的结果如何与我们的IntStream 类型的代数表示一致。其他递归数据类型也可以做类似的事情。也许你已经注意到了这种模式。我指的是一系列类型的函数
g :: T -> F T
T 是某种类型。从现在开始我们将定义
F1 T = Int × T
现在,F-coalgebra 是一对(T, g),其中T 是一个类型,g 是一个类型g :: T -> F T 的函数。例如,(IntStream, head&tail) 是 F1 代数。同样,就像在 F 代数中一样,g 和 T 可以是任意的,例如,(String, h :: String -> Int x String) 也是某些 h 的 F1 代数。
在所有的 F-余代数中,有所谓的终端 F-余代数,它们与初始 F-代数是对偶的。例如,IntStream 是一个终端 F 代数。这意味着对于每个类型 T 和每个函数 p :: T -> F1 T 都存在一个名为 anamorphism 的函数,它将 T 转换为 IntStream,并且这样的函数是唯一的。
考虑以下函数,它从给定的整数开始生成一个连续整数流:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
现在让我们检查一个函数natsBuilder :: Int -> F1 Int,即natsBuilder :: Int -> Int × Int:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
同样,我们可以看到nats 和natsBuilder 之间存在一些相似之处。这与我们之前观察到的减速器和折叠的连接非常相似。 nats 是 natsBuilder 的变形。
另一个例子,一个函数,它接受一个值和一个函数,并将函数的连续应用流返回到该值:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
它的builder函数如下:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
那么iterate 是iterateBuilder 的变形。
结论
因此,简而言之,F-代数允许定义折叠,即将递归结构简化为单个值的操作,而 F-代数允许做相反的事情:从单个价值。
事实上在Haskell F-代数和F-余代数是重合的。这是一个非常好的属性,这是每种类型中存在“底部”值的结果。因此在 Haskell 中,可以为每种递归类型创建折叠和展开。但是,这背后的理论模型比我上面介绍的更复杂,所以我故意避开它。
希望这会有所帮助。