余代数和自动机

Lobsters Hottest 论文

摘要

一份介绍性的 literate Haskell 文档,探讨余代数和自动机之间的关系,展示如何利用范畴论中的 fold 和 unfold 操作来建模状态机。

<p><a href="https://lobste.rs/s/grqzwv/coalgebras_automata">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/30 14:08

# 余代数与自动机 来源:https://web.archive.org/web/20071014215938/http://homepage.mac.com/sigfpe/Computing/fold.html Wayback Machine \- https://web\.archive\.org/web/20071014215938/http://homepage\.mac\.com:80/sigfpe/Computing/fold\.html `` 余代数与自动机 ----------------------- 本文档是文学化Haskell,这意味着它混合了实际可运行的代码和注释。 (此网页曾用错误标题。它与数据类型的微分无关,且不适用于非函数式程序员。) (另外,我后来了解到,下面所有fold和unfold的定义可以合并为一对定义。参见,例如:http://web.comlab.ox.ac.uk/oucl/work/jeremy.gibbons/publications/hodgp.pdf) 本文档的非HTML化版本可在以下地址获取: http://www.sigfpe.com/Computing/fold.lhs 你可以在hugs或ghc中运行它。 引言 ------------ 许多年前,我尝试为有限状态自动机编写代码。我想写一个函数,它接受状态标签和输入,然后返回一个新的状态标签。但后来我疑惑状态标签的意义何在。难道我不能直接写一个函数,它接受某些输入,然后返回另一个相同类型的函数吗?换句话说,我想要一个类型X,使得X = A -> X。总之,我当时觉得这也许有点反常,便忽略了这个问题多年。但后来我尝试玩转fold和unfold的推广,惊讶地发现这个类型以一种非常自然的方式重新出现。更有趣的是,将使用状态标签的状态机转换为不引用标签的函数,实际上是unfold操作的一个特例。 F-代数 ---------- 从头开始... 我正在看Pierce的《计算机科学家基础范畴论》中问题2.2.4.2。我构造了所需的逆,但抽象层次让我有点困惑,无法证明它是逆。于是我作弊,查看了Wadler的论文: http://homepages.inf.ed.ac.uk/wadler/papers/free-rectypes/free-rectypes.txt 然后我决定,应该实际为一些例子推导细节,并对发现感到惊讶。 给定范畴C中的一个函子F,一个F-代数是一个箭头 fin : F(X) -> X,其中X为某对象。我们也将一个F-代数写作(X,fin),以明确其对底层范畴对象的依赖。 我们可以将F-代数画为: F(X) | | fin V X 在Haskell中我们可以这样定义: class (Functor f) => Algebra f x where fin :: (f x) -> x (我们使用多参数类,因此在ghc中用-fglasgow-exts,在hugs中用-98运行。) F-代数构成一个范畴,其中的箭头是C中的箭头h : X -> X,使得以下图表交换: F(h) F(X) --> F(Y) | | | | V h V X --> Y 考虑一个简单例子,F = Maybe。记住Maybe函子定义为Maybe x = Nothing | Just x。类型Maybe x的对象要么是特定对象'Nothing',要么是一个x。我们可以将其写作F(X) = 1+X,其中+是不交并,1是终对象(例如单元素集合)。因此一个F-代数是一个函数fin : Maybe X -> X。 所以当我们对F(X)的一个元素应用fin时,需要考虑两种情况:它作用于Nothing的可能性,以及作用于Just x的可能性。对于前者,fin(Nothing)就是X中的一个元素,记为g。对于后者,它本质上是一个函数X -> X,记为h。因此,选择一个函数Maybe X -> X等同于选择X中的一个元素和一个函数X -> X。 让我们在自然数上定义这个例子: instance Algebra Maybe Integer where fin Nothing = 0 fin (Just a) = a+1 (Haskell没有提供自然数类,所以我们用Integer伪造。) 注意fin是如何按照上述两种情况编写的。 现在考虑另一个Maybe-代数(Y,f),其中f(Nothing) = g,f(Just x) = h(x)。 定义p : Integer -> Y为p(n) = h^n(g)。(h^n表示h应用n次。)显然以下图表交换: 1+N --> 1+Y | | |fin |f V V N --> Y (记住,函数f: X -> Y提升为函数f' : 1+X -> 1+Y,其中f(Nothing) = Nothing,f(Just x) = Just f(x)。参见Prelude中Maybe的fmap定义。) 让我们创建另一个Maybe-代数: instance Algebra Maybe Float where fin Nothing = 1.0 fin (Just x) = 2.0*x 在这种情况下,我们有 p :: Integer -> Float p n = 1.0*2^n 我们声称p是一个Maybe-代数箭头,因此我们应该比较p . fin和fin . (fmap p)在一些值上的结果: ex0 = map (fin . (fmap p)) [Nothing,Just 0,Just 1,Just 16] ex1 = map (p . fin) [Nothing,Just 0,Just 1,Just 16] 你应该看到ex0等于ex1。 因此我们展示了一个从F-代数(N,fin)到(Y,f)的F-代数箭头。更重要的是,从(N,s)到(Y,f)只可能存在唯一的F-代数箭头。由于我们的构造是完全通用的,它适用于任何(Y,f)。这意味着对于每个F-代数,存在唯一的F-代数箭头从(N,fin)指向它。换句话说,(N,fin)是F-代数范畴中的初始对象。假设对于某个F,我们有一个初始F-代数(X,fin)。那么对于任何F-代数(Y,f),使得以下图表交换的唯一映射h显然是f的函数。 F(h) F(X) --> F(Y) | | fin| | f V h V X --> X h = fold f 这个函数被称为'fold'。我们现在可以定义: class (Algebra f x) => Initial f x | x -> f where fold :: (f y -> y) -> (x -> y) 我们有一个箭头fin : F(N) -> N。由于F是函子,我们也有箭头F(fin) : F(F(N)) -> F(N)。这也是一个F-代数。现在考虑fold(F(fin))。这必须映射N -> F(N)。不难证明这是fin的逆。实际上它减1,但如果试图从0减去1,我们得到Nothing。可以证明,对于初始代数,一般情况下fold(F(fin))是fin的逆。即fold(F(fin))是fout的逆,这本质上是"Lambek引理"。我们现在可以通过在Haskell中编写所有内容来完成Initial的定义: fout :: x -> (f x) fout = fold (fmap fin) fout是fin的逆。Haskell使用fold自动为我们构造fout。 instance Initial Maybe Integer where fold f 0 = f Nothing fold f (n+1) = f $ Just $ fold f n 我们现在可以通过将其应用于整数来证明fin是fout的逆: --ex2 = fin $ fout $ (4::Integer) ex2 = map (fin . fout) [0,1,16 :: Integer] ex3 = map (fout . fin) [Nothing,Just 0,Just 1,Just 16 :: Maybe Integer] F-余代数 ------------ 我们现在可以开始对偶化我们的构造,看看会出现什么新对象。 我们首先定义F-余代数: class (Functor f) => Coalgebra f x where outf :: x -> f x 这次一个F-余代数是一个箭头f : X -> F(X),当需要显示对X的依赖时,我们将余代数写作(X,f)。F-余代数之间的箭头以明显的方式定义。我们现在考虑的不是F-余代数范畴中的初始对象,而是终对象。这次存在一个从任何F-余代数到终F-余代数的唯一箭头。如果F-余代数是(Y,f),则从(Y,f)到终F-余代数的唯一映射由一个称为'unfold'的函数指定。以下是终F-余代数的完整签名。 class (Coalgebra f x) => Final f x | x -> f where unfold :: (y -> f y) -> (y -> x) 同样我们可以构造outf的逆,这次使用unfold: inf :: (f x) -> x inf = unfold (fmap outf) 是时候举个例子了。定义自然数上的以下函数。 collatz :: Integer -> Maybe Integer collatz 1 = Nothing collatz n | even n = Just $ n `div` 2 | odd n = Just $ 3*n+1 这定义了一个Maybe-余代数 instance Coalgebra Maybe Integer where outf n = collatz n 著名的Collatz猜想指出,如果我们对任何自然数反复应用collatz,最终会到达1。假设Collatz猜想为真。那么我们可以问,对一个数应用多少次collatz才能到达1?定义: count f x = case f x of Nothing -> 0 Just x -> 1+count f x 这也应该给出为什么我们使用Maybe Natural而不是仅仅Natural的线索。我们想要计算在到达选定值之前需要迭代函数的次数,而不仅仅是零。我们可以通过让函数将选定值映射到Nothing来实现这一点。因此方便的是,函数f既描述了迭代,又指明了迭代结束的位置。 然而有一个陷阱。如果Collatz猜想为假,那么c collatz x可能不会终止。因此答案可能是'无限'。所以严格来说,c应该映射到由无穷大扩展的自然数: data Count = Infinity | Natural Integer deriving (Show) plusOne (Natural n) = Natural (1+n) plusOne Infinity = Infinity Count类型是一个Maybe-余代数: instance Coalgebra Maybe Count where outf (Natural 0) = Nothing outf (Natural (x+1)) = Just $ Natural x outf Infinity = Just Infinity 因此,如果我们调整count f使其类型为Count而不是integer,那么它给出一个Maybe-余代数箭头。它是到Count的唯一这样的箭头。我们没有使用任何关于collatz函数的特殊性质,因此我们构造了unfold。我们现在可以实现Count作为终Maybe-余代数: instance Final Maybe Count where unfold f x = case f x of Nothing -> Natural 0 (Just x) -> plusOne $ unfold f x 让我们展示inf . outf是恒等映射: ex4 = map (inf . outf) [Natural 0,Natural 1,Natural 17,Infinity] 你可能会注意到一个陷阱:它在列表的最后一个元素上失败。如果函数重复迭代从未到达Nothing,unfold返回Infinity。但我们没有办法仅仅通过迭代来知道这一点。所以,从数学上讲,inf是outf的逆。但我们并不总能计算它。 列表与树 --------------- 到了这个点,你肯定在想为什么这些函数被称为fold和unfold。 好吧,让我们定义一个函子F: data F a = One | Pair Integer a deriving (Eq,Show) F a 本质上是 Maybe (Integer,a) 我们需要一个函数将数据从F中提取出来: F是一个函子,因为我们可以定义: instance Functor F where fmap f One = One fmap f (Pair a b) = Pair a (f b) 这满足通常的函子规则。 不难看出,([Integer],fin)(其中fin定义如下)实际上是一个F-代数: instance Algebra F [Integer] where fin One = [] fin (Pair a b) = a:b (一个小前提:我们实际上是在讨论有限列表。) fin将一个整数和一个整数列表粘合在一起形成另一个列表。它还提供了一种创建空列表的方法。因此,和之前一样,我们使用fin编码一对元素:一个特指的元素[]和函数(:)。 假设我们有一个X中的元素g,以及一个函数f: X -> Integer -> X。那么我们可以使用标准Prelude函数foldr将任何整数列表映射到X。我们得到foldr f g : [Integer] -> X。事实上,如果我们把像[1,2,3]这样的列表视为函数的组合:(:) 1 ((:) 2 ((:) 3 [])),那么fold f g [1,2,3]就是用f替换(:),用g替换[]。现在应该容易看出([Integer],fin)是一个初始F-代数,并且fold和foldr本质上是相同的: instance Initial F [Integer] where -- 将f解包成适合foldr的形式 fold f = foldr (cons f) (f One) where cons f a b = f $ Pair a b 像往常一样,我们可以检查同构: ex5 = fin $ fout [1::Integer,2,3,4,5] 许多论文讨论unfold。我们在这里称它为unfoldr,因为它与foldr自然配对。 unfoldr p f g x = if p x then [] else f x : unfoldr p f g (g x) g是机器的把手。每次我们转动把手,g将一个对象映射到一个新对象,并通过将f应用于这个对象来收集输出。我们将所有输出收集到一个列表中,直到函数p告诉我们停止。例如 ex6 = unfoldr (>20) id (2*) 1 这返回不超过20的2的幂的列表。我们转动(2*)把手直到条件满足。 unfoldr本质上是列表的unfold操作(除了这次我们*允许*无限列表——例如如果你有p _ = False)。 instance Coalgebra F [Integer] where outf [] = One outf (a:b) = Pair a b instance Final F [Integer] where -- 将f解包成适合unfoldr的形式 unfold f = unfoldr (isOne . f) (fst . pair . f) (snd . pair . f) where isOne One = True isOne _ = False ex7 = inf $ outf [1::Integer,2,3,4,5] 我们现在可以对树而不是列表进行类似的分析。树是函子G的G-代数: data G a = One' | Triple Integer a a deriving (Show) instance Functor G where fmap f One' = One' fmap f (Triple a b b') = Triple a (f b) (f b') data Tree a = Empty | Tree a (Tree a) (Tree a) deriving (Show) instance Algebra G (Tree Integer) where fin One' = Empty fin (Triple a b b') = Tree a b b' instance Initial G (Tree Integer) where fold f Empty = f One' fold f (Tree a b b') = f (Triple a (fold f b) (fold f b')) 正如fold可以通过在连续元素之间应用一个函数来折叠列表,fold以完全相同的方式折叠树。 这里是我们通常的例子,展示同构的作用: ex8 = fin $ fout $ Tree (6::Integer) (Tree 2 Empty Empty) (Tree 7 Empty (Tree 0 Empty Empty)) 现在我们进入有趣的例子。 自动机 -------- 考虑函子H data H x = H (Char -> Maybe x) instance Functor H where fmap f (H g) = H (fmap f . g) 为方便起见,我们定义unH: 一个H-余代数是一个函数X -> H(X)。本质上它是一个函数X -> Char -> Maybe X。暂时忽略Maybe。我们有一个函数f : X -> Char -> X。这本质上是一个自动机,其中X标记状态,f定义转移。这是你用来识别字符串的正则表达式那种。但我们还需要最后一件事:一种标记字符串已被识别的方法。这就是为什么我们使用Maybe。Nothing值(而不是一个状态)表示字符串已被匹配。(这被称为部分自动机,因为它在此处停止。) 这是一个例子: f :: Integer -> Char -> Maybe Integer f 0 'c' = Just 1 f 0 _ = Just 0 f 1 'a' = Just 2 f 1 _ = Just 0 f 2 't' = Nothing f 2 _ = Just 0 这个函数可用于识别字符串"cat"出现在另一个字符串中作为子串时。事实上,我们可以定义 runMachine f state "" = False runMachine f state (c:cs) = case f state c of Nothing -> True (Just state') -> runMachine f state' cs ex9 = map (runMachine f 0) ["cat","dog","wildcats"] 现在Integer标签被安全地锁在(runMachine f 0)内部,但我们为此付出的代价是不能再引用状态。我们成功地隐藏了标签,却完全隐藏了状态。我们想要的是在不费心于标签的情况下操纵状态。 因此现在我们构造H-余代数。我称之为超树(Hypertree),因为它与上面的树例子有一些相似之处。从某种意义上说,超树是一棵树,对X的每个元素都有一个子节点(尽管使用Maybe意味着子节点"可能"是一个死胡同,没有进一步子节点)。 data Hypertree x = HTree (x -> Maybe (Hypertree x)) 注意我不是凭空想出那个定义的。我显式地定义了一些看起来应该给出H(X) = X的解的东西。 instance Coalgebra H (Hypertree Char) where outf (HTree f) = H f instance Final H (Hypertree Char) where

相似文章

Haskell中的Profunctor装备

Hacker News Top

这篇博客文章提供了一个用Haskell实现的Profunctor装备的玩具实现,包括自然变换和组合,旨在让范畴论概念对程序员来说更易于理解。

逻辑程序的抽象机

Lobsters Hottest

本文探讨了使用抽象栈机器实现逻辑程序的方法,详细说明了推理规则(如加法)的不同模式分配如何转换为状态机转换以进行计算。

使用Rust和范畴论构建机器学习框架

Hacker News Top

这篇文章宣布了一份工作草稿书籍《Category Theory for Tiny ML in Rust》以及一个公开工作坊,介绍一个使用Rust和范畴论的微型机器学习流水线,旨在通过类型化转换使机器学习结构变得明确。

Data types à la carte (2008)

Lobsters Hottest

本文提出了一种从独立组件组合数据类型和函数的技术,并将该方法扩展到结合自由单子,从而实现了对Haskell的IO单子的模块化结构。

国际象棋不变量

Hacker News Top

本文将国际象棋建模为并发系统,并推导出状态和转换不变量,展示了使用TLA+的形式化验证技术。