据我所知,来自Haskell的递归数据类型对应于data Nat = Zero | Succ Nat
类[1类别[2,{{3}}]中的endofunctors的初始代数。例如:
F(-) = 1 + (-)
对应于endofunctor data List a = Nil | Cons a (List a)
的初始代数。F(A, -) = 1 + A × (-)
对应于endofunctor data Rose a = Node a (List (Rose a))
的初始代数。但是,我不清楚与玫瑰树对应的endofunctor应该是什么:
F(A, •, -) = A × (1 + (-) × (•))
令我困惑的是,有两个递归:一个用于玫瑰树,另一个用于列表。根据我的计算,我会得到以下仿函数,但它看起来并不正确:
data Rose a = Node a (Forest a)
type Forest a = List (Rose a)
或者,可以将玫瑰树定义为相互递归的数据类型:
{{1}}
相互递归数据类型是否在类别理论中有解释?
答案 0 :(得分:14)
我不鼓励谈论“Hask类别”,因为它潜意识地阻止你在Haskell编程中寻找其他分类结构。
实际上,玫瑰树可以被视为类型和函数的endofunctor的固定点,我们可能更好地调用Type
,现在Type
是类型的类型。如果我们给自己一些通常的仿函数套件......
newtype K a x = K a deriving Functor -- constant functor
newtype P f g x = P (f x, g x) deriving Functor -- products
......和固定点...
newtype FixF f = InF (f (FixF f))
...然后我们可以采取
type Rose a = FixF (P (K a) [])
pattern Node :: a -> [Rose a] -> Rose a
pattern Node a ars = InF (P (K a, ars))
[]
本身是递归的这一事实并不妨碍它通过Fix
用于形成递归数据类型。为了明确地拼出递归,我们有嵌套的固定点,这里有一些暗示选择的绑定变量名:
Rose a = μrose. a * (μlist. 1 + (rose * list))
现在,当我们到达第二个固定点时,我们有一个类型公式
1 + (rose * list)
在rose
和list
中都是functorial(实际上是严格肯定的)。有人可能会说它是Bifunctor
,但这是不必要的术语:它是(Type, Type)
到Type
的仿函数。你可以通过在对的第二个组件中使用一个固定点来创建一个Type -> Type
仿函数,这就是上面发生的事情。
Rose
的上述定义失去了重要的属性。
Rose :: Type -> Type -- GHC might say this, but it's lying
只有Rose x :: Type
if x :: Type
。特别是,
Functor Rose
不是一个很好的类型约束,这是一个遗憾,直观地说,玫瑰树应该是它们存储的元素中的花色。
您可以通过将Rose
构建为Bifunctor
的固定点来解决此问题。所以,实际上,当我们到达列表时,我们在范围内有{em>三个类型变量,a
,rose
和list
,并且我们有了functoriality在所有这些中。您需要一个不同的修复点类型构造函数和一个用于构建Bifunctor
实例的不同的工具包:对于Rose
,生命变得更容易,因为{{1在内部修复点中没有使用参数,但一般来说,将bifunctors定义为fixpoints需要三元组,然后我们就去了!
a
但在Type
中工作(对于各种索引类型i -> Type
)并且您已准备好进行相互递归,GADT等等。
因此,缩小,玫瑰树是由相互固定点给出的,它们具有完全合理的分类帐户,只要您看到哪些类别实际上在起作用。
答案 1 :(得分:4)
这不是你要问的问题的答案,但无论如何也许有趣。请注意
Rose a = a * List (Rose a)
List a = 1 + a * List a
以及*
分布在+
上的事实,你有
Rose a
= {- definition of `Rose` -}
a * List (Rose a)
= {- definition of `List` -}
a * (1 + Rose a * List (Rose a))
= {- `*` distributes over `+` -}
a + a * Rose a * List (Rose a)
= {- `*` is commutative -}
a + Rose a * a * List (Rose a)
= {- definition of `Rose` -}
a + Rose a * Rose a
(相等实际上表示同构)。所以你可能已经定义了
Rose a = a + Rose a * Rose a
或在Haskell,
data Rose a = Leaf a | Bin (Rose a) (Rose a)
也就是说,玫瑰树与普通(叶标记)二叉树同构,并且明显形成正常的初始代数。
答案 2 :(得分:2)
正如您所注意到的,Rose a
的仿函数的定义比较棘手,因为类型的递归出现被送入List
。问题是List
本身就是一个作为固定点获得的递归类型。 List (Rose a)
基本上对应于“Rose a
的任意数量的元素”,这是您无法单独使用产品和总和的签名表达的内容,因此需要对这些多个递归点进行额外的抽象。
仿函数F A - : * -> *
将不起作用,因为我们需要找到
F A X ≃ A × (1 + X × List X)
F A X ≃ A × (1 + X × (1 + X × List X))
F A X ≃ A × (1 + X × (1 + X × (1 + X × List X)))
...
一种方法是将List
视为原始。然后Rose a
只是
RoseF A : * -> * = λ X . A × List X
另一个更有趣的方法是遵循您发布的引用中的建议,并注意Rose a
的类型可以推广到抽象函数的抽象,其中递归事件被送入
GRose F A ≃ A × F (GRose F A)
现在GRose
的类型为(* -> *) -> (* -> *)
,因此它是一个更高阶的函子,将endofunctor映射到另一个。在我们的示例中,它会将仿函数List
映射到玫瑰树的类型中。
请注意,GRose仍然是递归的,所以上面实际上是说明了同构而不是我们问题的解决方案。我们可以通过另外抽象递归点
来尝试修复(眨眼)HRose G F A = A × F (G F A)
请注意,现在HRose
是((* -> *) -> (* -> *)) -> (* -> *) -> (* -> *)
类型的常规高阶仿函数,因此它将高阶仿函数映射到高阶仿函数。计算HRose
的最小固定点给我们
μ(HRose) F A ≃ A × F (μ(HRose) F A)
因此,如果我们提出Rose ≡ μ(HRose) List
,我们就会
Rose A ≃ A × List (Rose A)
这正是玫瑰树的定义方程。 您可以在高阶仿函数上找到使用固定点的泛型编程理论和实践的更多示例。例如,Here,Bird和Paterson在嵌套数据类型的上下文中开发它(但通常明确地保持定义)。它们还显示了以这种方式定义的数据类型的折叠的系统构造,以及各种定律。
答案 3 :(得分:1)
您似乎明白这是如何建模的
data List a = Nil | Cons a (List a)
对任何给定的A
采用endofunctor F(A, -) = 1 + A × (-)
的初始代数。我们称之为初始代数L(A)
。
如果我们忘记L(A)
中的态射,我们可以认为L(A)
是我们类别的对象。更好,L(-)
不仅是从对象到对象的映射,而且可以看作是一个endofunctor。
一旦L
将种子作为endofunctor,递归类型
data Rose a = Node a (List (Rose a))
对于任何A
m函数的初始代数,可以解释
G A = A * L A
是通过编写L
和*
(以及对角函子)获得的仿函数。
因此,同样的方法也有效。