我在函数式编程和PLT圈子中多次听到过“enggebras”一词,特别是在讨论对象,组件,镜头等时。谷歌搜索这个术语给出了对这些结构进行数学描述的页面,这对我来说几乎是不可理解的。任何人都可以解释一下代数在编程环境中的意义,它们的意义是什么,以及它们与对象和组合的关系如何?
答案 0 :(得分:82)
F-代数和F-余代数是数学结构,有助于推理归纳类型(或递归类型)。
我们首先使用F-algebras开始。我会尽量简单。
我想你知道什么是递归类型。例如,这是整数列表的类型:
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-algebra 只是一对(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)
的{{1}}或g
也是相应F的F代数。
然后我们可以引入 F-代数同态,然后初始F-代数,它们具有非常有用的属性。事实上,h
是一个初始的F1代数,(IntList, Nil|Cons)
是一个初始的F2代数。我不会提供这些术语和属性的确切定义,因为它们比需要的更复杂和抽象。
尽管如此,例如(IntTree, Leaf|Branch)
是F代数的事实允许我们定义(IntList, Nil|Cons)
- 就像这种类型的函数一样。如您所知,fold是一种将一些递归数据类型转换为一个有限值的操作。例如,我们可以将整数列表折叠为单个值,该值是列表中所有元素的总和:
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
的{{1}}部分定义了这个函数的行为,第二部分定义了函数在{{{}}上的行为。 1}}部分。
现在假设我们不是在Haskell中编程,而是在某种语言中允许直接在类型签名中使用代数类型(从技术上讲,Haskell允许通过元组和Nil
数据类型使用代数类型,但这将导致不必要的冗长)。考虑一个函数:
IntList
可以看出Cons
是Either a b
类型的函数,就像F代数的定义一样!实际上,reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
对是一个F1代数。
因为reductor
是初始的F1代数,对于每种类型F1 Int -> Int
和每个函数(Int, reductor)
,都存在一个函数,称为 catamorphism for {{ 1}},将IntList
转换为T
,此功能是唯一的。实际上,在我们的例子中,r :: F1 T -> T
的一个解释是r
。请注意IntList
和T
的相似之处:它们具有几乎相同的结构!在reductor
定义sumFold
参数用法(其类型对应于reductor
)对应于sumFold
定义中reductor
的计算结果的使用。
为了让它更清晰并帮助您看到模式,这是另一个例子,我们再次从生成的折叠函数开始。考虑将s
函数附加到第二个参数的T
函数:
sumFold xs
它在sumFold
上的显示方式:
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
。
因此,基本上,F代数允许我们定义'折叠'在递归数据结构上,即将我们的结构减少到某个值的操作。
F-coalgebras是所谓的“双重”。 F代数项。它们允许我们为递归数据类型定义appendReductor
,即从某个值构造递归结构的方法。
假设您有以下类型:
IntList
这是一个无限的整数流。它唯一的构造函数具有以下类型:
IntList
或者,就集合而言
unfolds
Haskell允许您对数据构造函数进行模式匹配,因此您可以定义以下data IntStream = Cons (Int, IntStream)
的函数:
Cons :: (Int, IntStream) -> IntStream
你可以自然地加入'这些函数转换为Cons :: Int × IntStream -> IntStream
类型的单个函数:
IntStream
注意函数的结果如何与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
现在, F-coalgebra 是一对g :: T -> F T
,其中T
是一种类型,F1 T = Int × T
是(T, g)
类型的函数。例如,T
是F1-coalgebra。同样,就像在F代数中一样,g
和g :: T -> F T
可以是任意的,例如,(IntStream, head&tail)
也是某些h的F1代数。
在所有F-coalgebras中,都有所谓的终端F-coalgebras ,它们与初始F-代数是双重的。例如,g
是终端F-余代数。这意味着对于每个类型T
和每个函数(String, h :: String -> Int x String)
,都存在一个名为 anamorphism 的函数,它将IntStream
转换为T
,并且这种功能是独一无二的。
考虑以下函数,该函数从给定的一个开始生成连续整数流:
p :: T -> F1 T
现在让我们检查一个函数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))
的变形。
因此,简而言之,F-algebras允许定义折叠,即将递归结构减少为单个值的操作,而F-coalgebras允许相反:从单个构造[潜在]无限结构值。
实际上在Haskell F-algebras与F-coalgebras重合。这是一个非常好的财产,这是由于底部存在的结果。每种类型的价值。所以在Haskell中,可以为每个递归类型创建折叠和展开。然而,这背后的理论模型比我上面提到的更复杂,所以我故意避免它。希望这有帮助。
答案 1 :(得分:37)
通过教程论文 A tutorial on (co)algebras and (co)induction 可以让您对计算机科学中的共同代数有所了解。
以下是引用你的引用,
一般而言,某些编程语言中的程序操纵数据。在此期间 在过去的几十年里,计算机科学的发展变得清晰,一个抽象的 这些数据的描述是可取的,例如,以确保一个人的程序不依赖于其运行的数据的特定表示。此外,这种抽象性有助于正确性证明 这种愿望导致在计算机科学中使用代数方法,在称为代数规范或抽象数据类型理论的分支中。研究对象本身就是数据类型,使用了代数中熟悉的技术概念。计算机科学家使用的数据类型通常是从给定的(构造函数)操作集合生成的,正是由于这个原因,代数的“初始性”起着如此重要的作用。 事实证明,标准代数技术可用于捕获计算机科学中使用的数据结构的各种基本方面。但事实证明,很难用代数方式描述计算中发生的一些固有的动态结构。这种结构通常涉及国家概念,可以以各种方式进行转换。这种基于状态的动力系统的形式化方法通常使用自动机或过渡系统,作为经典的早期参考 在过去的十年中,人们逐渐认识到,这种基于状态的系统不应该被描述为代数,而应该被称为代数代数。这些是代数的正式对偶,在本教程中将以精确的方式进行。代数的“初始性”的双重性质,即终结性对于这种共代数来说是至关重要的。而这种最终共代数所需的逻辑推理原则不是归纳,而是共同归纳。
前奏,关于类别理论。 类别理论应该重命名仿函数理论。 因为类别是定义仿函数必须定义的类别。 (此外,函子是人们必须定义的,以便定义自然变换。)
什么是仿函数? 它是从一组到另一组的转换,保留了它们的结构。 (有关详细信息,网上有很多很好的描述。)
什么是F代数? 它是仿函数的代数。 这只是对仿函数普遍适用性的研究。
如何与计算机科学相关联? 程序可以作为一组结构化信息进行查看。 程序的执行对应于该结构化信息集的修改。 执行应保留程序结构听起来不错。 然后可以将执行视为这组信息的仿函数应用程序。 (定义程序的那个)。
为何选择F-co-algebra? 程序是本质上是双重的,因为它们是通过信息来描述的,并且它们依赖于它。 那么主要是组成程序并使其改变的信息可以以两种方式查看。
然后在这个阶段,我想说,
在程序的生命周期中,数据和状态共存,并且它们彼此完成。 他们是双重的。