首页 文章

“编程代码”在编程环境中意味着什么?

提问于
浏览
316

我在函数式编程和PLT圈子里曾多次听到过“enggebras”这个术语,特别是在讨论对象,comonads,镜头等时 . 谷歌搜索这个术语给出了这些结构的数学描述的页面,这对我来说是非常难以理解的 . 任何人都可以解释一下代数在编程环境中的含义,它们的意义是什么,以及它们与对象和共同体的关系?

4 回答

  • 80

    F-代数和F-余代数是数学结构,有助于推理归纳类型(或递归类型) .

    F-algebras

    我们先从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 操作是两个集合 AB 的交叉积(即成对的集合 (a, b) ,其中 a 遍历 A 的所有元素, b 遍历 B 的所有元素) .

    两组 AB 的不相交联合是一组 A | B ,它是集合 {(a, 1) : a in A}{(b, 2) : b in B} 的并集 . 本质上它是 AB 的所有元素的集合,但是每个元素'marked'属于 AB ,所以当我们从 A | B 中选择任何元素时,我们将立即知道该元素是来自 A 还是来自 B .

    我们可以'join' 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 是某种类型, ff :: F T -> T 类型的函数 . 在我们的例子中,F-代数是 (IntList, Nil|Cons)(IntTree, Leaf|Branch) . 但请注意,尽管每个F的 f 函数类型相同,但 Tf 本身可以是任意的 . 例如,某些 gh(String, g :: 1 | (Int x String) -> String)(Double, h :: Int | (Double, Double) -> Double) 也是相应F的F-代数 .

    之后我们可以引入F-代数同态,然后引入具有非常有用性质的初始F-代数 . 事实上, (IntList, Nil|Cons) 是一个初始的F1代数, (IntTree, Leaf|Branch) 是一个初始的F2代数 . 我不会提供这些术语和属性的确切定义,因为它们比需要的更复杂和抽象 .

    尽管如此,例如 (IntList, Nil|Cons) 是F-代数的事实允许我们在这种类型上定义类似于 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
    

    我们看到这个函数由两部分组成:第一部分定义了这个函数行为 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 ,存在一个函数,称为 r 的catamorphism,它将 IntList 转换为 T ,并且这样的函数是唯一的 . 实际上,在我们的例子中, reductor 的一个变形是 sumFold . 注意 reductorsumFold 是如何相似的:它们具有几乎相同的结构!在 reductor 中定义 s 参数用法(其类型对应于 T )对应于 sumFold 定义中 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-coalgebras

    F-coalgebras是F-algebras的所谓'dual'术语 . 它们允许我们为递归数据类型定义 unfolds ,即从某个值构造递归结构的方法 .

    假设您有以下类型:

    data IntStream = Cons (Int, IntStream)
    

    这是一个无限的整数流 . 它唯一的构造函数具有以下类型:

    Cons :: (Int, IntStream) -> IntStream
    

    或者,就集合而言

    Cons :: Int × IntStream -> IntStream
    

    Haskell允许您对数据构造函数进行模式匹配,因此您可以在 IntStream 上定义以下函数:

    head :: IntStream -> Int
    head (Cons (x, xs)) = x
    
    tail :: IntStream -> IntStream
    tail (Cons (x, xs)) = xs
    

    您可以自然地将这些函数'join'转换为 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 是一个类型, gg :: T -> F T 类型的函数 . 例如, (IntStream, head&tail) 是F1-余代数 . 同样,就像在F-代数中一样, gT 可以是任意的,例如, (String, h :: String -> Int x String) 也是某些h的F1-代数 .

    在所有的F-coalgebras中都有所谓的终端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))
    

    它的构建器功能如下:

    iterateBuilder :: (Int -> Int) -> Int -> Int × Int
    iterateBuilder f n = (n, f n)
    

    然后 iterateiterateBuilder 的变形 .

    结论

    因此,简而言之,F-代数允许定义折叠,即将递归结构减少为单个值的操作,而F-coalgebras允许相反:从单个值构造[潜在]无限结构 .

    事实上在Haskell F-algebras和F-coalgebras重合 . 这是一个非常好的属性,这是每种类型中存在“底部”值的结果 . 所以在Haskell中,可以为每个递归类型创建折叠和展开 . 然而,这背后的理论模型比我上面提到的更复杂,所以我故意避免它 .

    希望这可以帮助 .

  • 455

    通过教程论文A tutorial on (co)algebras and (co)induction应该会给你一些关于计算机科学中的共同代数的见解 .

    下面是它的引用来说服你,

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


    Prelude, about Category theory. 类别理论应该重命名仿函数理论 . 因为类别是定义仿函数必须定义的类别 . (此外,函子是人们必须定义的,以便定义自然变换 . )

    What's a functor? 这是从一个集合到另一个集合的转换,它保留了它们的结构 . (有关详细信息,网上有很多很好的描述) .

    What's is an F-algebra? 它只是研究仿函数的普遍适用性 .

    How can it be link to computer science ? 程序可以作为一组结构化信息进行查看 . 程序的执行对应于此结构化信息集的修改 . 执行应保留程序结构听起来不错 . 然后可以将执行视为这组信息的仿函数应用程序 . (定义程序的那个) .

    Why F-co-algebra ? 程序本质上是双重的,因为它们是通过信息来描述的,并且它们对它起作用 . 那么主要是组成程序并使其改变的信息可以以两种方式查看 .

    • 可以定义为程序正在处理的信息的数据 .

    • 可以定义为程序共享信息的状态 .

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

    • F-代数是对数据宇宙上的函数变换的研究(如此处所定义) .

    • F-co-algebras是研究状态宇宙上的函数变换(如本文所定义) .

    在程序的生命周期中,数据和状态共存,并且它们彼此完成 . 他们是双重的 .

  • 4

    代数

    我认为开始的地方是理解 algebra 的想法 . 这只是代数结构的推广,如群,环,幺半群等 . 大多数时候,这些东西是以集合的形式引入的,但是因为我们会讨论Haskell类型 . (我无法抗拒使用一些希腊字母 - 它们让一切看起来都更酷!)

    那么,代数只是一种具有一些功能和身份的类型 τ . 这些函数使用不同数量的 τ 类型的参数并生成 τ :uncurried,它们看起来都像 (τ, τ,…, τ) → τ . 它们还可以具有"identities"-元素 τ ,它们对某些函数有特殊行为 .

    最简单的例子就是幺半群 . monoid是任何类型 τ ,其函数为 mappend ∷ (τ, τ) → τ ,标识为 mzero ∷ τ . 其他示例包括诸如组之类的东西(除了具有额外的 invert ∷ τ → τ 函数之外,它们就像monoids一样),环,格子等等 .

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

    代数只是数学中常见的模式,已被“排除”,就像我们使用代码一样 . 人们注意到一大堆有趣的东西 - 前面提到的幺半群,群体,格子等都遵循类似的模式,因此他们将其抽象出来 . 这样做的好处与编程相同:它创建了可重复使用的证据,并使某些推理更容易 .

    F-Algebras

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

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

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

    这让我们可以将代数作为 τ 类型进行讨论,其中包含从 Either 的一些混乱到单个 τ 的单个函数 . 对于幺半群,这个烂摊子是: Either (τ, τ) () ;对于组(具有额外的 τ → τ 操作),它是: Either (Either (τ, τ) τ) () . 对于每种不同的结构,它都是不同的类型 . 那么所有这些类型有什么共同之处呢?最明显的是它们都只是产品 - 代数数据类型的总和 . 例如,对于monoids,我们可以创建一个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-algebra",因为它由仿函数 F 决定 . 如果我们可以部分应用类型类,我们可以定义像 class Monoid = Algebra MonoidArgument 这样的东西 .

    Coalgebras

    现在,希望你能很好地掌握代数是什么以及它如何只是普通代数结构的推广 . 那么F-coalgebra是什么?好吧,co意味着它是代数的“双重” - 也就是说,我们采用代数并翻转一些箭头 . 我只看到上面定义中的一个箭头,所以我只是翻转它:

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

    这就是全部!现在,这个结论可能看起来有点轻率(嘿) . 它告诉你什么是代数是什么,但是一旦我找到或想出一个好的例子,它并没有真正给出任何有关它如何得到它的见解:P .

    类和对象

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

    如代数示例所示,如果我们有 a → τb → τ 等一系列函数用于任何 a, b,… ,我们可以使用 Either (和类型)将它们全部组合成单个函数 . 双"notion"将组合一堆 τ → aτ → b 等类型的函数 . 我们可以使用sum类型的对偶 - 产品类型来实现这一点 . 所以考虑到上面的两个函数(称为 fg ),我们可以像这样创建一个函数:

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

    类型 (a, a) 是一种直接方式的仿函数,所以它肯定符合我们的F-coalgebra概念 . 这个特殊的技巧让我们将一堆不同的函数 - 或者对于OOP,方法 - 打包成一个类型为 τ → f τ 的函数 .

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

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

    这里重要的模式是对象的"methods"和"properties"将对象本身作为它们的第一个参数 . 这就像Python中的 self 参数一样,与许多其他语言的隐式 this 类似 . 一个代数基本上只是封装了一个 self 参数的行为:这就是 C → F C 中的第一个 C .

    所以让's put it all together. Let'想象一个具有 position 属性, name 属性和 setPosition 函数的类:

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

    我们需要两个部分来代表这个类 . 首先,我们需要表示对象的内部状态;在这种情况下,它只包含两个 Int 和一个 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) -对于任何 c - 是一个仿函数,所以 coop 确实具有我们想要的形式: Functor f ⇒ C → f C .

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

    这让我们可以使用代数推理来处理类 . 例如,我们可以引入“F-coalgebra同态”的概念来表示类之间的转换 . 这是一个可怕的声音术语,只意味着保留结构的天使之间的转换 . 这使得考虑将类映射到其他类更容易 .

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

    其他类别

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

    但是,没有什么能真正将这些想法与Haskell本身联系起来 . 事实上,它们通常是根据集合和数学函数而不是类型和Haskell函数引入的 . 实际上,我们可以将这些概念概括为任何类别!

    我们可以为某些类别 C 定义F代数 . 首先,我们需要一个算子 F : C → C - 即一个endofunctor . (所有Haskell Functor s实际上都是来自 Hask → Hask 的endofunctors . )然后,代数只是来自 C 的一个对象 A ,其中有一个态射 F A → A . 余代数是相同的,除了 A → F A .

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

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

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

    函子自己形成一个类别,它们之间的态射是所谓的"natural transformations" . 自然变换只是将一个仿函数转换为另一个仿函数同时保留其结构的一种方法 . 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 α . 这看起来与众不同 - 第一个 α 不是"in"一个仿函数!幸运的是,我们可以通过在那里添加一个标识函数来解决这个问题: ∀α. Identity α → f α . 所以 return 是一个转型 Identity → f .

    现在我们可以将monad想象成一个代数,基于一些函数 f ,其中包含操作 f ∘ f → fIdentity → f . 不是't this look familiar? It'非常类似于monoid,它只是某种类型的 τ ,其操作是 τ × τ → τ() → τ .

    所以monad就像一个monoid,除了没有类型我们有一个仿函数 . 它是同一种代数,只是在不同的类别中 . (据我所知,这就是“monad只是endofunctors类别中的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是一个endofunctors类别的代数 .

  • 37

    我将从与编程相关的内容开始,然后添加一些数学内容,以尽可能保持具体和实际 .


    让我们引用一些关于coinduction的计算机科学家的话......

    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
    examples of coalgebras by Alexandra Silva


    将环境数学上下文与通常的编程任务相关联

    什么是“代数”?

    代数结构通常如下:

    • 东西

    • 这些东西可以做什么

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

    标准数学例子包括monoid⊃group⊃矢量空间⊃"an algebra" . 幺半群就像自动机一样:动词序列(例如, f.g.h.h.nothing.f.g.f ) . 始终添加历史记录并且永远不会删除它的 git 日志将是一个幺半群而不是一个组 . 如果你添加了反转(例如负数,分数,根,删除累积的历史记录,不破坏破碎的镜像),你会得到一个组 .

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

    代数⊂向量空间可以做另一件事:有一些 m :: (T,T) → T . 称之为"multiplication"或不要,因为一旦你离开 Integers ,"multiplication"(或"exponentiation")应该是不太明显的 .

    (这就是人们关注(类别理论)通用属性的原因:告诉他们乘法应该做什么或者是什么样的:

    universal property of product


    代数→代数

    与乘法相比,乘法更容易以感觉非任意的方式定义,因为从 T → (T,T) 开始,你可以重复相同的元素 . ("diagonal map" - 类似于光谱理论中的对角矩阵/算子)

    对比通常是跟踪(对角线条目的总和),尽管重要的是你的国家所做的事情; trace 对于矩阵来说只是一个很好的答案 .

    一般来说,查看dual space的原因是因为它有时更容易考虑法向量而不是关于它所说的熟悉的几何向量的平面,就像在光线跟踪器中一样 .


    驯服(非)结构化数据

    数学家可能会像TQFT's那样建模有趣的东西,而程序员则需要与之搏斗

    • 个日期/时间( + :: (Date,Duration) → Date ),

    • 个地方( Paris(+48.8567,+2.3508) !这是一个形状,而不是一个点 . ),

    • 非结构化JSON,在某种意义上应该是一致的,

    • 错误但关闭的XML,

    • 极其复杂的GIS数据应满足合理的关系,

    • 正则表达式对你来说意味着什么,但对perl意味着更少 .

    • CRM应该包含所有执行's phone numbers and villa locations, his (now ex-) wife and kids'名称,生日和所有以前的礼物,每个礼物都应该满足"obvious"关系(对客户来说很明显),这些关系难以编码,

    • .....

    计算机科学家在谈论代数时,通常会考虑定制操作,就像笛卡尔积 . 我相信这就是人们说"Algebras are coalgebras in Haskell"时的意思 . 但是程序员必须对复杂的数据类型进行建模,如 PlaceDate/TimeCustomer ,并使这些模型看起来像现实世界(或至少是最终用户对现实世界的看法)尽可能 - 我相信对偶,可能超越集合世界 .

相关问题