在编程的上下文中,“代数”是啥意思?

Posted

技术标签:

【中文标题】在编程的上下文中,“代数”是啥意思?【英文标题】:What does "coalgebra" mean in the context of programming?在编程的上下文中,“代数”是什么意思? 【发布时间】:2013-04-07 13:23:30 【问题描述】:

我在函数式编程和 PLT 圈子中多次听到“代数”这个词,尤其是在讨论对象、共单子、透镜等时。谷歌搜索这个术语会给出对这些结构进行数学描述的页面,这对我来说非常难以理解。谁能解释一下余代数在编程环境中的含义,它们的意义是什么,以及它们与对象和共子的关系?

【问题讨论】:

我可以推荐 Jeremy Gibbons 的优秀书籍 Patterns in FP:patternsinfp.wordpress.com 和他相当易懂的论文“计算函数式程序”吗?它们都以非常严格的方式涵盖了余代数(与例如博客文章相比),对于了解一点 Haskell 的人来说,它们也是相当独立的。 【参考方案1】:

阅读教程论文 A tutorial on (co)algebras and (co)induction 应该会让您对计算机科学中的协代数有所了解。

下面是引用它来说服你,

一般来说,某种编程语言中的程序操作数据。在此期间 在过去几十年计算机科学的发展中,很明显,一个抽象的 对这些数据的描述是可取的,例如,以确保一个程序不依赖于它所操作的数据的特定表示。此外,这种抽象性有助于正确性证明。 这种愿望导致在计算机科学中使用代数方法,在一个称为代数规范或抽象数据类型理论的分支中。研究的对象是数据类型本身,使用代数中熟悉的技术概念。计算机科学家使用的数据类型通常是从​​给定的(构造函数)操作集合中生成的,因此代数的“初始性”起着如此重要的作用。 标准代数技术已被证明可用于捕获计算机科学中使用的数据结构的各个基本方面。但事实证明,用代数方式描述计算中出现的一些固有的动态结构是很困难的。这种结构通常涉及状态的概念,它可以通过各种方式进行转换。这种基于状态的动态系统的形式化方法通常使用自动机或转换系统,作为经典的早期参考。 在过去的十年中,这种基于状态的系统不应被描述为代数,而应被描述为所谓的余代数,这一观点逐渐加深。这些是代数的形式对偶,在本教程中将更加精确。代数的“初始性”的双重性质,即最终性,对于这种协代数来说是至关重要的。而这种最终的协代数所需要的逻辑推理原则不是归纳而是协归纳。


序曲,关于范畴论。 范畴论应该是函子的重命名论。 因为类别是定义函子必须定义的。 (此外,为了定义自然变换,必须定义函子。)

什么是函子? 这是从一组到另一组的转换,保留了它们的结构。 (更详细的网上有很多很好的描述)。

什么是 F 代数? 这是函子的代数。 只是对函子的普遍适用性的研究。

它如何与计算机科学联系起来? 程序可以看作是一组结构化的信息。 程序的执行对应于对这组结构化信息的修改。 执行应该保留程序结构听起来不错。 然后可以将执行视为函子在这组信息上的应用。 (定义程序的那个)。

为什么是 F-余代数? 程序本质上是双重的,因为它们是由信息描述的,它们会根据信息采取行动。 那么主要是编写程序和改变它们的信息可以通过两种方式查看。

可以定义为程序正在处理的信息的数据。 可以定义为程序共享的信息的状态。

那么在这个阶段,我想说的是,

F 代数是研究作用于数据宇宙(定义见此处)的函数变换。 F-余代数是研究作用于状态宇宙(定义见此处)的泛函变换。

在程序的生命周期中,数据和状态共存,并且相互补充。 它们是双重的。

【讨论】:

【参考方案2】:

代数

我认为首先要了解代数的概念。这只是对群、环、幺半群等代数结构的概括。大多数时候,这些东西都是以集合的形式介绍的,但由于我们是朋友,我将改为讨论 Haskell 类型。 (不过,我无法抗拒使用一些希腊字母——它们让一切看起来更酷!)

那么,代数只是一个类型τ,具有一些功能和身份。这些函数采用不同数量的τ 类型的参数并生成τ:未加咖喱,它们看起来都像(τ, τ,…, τ) → τ。它们还可以具有“身份”——τ 的元素,在某些函数中具有特殊行为。

最简单的例子是幺半群。一个幺半群是任何类型τ,具有函数mappend ∷ (τ, τ) → τ 和标识mzero ∷ τ。其他示例包括诸如组(除了具有额外的 invert ∷ τ → τ 函数外,它们与幺半群类似)、环、格等。

所有函数都在τ 上运行,但可以有不同的arities。我们可以将这些写成τⁿ → τ,其中τⁿ 映射到nτ 的元组。这样,将身份视为τ⁰ → τ 是有意义的,其中τ⁰ 只是空元组()。所以我们现在实际上可以简化代数的概念:它只是某种类型,上面有一些函数。

代数只是数学中的一种常见模式,已被“分解”,就像我们处理代码一样。人们注意到一大堆有趣的东西——前面提到的幺半群、群、格等等——都遵循类似的模式,所以他们把它抽象出来。这样做的好处与编程相同:它创建可重用的证明并使某些类型的推理更容易。

F-代数

但是,我们还没有完成保理。到目前为止,我们有一堆函数τⁿ → τ。我们实际上可以做一个巧妙的技巧将它们全部组合成一个函数。特别是,让我们看看幺半群:我们有mappend ∷ (τ, τ) → τmempty ∷ () → τ。我们可以使用 sum 类型将这些转换为单个函数 —Either。它看起来像这样:

op ∷ Monoid τ ⇒ Either (τ, τ) () → τ
op (Left (a, b)) = mappend (a, b)
op (Right ())    = mempty

实际上,对于任何代数,我们实际上可以重复使用这种转换来将所有τⁿ → τ 函数组合成一个函数。 (事实上​​,我们可以对任意数量的函数 a → τb → τ 以及 any a, b,… 执行此操作。)

这让我们可以将代数作为τ 的一个类型来讨论,它具有一个单个 函数,从一些混乱的Eithers 到单个τ。对于幺半群,这个烂摊子是:Either (τ, τ) ();对于组(有一个额外的τ → τ 操作),它是:Either (Either (τ, τ) τ) ()。对于每个不同的结构,它都是不同的类型。那么所有这些类型有什么共同点呢?最明显的是它们都只是乘积的总和——代数数据类型。例如,对于 monoid,我们可以创建一个适用于 any monoid τ 的 monoid 参数类型:

data MonoidArgument τ = Mappend τ τ -- here τ τ is the same as (τ, τ)
                      | Mempty      -- here we can just leave the () out

我们可以对群、环、格以及所有其他可能的结构做同样的事情。

所有这些类型还有什么特别之处?嗯,他们都是Functors!例如:

instance Functor MonoidArgument where
  fmap f (Mappend τ τ) = Mappend (f τ) (f τ)
  fmap f Mempty        = Mempty

所以我们可以进一步概括我们的代数概念。它只是某种类型的τ,带有函数f τ → τ,用于某些函子f。事实上,我们可以把它写成一个类型类:

class Functor f ⇒ Algebra f τ where
  op ∷ f τ → τ

这通常被称为“F 代数”,因为它由函子 F 确定。如果我们可以部分应用类型类,我们可以定义类似class Monoid = Algebra MonoidArgument 的东西。

代数

现在,希望您能很好地掌握代数是什么,以及它如何只是普通代数结构的概括。那么什么是 F 代数呢?嗯,co 暗示它是代数的“对偶”——也就是说,我们取一个代数并翻转一些箭头。我在上面的定义中只看到一个箭头,所以我将其翻转:

class Functor f ⇒ CoAlgebra f τ where
  coop ∷ τ → f τ

仅此而已!现在,这个结论似乎有点轻率(呵呵)。它告诉您 什么 是余代数,但并没有真正说明它的用途或我们为什么关心它。一旦我找到或想出一两个很好的例子,我稍后会谈到这个:P。

类和对象

阅读了一下之后,我想我对如何使用代数来表示类和对象有了一个很好的了解。我们有一个类型C,它包含类中对象所有可能的内部状态;该类本身是 C 的代数,它指定了对象的方法和属性。

如代数示例所示,如果我们有一堆函数,如 a → τb → τ 用于任何 a, b,…,我们可以使用求和类型 Either 将它们组合成一个函数。双重“概念”将结合一组τ → aτ → b 等类型的函数。我们可以使用求和类型的对偶——乘积类型来做到这一点。因此,鉴于上面的两个函数(称为fg),我们可以像这样创建一个:

both ∷ τ → (a, b)
both x = (f x, g x)

(a, a) 类型是直截了当的函子,因此它当然符合我们的 F 代数概念。这个特殊的技巧让我们可以将一堆不同的函数——或者,对于 OOP,方法——打包成一个 τ → f τ 类型的函数。

C 类型的元素代表对象的内部 状态。如果对象具有一些可读属性,它们必须能够依赖于状态。最明显的方法是让它们成为C 的函数。所以如果我们想要一个长度属性(例如object.length),我们将有一个函数C → Int

我们需要可以接受参数并修改状态的方法。为此,我们需要获取所有参数并生成一个新的C。让我们想象一个setPosition 方法,它采用xy 坐标:object.setPosition(1, 2)。它看起来像这样:C → ((Int, Int) → C)

这里的重要模式是对象的“方法”和“属性”将对象本身作为它们的第一个参数。这就像 Python 中的 self 参数和许多其他语言的隐式 this 一样。余代数本质上只是封装了采用self 参数的行为:这就是C → F C 中的第一个C

所以让我们把它们放在一起。让我们想象一个具有position 属性、name 属性和setPosition 函数的类:

class C
  private
    x, y  : Int
    _name : String
  public
    name        : String
    position    : (Int, Int)
    setPosition : (Int, Int) → C

我们需要两个部分来表示这个类。首先,我们需要表示对象的内部状态;在这种情况下,它只包含两个Ints 和一个String。 (这是我们的类型C。)然后我们需要提出代表类的代数。

data C = Obj  x, y  ∷ Int
             , _name ∷ String 

我们有两个属性要写。它们很简单:

position ∷ C → (Int, Int)
position self = (x self, y self)

name ∷ C → String
name self = _name self

现在我们只需要能够更新位置:

setPosition ∷ C → (Int, Int) → C
setPosition self (newX, newY) = self  x = newX, y = newY 

这就像一个带有显式self 变量的Python 类。现在我们有一堆self → 函数,我们需要将它们组合成一个用于余代数的函数。我们可以用一个简单的元组来做到这一点:

coop ∷ C → ((Int, Int), String, (Int, Int) → C)
coop self = (position self, name self, setPosition self)

类型((Int, Int), String, (Int, Int) → c)——对于any c——是一个函子,所以coop确实有我们想要的形式:Functor f ⇒ C → f C

鉴于此,Ccoop 形成一个余代数,它指定了我在上面给出的类。您可以看到我们如何使用相同的技术为我们的对象指定任意数量的方法和属性。

这让我们可以使用代数推理来处理类。例如,我们可以引入“F-余代数同态”的概念来表示类之间的转换。这是一个听起来很吓人的术语,仅表示保留结构的余代数之间的转换。这使得考虑将类映射到其他类变得更加容易。

简而言之,F-coalgebra 表示一个类,它具有一堆属性和方法,这些属性和方法都依赖于包含每个对象内部状态的 self 参数。

其他类别

到目前为止,我们已经将代数和余代数作为 Haskell 类型进行了讨论。代数只是类型 τ 和函数 f τ → τ,而余代数只是类型 τ 和函数 τ → f τ

然而,没有什么能真正将这些想法与 Haskell 联系起来本身。事实上,它们通常是根据集合和数学函数而不是类型和 Haskell 函数来介绍的。事实上,我们可以将这些概念推广到任何类!

我们可以为某个类别C 定义一个 F 代数。首先,我们需要一个函子F : C → C——也就是一个endofunctor。 (所有 Haskell Functors 实际上都是来自Hask → Hask 的内函子。)然后,代数只是来自C 的一个对象A,带有一个态射F A → A。除了 A → F A 之外,余代数是相同的。

我们通过考虑其他类别可以获得什么?好吧,我们可以在不同的环境中使用相同的想法。像单子一样。在 Haskell 中,monad 是某种类型的 M ∷ ★ → ★,具有三个操作:

map      ∷ (α → β) → (M α → M β)
return   ∷ α → M α
join     ∷ M (M α) → M α

map 函数只是证明MFunctor 的事实。所以我们可以说 monad 只是一个具有 两个 操作的函子:returnjoin

函子本身形成一个范畴,它们之间的态射是所谓的“自然变换”。自然转换只是将一个函子转换为另一个函子同时保留其结构的一种方式。 Here's 一篇很好的文章,有助于解释这个想法。它谈到了concat,这只是join 的列表。

使用 Haskell 函子,两个函子的组合就是函子本身。在伪代码中,我们可以这样写:

instance (Functor f, Functor g) ⇒ Functor (f ∘ g) where
  fmap fun x = fmap (fmap fun) x

这有助于我们将join 视为f ∘ f → f 的映射。 join 的类型是 ∀α. f (f α) → f α。直观地,我们可以看到一个对所有类型有效的函数α可以被认为是f的转换。

return 是一个类似的转换。它的类型是∀α. α → f α。这看起来不同——第一个 α 不是“在”函子中!令人高兴的是,我们可以通过在此处添加一个恒等函子来解决此问题:∀α. Identity α → f α。所以return 是一个转换Identity → f

现在我们可以将 monad 看作是一个基于函子 f 和运算 f ∘ f → fIdentity → f 的代数。这看起来是不是很熟悉?它与幺半群非常相似,只是某种类型的 τ,操作为 τ × τ → τ() → τ

所以一个 monad 就像一个 monoid,除了我们有一个函子而不是一个类型。这是同一种代数,只是属于不同的类别。 (据我所知,这就是“单子只是内函子范畴中的幺半群”这句话的来源。)

现在,我们有这两个操作:f ∘ f → fIdentity → f。为了得到相应的代数,我们只需翻转箭头。这给了我们两个新的操作:f → f ∘ ff → Identity。我们可以通过添加上面的类型变量将它们变成 Haskell 类型,给我们∀α. f α → f (f α)∀α. f α → α。这看起来就像comonad的定义:

class Functor f ⇒ Comonad f where
  coreturn ∷ f α → α
  cojoin   ∷ f α → f (f α)

因此,comonad 是 coalgebra 在内函子范畴中的一个。

【讨论】:

这是非常有价值的。我已经设法通过阅读和示例(例如,从看到它们与变态的使用)模糊了对整个 F 代数业务的一些直觉,但这对我来说也非常清楚。谢谢! 这是一个很好的解释。 @EdwardKmett:谢谢。我添加的关于类和对象的东西好吗?我今天才读到它,但它似乎有道理。 这是一个很好的展示,Tikhon,非常感谢!我已经看到这部分内容以多种不同的方式表达,但直到现在才如此有意义。你已经把代数的概念说得很清楚了。我对余代数仍然很模糊(OOP 类比并没有真正帮助我),但它只是翻转箭头的事实意味着它不会太难。我也一直期待您将变异对象状态的概念与 Edward 的镜头库联系起来,因为它代表了这样一个余代数。 为了它的价值:这里的“内函子类别”更准确地说是一个类别,其对象是某个类别的内函子,其箭头是自然变换。这是一个幺半群类别,函子组合对应于(,),恒等函子对应于()。幺半群类别中的幺半群对象是一个带有与您的幺半群代数相对应的箭头的对象,它在 Hask 中描述了一个幺半群对象,其中产品类型作为幺半群结构。 C 上的 endofunctors 类别中的幺半群对象是 C 上的单子,所以是的,您的理解是正确的。 :]【参考方案3】:

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 运算是两个集合AB 的叉积(即,一对集合(a, b) 其中a遍历A 的所有元素,b 遍历B 的所有元素。

两个集合AB 的不相交并集是集合A | B,它是集合(a, 1) : a in A(b, 2) : b in B 的并集。本质上它是来自AB 的所有元素的集合,但是每个元素都被“标记”为属于AB,所以当我们从A | B 中选择任何元素时,我们将立即知道该元素是来自A 还是来自B

我们可以“加入”NilCons 函数,因此它们将形成一个处理集合 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 的用法和可能的其他类型组成。例如,IntListIntTree 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 都是相同的,但 Tf 本身可以是任意的。例如,(String, g :: 1 | (Int x String) -> String)(Double, h :: Int | (Double, Double) -> Double) 对于某些 gh 也是对应 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

可以看出reductorF1 Int -> Int类型的函数,就像F-代数的定义一样!事实上,(Int, reductor) 是一个 F1 代数。

因为IntList 是一个初始的 F1 代数,对于每个类型 T 和每个函数 r :: F1 T -> T 都存在一个函数,称为 catamorphism 对于 r,它转换 @987654411 @ to T,这样的功能是独一无二的。实际上,在我们的示例中,reductor 的变质是sumFold。请注意reductorsumFold 的相似之处:它们的结构几乎相同! 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

appendFoldappendReductor 的变态,它将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 代数中一样,gT 可以是任意的,例如,(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)

同样,我们可以看到natsnatsBuilder 之间存在一些相似之处。这与我们之前观察到的减速器和折叠的连接非常相似。 natsnatsBuilder 的变形。

另一个例子,一个函数,它接受一个值和一个函数,并将函数的连续应用流返回到该值:

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)

那么iterateiterateBuilder 的变形。

结论

因此,简而言之,F-代数允许定义折叠,即将递归结构简化为单个值的操作,而 F-代数允许做相反的事情:从单个价值。

事实上在Haskell F-代数和F-余代数是重合的。这是一个非常好的属性,这是每种类型中存在“底部”值的结果。因此在 Haskell 中,可以为每种递归类型创建折叠和展开。但是,这背后的理论模型比我上面介绍的更复杂,所以我故意避开它。

希望这会有所帮助。

【讨论】:

appendReductor 的类型和定义看起来有点奇怪,并没有真正帮助我看到那里的模式...... :) 你能仔细检查它是否正确吗?......应该怎么做减速机种类一般是什么样子的?在r 的定义中,F1 是由 IntList 确定的,还是任意的 F?【参考方案4】:

我将从明显与编程相关的内容开始,然后添加一些数学内容,以使其尽可能具体和脚踏实地。


让我们引用一些计算机科学家的共同归纳......

http://www.cs.umd.edu/~micinski/posts/2012-09-04-on-understanding-coinduction.html

归纳是关于有限数据,协归纳是关于无限数据。

无限数据的典型例子是惰性列表的类型(一个 溪流)。例如,假设我们有以下对象 记忆:

 let (pi : int list) = (* some function which computes the digits of
 π. *)

计算机无法容纳所有的 π,因为它只有有限的数量 内存!但它可以做的是保存一个有限程序,它将 产生你想要的任意长的 π 展开。只要 由于您只使用列表的有限部分,您可以用它来计算 无限列表尽可能多。

但是,请考虑以下程序:

let print_third_element (k : int list) =   match k with
     | _ :: _ :: thd :: tl -> print thd


 print_third_element pi

这个程序应该打印 pi 的第三个数字。但是在某些语言中,函数的任何参数在传递之前都会被评估 进入一个函数(严格,而不是懒惰,评估)。如果我们使用这个 减少顺序,那么我们上面的程序将永远运行计算 pi 的数字,然后才能传递给我们的打印机函数(其中 永远不会发生)。由于机器没有无限内存, 程序最终会耗尽内存并崩溃。这可能不是最好的评估顺序。

http://adam.chlipala.net/cpdt/html/Coinductive.html

在 Haskell 等惰性函数式编程语言中,无限数据结构 无处不在。无限列表和更奇特的数据类型提供了方便 程序各部分之间通信的抽象。实现类似 在许多情况下,没有无限惰性结构的便利性需要 控制流的杂技反转。

http://www.alexandrasilva.org/#/talks.html


将环境数学上下文与通常的编程任务联系起来

什么是“代数”?

代数结构通常如下所示:

    东西 这些东西能做什么

这听起来应该像具有 1. 属性和 2. 方法的对象。或者更好的是,它应该听起来像类型签名。

标准数学示例包括幺半群 ⊃ 群 ⊃ 向量空间 ⊃ “代数”。 Monoids 就像自动机:动词序列(例如,f.g.h.h.nothing.f.g.f)。 git 日志总是添加历史并且从不删除它将是一个幺半群而不是一个组。如果您添加倒数(例如负数、分数、根、删除累积的历史记录、恢复破碎的镜子),您将得到一个组。

组包含可以一起添加或减去的事物。例如Durations 可以加在一起。 (但Dates 不能。)持续时间存在于向量空间(不仅仅是一个组)中,因为它们也可以通过外部数字进行缩放。 (scaling :: (Number,Duration) → Duration 的类型签名。)

代数⊂向量空间可以做另一件事:有一些m :: (T,T) → T。将此称为“乘法”或不称为“乘法”,因为一旦您离开Integers,“乘法”(或"exponentiation")应该是什么就不太明显了。

(这就是为什么人们希望(范畴论)通用属性:告诉他们应该应该像什么乘法

)


代数 → 代数

与乘法相比,以非任意方式定义共乘法更容易,因为从T → (T,T) 开始,您可以重复相同的元素。 (“对角线图”——类似于谱理论中的对角线矩阵/算子)

Counit 通常是轨迹(对角线条目的总和),尽管同样重要的是你的 counit 做什么trace 只是矩阵的一个很好的答案。

一般来说,查看dual space 的原因是因为在该空间中更容易思考。例如,有时考虑法线向量比考虑法线向量更容易,但是您可以用向量控制平面(包括超平面)(现在我说的是熟悉的几何向量,就像在光线追踪器中一样) .


驯服(非)结构化数据

数学家可能正在建模一些有趣的东西,比如TQFT's,而程序员则必须与之搏斗

日期/时间 (+ :: (Date,Duration) → Date), 地点(Paris(+48.8567,+2.3508)!这是一个形状,而不是一个点。), 在某种意义上应该是一致的非结构化 JSON, 错误但关闭的 XML, 非常复杂的 GIS 数据,应该满足大量合理的关系, 正则表达式对你有意义,但对 perl 的意义要小得多。 CRM 应该包含所有主管的电话号码和别墅位置、他(现在的前)妻子和孩子的姓名、生日和所有以前的礼物,每个都应该满足“明显”的关系(对客户来说很明显)很难编写代码, .....

计算机科学家在谈论余代数时,通常会想到固定运算,例如笛卡尔积。我相信这就是人们所说的“代数是 Haskell 中的余代数”的意思。但在某种程度上,程序员必须对PlaceDate/TimeCustomer 等复杂的数据类型进行建模,并使这些模型看起来与真实世界一样(或者至少是最终用户对真实世界的看法) world) 尽可能——我相信对偶,可能在集合世界之外有用。

【讨论】:

以上是关于在编程的上下文中,“代数”是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章

在上下文(非)敏感分析中,“上下文”到底是啥意思?

在哈希表的上下文中,“桶条目”是啥意思?

在 MySQL EXPLAIN EXTENDED 的上下文中过滤是啥意思?

在推送通知的上下文中,“后台应用程序”是啥意思?

在 dockerized 开发环境的上下文中,“构建工件”是啥意思?

-> 在这个函数调用的上下文中是啥意思? [复制]