现在,我们有这两个操作:f ∘ f → f和Identity → f。为了得到相应的协代数,我们只需要翻转箭头。这给了我们两个新的操作:f → f ∘ f和f → Identity。我们可以像上面那样通过添加类型变量将它们转换为Haskell类型,给我们∀α. f α → f (f α)和∀α. f α → α。这看起来就像一个comonad的定义:
class Functor f ⇒ Comonad f where
coreturn ∷ f α → α
cojoin ∷ f α → f (f α)
现在我们可以定义f -代数。T3只是一对(T, f),其中T是某种类型,而f是类型f :: F T -> T的函数。在我们的例子中,f代数是(IntList, Nil|Cons)和(IntTree, Leaf|Branch)。然而,请注意,尽管f函数类型对于每个F都是相同的,但T和f本身可以是任意的。例如,某些T1和T2的(String, g :: 1 | (Int x String) -> String)或T0也是对应F的F代数。
现在假设我们不是用Haskell编程,而是用某种语言编程,这种语言允许在类型签名中直接使用代数类型(好吧,技术上Haskell允许通过元组和Either a b数据类型使用代数类型,但这会导致不必要的冗长)。考虑一个函数:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
可以看出reductor是一个类型为F1 Int -> Int的函数,就像F-algebra的定义一样!的确,(Int, reductor)对是一个f1代数。
因为IntList是初始的f1代数,对于每个类型T和每个函数r :: F1 T -> T都存在一个函数,对于r称为T5,它将IntList转换为T,并且这样的函数是唯一的。的确,在我们的例子中,reductor的变形是sumFold。注意reductor和sumFold的相似之处:它们的结构几乎相同!在reductor定义中,T1参数的使用(其类型对应于T)对应于sumFold定义中T3计算结果的使用。
现在,F-coalgebra是一对(T, g),其中T是类型,g是类型g :: T -> F T的函数。例如,(IntStream, head&tail)是一个f1协代数。同样,就像在f代数中一样,g和T可以是任意的,例如,(String, h :: String -> Int x String)也是某个h的f1余代数。
在所有的f -共代数中,有所谓的终端F-coalgebras,它是初始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)