[英]What does “coalgebra” mean in the context of programming?
我在函数式编程和PLT圈子中多次听到过“enggebras”这个术语,特别是在讨论对象,comonads,镜头等时。 谷歌搜索这个术语给出了对这些结构进行数学描述的页面,这对我来说几乎是不可理解的。 任何人都可以解释一下代数在编程环境中的意义,它们的意义是什么,以及它们与对象和共同体的关系?
我认为开始的地方是理解代数的概念。 这只是代数结构的推广,如群,环,幺半群等。 大多数时候,这些东西是以集合的形式引入的,但由于我们是朋友之间,我将讨论Haskell类型。 (我无法抗拒使用一些希腊字母 - 它们让一切看起来都更酷!)
因此,代数只是具有一些函数和身份的类型τ
。 这些函数采用不同数量的τ
类型的参数并产生τ
:uncurried,它们看起来都像(τ, τ,…, τ) → τ
。 它们也可以具有“身份” - τ
元素,它们具有某些功能的特殊行为。
最简单的例子就是幺半群。 幺半群是任何类型τ
,具有函数mappend ∷ (τ, τ) → τ
和同一性mzero ∷ τ
。 其他例子包括诸如组之类的东西(除了具有额外的invert ∷ τ → τ
函数之外,它们就像幺半群一样),环,格子等。
所有功能都在τ
运行,但可以有不同的特性。 我们可以写这些出来作为τⁿ → τ
,其中τⁿ
映射到的元组n
τ
。 这样,将身份视为τ⁰ → τ
是有意义的,其中τ⁰
只是空元组()
。 所以我们现在实际上可以简化代数的概念:它只是一些带有一些函数的类型。
代数只是数学中常见的模式,已被“排除”,就像我们使用代码一样。 人们注意到一大堆有趣的东西 - 前面提到的幺半群,群体,格子等都遵循类似的模式,因此他们将其抽象出来。 这样做的好处与编程相同:它可以创建可重复使用的证据,并使某些推理更容易。
但是,我们还没有完成因子分解。 到目前为止,我们有一堆函数τⁿ → τ
。 我们实际上可以做一个巧妙的技巧,将它们全部合并到一个函数中。 特别是,让我们看看幺半群:我们有mappend ∷ (τ, τ) → τ
和mempty ∷ () → τ
。 我们可以使用类型A和把这些成一个单一的功能, Either
。 它看起来像这样:
op ∷ Monoid τ ⇒ Either (τ, τ) () → τ
op (Left (a, b)) = mappend (a, b)
op (Right ()) = mempty
对于任何代数,我们实际上可以反复使用这个变换将所有 τⁿ → τ
函数组合成一个函数。 (事实上,对于任何 a, b,…
,我们可以为任意数量的函数a → τ
, b → τ
等执行此操作。)
这让我们谈论代数作为一种τ
从一些乱七八糟的单一功能Either
s到一个单一的τ
。 对于幺半群,这个混乱是: 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代数”,因为它由函子F
确定。 如果我们可以部分应用类型类,我们可以定义class Monoid = Algebra MonoidArgument
。
现在,希望你能很好地掌握代数是什么,以及它如何只是普通代数结构的推广。 什么是F-coalgebra? 好吧,co意味着它是代数的“双重” - 也就是说,我们采用代数并翻转一些箭头。 我只在上面的定义中看到一个箭头,所以我只是翻转它:
class Functor f ⇒ CoAlgebra f τ where
coop ∷ τ → f τ
这就是全部! 现在,这个结论可能看起来有点轻率(嘿)。 它告诉你什么是代数是什么 ,但并没有真正给出任何关于它如何有用或我们关心的原因的见解。 一旦我找到或想出一个好的例子,我会稍微谈谈它:P。
在阅读了一下之后,我想我很清楚如何使用代数来表示类和对象。 我们有一个C
,它包含了类中对象的所有可能的内部状态; 类本身是C
上的代数,它指定了对象的方法和属性。
如代数示例所示,如果我们有一堆函数,如a → τ
和b → τ
对于任何a, b,…
,我们可以使用Either
(和类型)将它们全部组合成单个函数。 双重“概念”将组合τ → a
, τ → b
等类型的一系列函数。 我们可以使用sum类型的对偶 - 产品类型来实现这一点。 所以考虑到上面的两个函数(称为f
和g
),我们可以像这样创建一个函数:
both ∷ τ → (a, b)
both x = (f x, g x)
类型(a, a)
是一种直接方式的仿函数,所以它肯定符合我们的F-余代数的概念。 这个特殊的技巧让我们将一堆不同的函数 - 或者对于OOP,方法 - 打包成一个类型为τ → f τ
函数。
类型C
的元素表示对象的内部状态。 如果对象具有一些可读属性,则它们必须能够依赖于状态。 最明显的方法是使它们成为C
的函数。 因此,如果我们想要一个长度属性(例如object.length
),我们将有一个函数C → Int
。
我们想要可以采用参数和修改状态的方法。 为此,我们需要获取所有参数并生成一个新的C
让我们设想一个采用x
和y
坐标的setPosition
方法: object.setPosition(1, 2)
。 它看起来像这样: C → ((Int, Int) → C)
。
这里重要的模式是对象的“方法”和“属性”将对象本身作为它们的第一个参数。 这就像self
在Python参数和喜欢的隐性this
许多其他语言的。 一个代数基本上只是封装了一个self
参数的行为:这就是C → FC
的第一个C
所以我们把它们放在一起吧。 让我们设想一个具有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
鉴于此, C
与coop
构成了一个代数,它指定了我上面给出的类。 您可以看到我们如何使用相同的技术为对象指定任意数量的方法和属性。
这让我们可以使用代数推理来处理类。 例如,我们可以引入“F-coalgebra同态”的概念来表示类之间的转换。 这是一个可怕的声音术语,意味着保留结构的天使之间的转换。 这使得考虑将类映射到其他类更容易。
简而言之,F-coalgebra通过拥有一堆属性和方法来表示一个类,这些属性和方法都依赖于包含每个对象内部状态的self
参数。
到目前为止,我们已经将代数和余代数作为Haskell类型进行了讨论。 一个代数仅仅是一个类型τ
与函数f τ → τ
和一个余代数只是一个类型τ
与函数τ → f τ
。
然而,没有什么能真正将这些想法与Haskell 本身联系起来 。 事实上,它们通常是根据集合和数学函数而不是类型和Haskell函数引入的。 实际上,我们可以将这些概念概括为任何类别!
我们可以为某些类别C
定义F代数。 首先,我们需要一个仿函数F : C → C
- 即一个endofunctor 。 (所有Haskell Functor
实际上都是来自Hask → Hask
。)然后,代数只是来自C
的对象A
,具有态射FA → A
。 除了A → FA
之外,余代数是相同的。
考虑其他类别我们可以获得什么? 好吧,我们可以在不同的环境中使用相同的想法。 像单子一样。 在Haskell中,monad是某种类型的M ∷ ★ → ★
有三个操作:
map ∷ (α → β) → (M α → M β)
return ∷ α → M α
join ∷ M (M α) → M α
map
函数只是M
是Functor
这一事实的证明。 所以我们可以说monad只是一个有两个操作的仿函数: return
和join
。
函子本身形成一个类别,它们之间的态射是所谓的“自然变换”。 自然变换只是将一个仿函数转换为另一个仿函数同时保留其结构的一种方法。 这是一篇很好的文章,有助于解释这个想法。 它谈论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 (f α) → f α
。 直观地,我们可以看到对所有类型α
有效的函数如何被认为是f
的变换。
return
是一种类似的转变。 它的类型是∀α. α → f α
∀α. α → f α
。 这看起来不同 - 第一个α
不是“in”的仿函数! 令人高兴的是,我们可以通过在那里添加一个身份∀α. Identity α → f α
函数来解决这个问题: ∀α. Identity α → f α
∀α. Identity α → f α
。 所以return
是转换Identity → f
。
现在我们可以将monad想象成一个基于f ∘ f → f
子f
的代数,其运算f ∘ f → f
和Identity → f
。 这看起来不熟悉吗? 它与幺半群非常相似,它只是某种类型τ
,其操作τ × τ → τ
和() → τ
。
所以monad就像一个monoid,除了没有类型我们有一个functor。 它是同一种代数,只是在不同的类别中。 (据我所知,这就是“monad只是endofunctors类别中的monoid”这句话。)
现在,我们有这两个操作: f ∘ f → f
和Identity → f
。 为了获得相应的代数,我们只需翻转箭头。 这给了我们两个新的操作: f → f ∘ f
和f → Identity
。 我们可以通过添加上面的类型变量将它们变成Haskell类型,给我们∀α. f α → f (f α)
∀α. f α → α
∀α. f α → f (f α)
∀α. f α → α
∀α. f α → f (f α)
和∀α. f α → α
∀α. f α → α
。 这看起来就像comonad的定义:
class Functor f ⇒ Comonad f where
coreturn ∷ f α → α
cojoin ∷ f α → f (f α)
所以comonad然后在endofunctors的类别余代数 。
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 | B
是集合{(a, 1) : a in A}
的集合{(a, 1) : a in A}
{(b, 2) : b in B}
。 本质上它是来自A
和B
的所有元素的集合,但是每个元素'标记'属于A
或B
,所以当我们从A | B
选择任何元素时 A | B
我们将立即知道这个元素是来自A
还是来自B
我们可以“加入” Nil
和Cons
函数,因此它们将构成一个函数,用于集合1 | (Int × IntList)
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 :: FT -> T
类型的函数。 在我们的例子中,F-代数是(IntList, Nil|Cons)
和(IntTree, Leaf|Branch)
。 但是请注意,尽管每种F的f
函数类型相同,但T
和f
本身可以是任意的。 例如,对于某些g
和h
, (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
我们看到这个函数由两部分组成:第一部分在IntList
Nil
部分定义了这个函数的行为,第二部分在Cons
部分定义了函数的行为。
现在假设我们不是在Haskell中编程,而是在某种语言中允许直接在类型签名中使用代数类型(从技术上讲,Haskell允许通过元组和Either ab
数据类型来使用代数类型,但这会导致不必要的冗长)。 考虑一个功能:
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
它转换IntList
至T
,并且这样的功能是唯一的。 实际上,在我们的例子中,缩减reductor
的sumFold
是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
上的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
一个catamorphism,它将IntList
转换为IntList
。
因此,基本上,F代数允许我们在递归数据结构上定义“折叠”,即将结构减少到某个值的操作。
F-coalgebras是F-algebras的所谓“双重”术语。 它们允许我们为递归数据类型定义unfolds
,即从某个值构造递归结构的方法。
假设您有以下类型:
data IntStream = Cons (Int, IntStream)
这是一个无限的整数流。 它唯一的构造函数具有以下类型:
Cons :: (Int, IntStream) -> IntStream
或者,就集合而言
Cons :: Int × IntStream -> IntStream
Haskell允许您对数据构造函数进行模式匹配,因此您可以定义在IntStream
上IntStream
的以下函数:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
您可以自然地将这些函数“连接”到IntStream -> Int × IntStream
类型的单个函数中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 -> FT
类型的函数。 例如, (IntStream, head&tail)
是F1-coalgebra。 同样,就像在F-algebras中一样, g
和T
可以是任意的,例如, (String, h :: String -> Int x String)
也是某些h的F1-代数。
在所有的F-coalgebras中都有所谓的终端F-余代数 ,它们是初始F-代数的双重代数。 例如, IntStream
是终端F-代数。 这意味着对于每个类型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
的变形natsBuilder
。
另一个例子,一个函数,它接受一个值和一个函数,并将函数的连续应用程序流返回给值:
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)
然后, iterate
是iterateBuilder
一个变形iterateBuilder
。
因此,简而言之,F-代数允许定义折叠,即将递归结构减少为单个值的操作,而F-coalgebras允许相反:从单个值构造[潜在]无限结构。
事实上在Haskell F-algebras和F-coalgebras重合。 这是一个非常好的属性,这是每种类型中存在“底部”值的结果。 所以在Haskell中,可以为每个递归类型创建折叠和展开。 然而,这背后的理论模型比我上面提到的更复杂,所以我故意避免它。
希望这可以帮助。
阅读教程论文关于(共)代数和(共)归纳的教程应该会给你一些关于计算机科学中的共同代数的见解。
以下是引用你的引用,
一般而言,某些编程语言中的程序操纵数据。 在过去几十年的计算机科学发展过程中,很明显需要对这些数据进行抽象描述,例如确保一个人的程序不依赖于其运行的数据的特定表示。 而且,这种抽象性有助于正确性证明。
这种愿望导致在计算机科学中使用代数方法,在称为代数规范或抽象数据类型理论的分支中。 研究对象本身就是数据类型,使用了代数中熟悉的技术概念。 计算机科学家使用的数据类型通常是从给定的(构造函数)操作集合生成的,正是由于这个原因,代数的“初始性”起着如此重要的作用。
事实证明,标准代数技术可用于捕获计算机科学中使用的数据结构的各种基本方面。 但事实证明,很难用代数方式描述计算中发生的一些固有的动态结构。 这种结构通常涉及国家概念,可以以各种方式进行转换。 这种基于状态的动态系统的形式化方法通常使用自动机或过渡系统,作为经典的早期参考。
在过去的十年中,人们逐渐认识到,这种基于状态的系统不应该被描述为代数,而应该被称为代数代数。 这些是代数的正式对偶,在本教程中将以精确的方式进行。 代数的“初始性”的双重性质,即终结性对于这种共代数来说是至关重要的。 并且这种最终共代数所需的逻辑推理原理不是归纳,而是共同归纳。
前奏,关于范畴理论。 类别理论应该重命名仿函数理论。 因为类别是定义仿函数必须定义的类别。 (此外,仿函数是人们必须定义的,以便定义自然变换。)
什么是算子? 它是从一组到另一组的转换,保留了它们的结构。 (有关详细信息,网上有很多很好的描述)。
什么是F代数? 它是仿函数的代数。 这只是对仿函数普遍适用性的研究。
它如何与计算机科学联系起来? 程序可以作为一组结构化信息进行查看。 程序的执行对应于该结构化信息集的修改。 执行应保留程序结构听起来不错。 然后可以将执行视为这组信息的仿函数应用程序。 (定义程序的那个)。
为什么F-co-algebra? 程序是本质上是双重的,因为它们是通过信息来描述的,并且它们依赖于它。 那么主要是组成程序并使它们改变的信息可以以两种方式查看。
那么在这个阶段,我想说,
在程序的生命周期中,数据和状态共存,并且它们彼此完成。 他们是双重的。
我将从显然与编程相关的东西开始,然后添加一些数学内容,尽可能保持具体和脚踏实地。
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.方法的对象。 或者甚至更好,它应该听起来像类型签名。
标准数学例子包括monoid⊃group⊃矢量空间⊃“代数”。 幺半群就像自动机:动词序列(例如, fghhnothing.fgf
)。 总是添加历史记录并且永远不会删除它的git
日志将是一个monoid而不是一个组。 如果你添加了反转(例如负数,分数,根,删除累积的历史记录,不破坏破碎的镜像),你会得到一个组。
组包含可以一起添加或减去的内容。 例如,可以将Duration
s加在一起。 (但是Date
不能。)持续时间存在于向量空间(不仅仅是一个组),因为它们也可以通过外部数字进行缩放。 ( scaling :: (Number,Duration) → Duration
的类型签名scaling :: (Number,Duration) → Duration
。)
代数⊂向量空间可以做另一件事:有一些m :: (T,T) → T
。 称之为“乘法”或不这样做,因为一旦你离开Integers
,那么“乘法”(或“取幂” )应该是什么不太明显。
(这就是人们关注(类别理论)通用属性的原因:告诉他们乘法应该做什么或者是什么样的 :
)
与乘法相比,乘法更容易以感觉非任意的方式定义,因为从T → (T,T)
你可以重复相同的元素。 (“对角线图” - 类似于光谱理论中的对角矩阵/算子)
计数通常是跟踪(对角线条目的总和),尽管重要的是你的国家所做的事情; trace
只是矩阵的一个很好的答案。
一般来说,看待双重空间的原因是因为在这个空间中思考更容易。 例如,考虑法向量有时比考虑它的法线更容易,但你可以用向量控制平面(包括超平面)(现在我说的是熟悉的几何向量,就像在光线跟踪器中一样) 。
数学家可能会像TQFT那样建模有趣的东西,而程序员则必须与之搏斗
+ :: (Date,Duration) → Date
), Paris
≠ (+48.8567,+2.3508)
!这是一个形状,而不是一个点。), 计算机科学家在谈论代数时,通常会考虑定制操作,就像笛卡尔积。 我相信当人们说“代数是哈斯克尔的代数”时,这就是人们的意思。 但是程序员必须对像Place
, Date/Time
和Customer
这样的复杂数据类型进行建模,并使这些模型看起来尽可能像现实世界(或者至少是最终用户对现实世界的看法) - 我相信双重身份,可能只有在世界范围内有用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.