如何使catamorphisms能够与参数化/索引类型一起使用?

12
我最近学习了一些关于F-代数的知识:https://www.fpcomplete.com/user/bartosz/understanding-algebras。我想将这种功能扩展到更高级的类型(索引和高阶)。此外,我查看了“使Haskell晋升”(http://research.microsoft.com/en-us/people/dimitris/fc-kind-poly.pdf),它非常有帮助,因为它给我的模糊“发明”命名了名称。
然而,我似乎无法创建适用于所有形状的统一方法。
代数需要一些“载体类型”,但我们正在遍历的结构预期特定形状(它本身,递归应用),因此我想出了一个“虚拟”容器,可以携带任何类型,但形状符合预期。然后,我使用类型族来将它们耦合在一起。
这种方法似乎有效,为我的“cata”函数提供了相当通用的签名。
然而,我使用的其他东西(Mu、Algebra)仍然需要针对每个形状单独的版本,只是为了传递一堆类型变量。我希望类似PolyKinds的东西可以帮助(我成功地使用它来形成虚拟类型),但似乎它只适用于另一种方式。
由于IFunctor1和IFunctor2没有额外的变量,因此我尝试通过附加(通过类型族)保持索引的功能类型来统一它们,但由于存在量化,这似乎是不允许的,因此我在那里留下了多个版本。
有没有什么方法可以统一这两种情况?我是否忽略了某些技巧,还是现在只是一个限制?还有其他可以简化的事情吗?
{-# LANGUAGE DataKinds                 #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE PolyKinds                 #-}
{-# LANGUAGE Rank2Types                #-}
{-# LANGUAGE StandaloneDeriving        #-}
{-# LANGUAGE TypeFamilies              #-}
{-# LANGUAGE TypeOperators             #-}
{-# LANGUAGE UndecidableInstances      #-}
module Cata where

-- 'Fix' for indexed types (1 index)
newtype Mu1 f a = Roll1 { unRoll1 :: f (Mu1 f) a }
deriving instance Show (f (Mu1 f) a) => Show (Mu1 f a)

-- 'Fix' for indexed types (2 index)
newtype Mu2 f a b = Roll2 { unRoll2 :: f (Mu2 f) a b }
deriving instance Show (f (Mu2 f) a b) => Show (Mu2 f a b)

-- index-preserving function (1 index)
type s :-> t = forall i. s i -> t i

-- index-preserving function (2 index)
type s :--> t = forall i j. s i j -> t i j

-- indexed functor (1 index)
class IFunctor1 f where
  imap1 :: (s :-> t) -> (f s :-> f t)

-- indexed functor (2 index)
class IFunctor2 f where
  imap2 :: (s :--> t) -> (f s :--> f t)

-- dummy container type to store a solid result type
-- the shape should follow an indexed type
type family Dummy (x :: i -> k) :: * -> k

type Algebra1 f a = forall t. f ((Dummy f) a) t -> (Dummy f) a t
type Algebra2 f a = forall s t. f ((Dummy f) a) s t -> (Dummy f) a s t

cata1 :: IFunctor1 f => Algebra1 f a -> Mu1 f t -> (Dummy f) a t
cata1 alg = alg . imap1 (cata1 alg) . unRoll1

cata2 :: IFunctor2 f => Algebra2 f a -> Mu2 f s t -> (Dummy f) a s t
cata2 alg = alg . imap2 (cata2 alg) . unRoll2

以下是两个需要处理的结构示例:

ExprF1 看起来像一个普通的有用的东西,它将嵌入式类型附加到对象语言中。

ExprF2 是人为的(额外的参数恰好被提升(DataKinds)),只是为了找出“通用”的 cata2 是否能够处理这些形式。

-- our indexed type, which we want to use in an F-algebra (1 index)
data ExprF1 f t where
     ConstI1 :: Int -> ExprF1 f Int
     ConstB1 :: Bool -> ExprF1 f Bool
     Add1    :: f Int  -> f Int -> ExprF1 f Int
     Mul1    :: f Int  -> f Int -> ExprF1 f Int
     If1     :: f Bool -> f t -> f t -> ExprF1 f t
deriving instance (Show (f t), Show (f Bool)) => Show (ExprF1 f t)

-- our indexed type, which we want to use in an F-algebra (2 index)
data ExprF2 f s t where
     ConstI2 :: Int -> ExprF2 f Int True
     ConstB2 :: Bool -> ExprF2 f Bool True
     Add2    :: f Int True -> f Int True -> ExprF2 f Int True
     Mul2    :: f Int True -> f Int True -> ExprF2 f Int True
     If2     :: f Bool True -> f t True -> f t True -> ExprF2 f t True
deriving instance (Show (f s t), Show (f Bool t)) => Show (ExprF2 f s t)

-- mapper for f-algebra (1 index)
instance IFunctor1 ExprF1 where
  imap1 _    (ConstI1 x)  = ConstI1 x
  imap1 _    (ConstB1 x)  = ConstB1 x
  imap1 eval (x `Add1` y) = eval x `Add1` eval y
  imap1 eval (x `Mul1` y) = eval x `Mul1` eval y
  imap1 eval (If1 p t e)  = If1 (eval p) (eval t) (eval e)

-- mapper for f-algebra (2 index)
instance IFunctor2 ExprF2 where
  imap2 _    (ConstI2 x)  = ConstI2 x
  imap2 _    (ConstB2 x)  = ConstB2 x
  imap2 eval (x `Add2` y) = eval x `Add2` eval y
  imap2 eval (x `Mul2` y) = eval x `Mul2` eval y
  imap2 eval (If2 p t e)  = If2 (eval p) (eval t) (eval e)

-- turned into a nested expression
type Expr1 = Mu1 ExprF1

-- turned into a nested expression
type Expr2 = Mu2 ExprF2

-- dummy containers
newtype X1 x y = X1 x deriving Show
newtype X2 x y z = X2 x deriving Show

type instance Dummy ExprF1 = X1
type instance Dummy ExprF2 = X2


-- a simple example agebra that evaluates the expression
-- turning bools into 0/1    
alg1 :: Algebra1 ExprF1 Int
alg1 (ConstI1 x)            = X1 x
alg1 (ConstB1 False)        = X1 0
alg1 (ConstB1 True)         = X1 1
alg1 ((X1 x) `Add1` (X1 y)) = X1 $ x + y
alg1 ((X1 x) `Mul1` (X1 y)) = X1 $ x * y
alg1 (If1 (X1 0) _ (X1 e))  = X1 e
alg1 (If1 _ (X1 t) _)       = X1 t

alg2 :: Algebra2 ExprF2 Int
alg2 (ConstI2 x)            = X2 x
alg2 (ConstB2 False)        = X2 0
alg2 (ConstB2 True)         = X2 1
alg2 ((X2 x) `Add2` (X2 y)) = X2 $ x + y
alg2 ((X2 x) `Mul2` (X2 y)) = X2 $ x * y
alg2 (If2 (X2 0) _ (X2 e))  = X2 e
alg2 (If2 _ (X2 t) _)       = X2 t


-- simple helpers for construction
ci1 :: Int -> Expr1 Int
ci1 = Roll1 . ConstI1

cb1 :: Bool -> Expr1 Bool
cb1 = Roll1 . ConstB1

if1 :: Expr1 Bool -> Expr1 a -> Expr1 a -> Expr1 a
if1 p t e = Roll1 $ If1 p t e

add1 :: Expr1 Int -> Expr1 Int -> Expr1 Int
add1 x y = Roll1 $ Add1 x y

mul1 :: Expr1 Int -> Expr1 Int -> Expr1 Int
mul1 x y = Roll1 $ Mul1 x y

ci2 :: Int -> Expr2 Int True
ci2 = Roll2 . ConstI2

cb2 :: Bool -> Expr2 Bool True
cb2 = Roll2 . ConstB2

if2 :: Expr2 Bool True -> Expr2 a True-> Expr2 a True -> Expr2 a True
if2 p t e = Roll2 $ If2 p t e

add2 :: Expr2 Int True -> Expr2 Int True -> Expr2 Int True
add2 x y = Roll2 $ Add2 x y

mul2 :: Expr2 Int True -> Expr2 Int True -> Expr2 Int True
mul2 x y = Roll2 $ Mul2 x y


-- test case
test1 :: Expr1 Int
test1 = if1 (cb1 True)
            (ci1 3 `mul1` ci1 4 `add1` ci1 5)
            (ci1 2)

test2 :: Expr2 Int True
test2 = if2 (cb2 True)
            (ci2 3 `mul2` ci2 4 `add2` ci2 5)
            (ci2 2)


main :: IO ()
main = let (X1 x1) = cata1 alg1 test1
           (X2 x2) = cata2 alg2 test2
       in do print x1
             print x2

输出:

17
17

4
使用一对索引来编制索引比使用两个索引更加统一。我的建议是,在处理索引集时,尽可能使用一个索引,并利用提升类型来构造索引结构。1、2、4、8,该指数了! - pigworker
1
@pigworker:啊,是的,这确实是一个不错的解决方法。那么我只需要 cata1 和它的伙伴们了。 - Mathijs Kwik
好问题,引发了一位大师的精彩回答。很少有标签可以让你从基础到卓越。耶,Haskell。 - AndrewC
2个回答

13
我在2009年写了一篇关于这个主题的演讲,名为"Slicing It"。它确实指向了我的Strathclyde同事Johann和Ghani对于GADTs的初始代数语义的工作。我使用了SHE提供的符号来编写数据索引类型,但令人高兴的是,这已经被"promotion"故事所取代。
演讲的关键点,正如我的评论所述,是要系统地使用恰好一个索引,但利用其种类可以变化的事实。
因此,我们确实有(使用我当前首选的"Goscinny and Uderzo"名称):
type s :-> t = forall i. s i -> t i

class FunctorIx f where
  mapIx :: (s :-> t) -> (f s :-> f t)

现在您可以展示FunctorIx在不动点下是闭合的。关键是将两个索引集合合并成一个,从而提供索引选择。
data Case (f :: i -> *) (g :: j -> *) (b :: Either i j) :: * where
  L :: f i -> Case f g (Left i)
  R :: g j -> Case f g (Right j)

(<?>) :: (f :-> f') -> (g :-> g') -> Case f g :-> Case f' g'
(f <?> g) (L x) = L (f x)
(f <?> g) (R x) = R (g x)

现在我们可以对表示“有效载荷”或“递归子结构”的“包含元素”的函子进行不动点处理。
data MuIx (f :: (Either i j -> *) -> j -> *) :: (i -> *) -> j -> * where
  InIx :: f (Case x (MuIx f x)) j -> MuIx f x j

作为结果,我们可以在“payload”上进行mapIx...
instance FunctorIx f => FunctorIx (MuIx f) where
  mapIx f (InIx xs) = InIx (mapIx (f <?> mapIx f) xs)

...或者对“递归子结构”进行范畴论折叠...

foldIx :: FunctorIx f => (f (Case x t) :-> t) -> MuIx f x :-> t
foldIx f (InIx xs) = f (mapIx (id <?> foldIx f) xs)

...或同时两者。

mapFoldIx :: FunctorIx f => (x :-> y) -> (f (Case y t) :-> t) -> MuIx f x :-> t
mapFoldIx e f (InIx xs) = f (mapIx (e <?> mapFoldIx e f) xs)

FunctorIx的乐趣在于它具有出色的闭包特性,这要归功于能够变化索引种类的能力。MuIx允许携带有效负载的概念,并且可以进行迭代。因此,与使用多个索引相比,使用结构化索引是有动力的。

1
哇,那有点让我大开眼界!我有点能看出它是如何工作的,但我只能梦想着自己也能创造出这样的东西。它在很多情况下都非常有用,包括我的问题,所以我要把答案改成这个。你能否详细解释一下你所说的“闭合”和“闭包性质”?我没有广泛的数学背景:(顺便说一句,FunctorIx、FoldIx和朋友们让我想起了某个卡通:P - Mathijs Kwik
2
未索引的故事定义了 Mu (f :: * -> *) :: *,其中一些 Functor f 描述了递归数据类型的节点结构:f 抽象出递归子结构的位置。但是,即使它具有“元素”的概念,Mu f 本身也不是一个 Functor。想想列表:列表节点可以有一个列表元素的位置和一个子列表的位置。您可以通过获取描述一个节点的双函子(即不是 Functor)的不动点来描述列表 Functor。但是,递归索引的 Functor 是其他索引 Functor 的不动点。因此,索引 Functor 在不动点下是封闭的。 - pigworker

3
如果我理解正确,这正是Johann和Ghani的“初始代数语义就足够了!”所解决的问题。
请参阅他们的,特别是在https://personal.cis.strath.ac.uk/neil.ghani/papers/ghani-tlca07.pdf中。
编辑:对于GADT案例,请参阅他们的后续论文“使用GADTs进行结构化编程的基础”。请注意,他们遇到了一个障碍,可以使用PolyKinds来解决,我们现在已经拥有:http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.111.2948 此博客文章也可能会引起兴趣:http://www.timphilipwilliams.com/posts/2013-01-16-fixing-gadts.html

看起来这篇论文肯定非常有趣。 然而,我不确定它是否涉及我的问题(通过GADT索引类型)。该论文的最后一段明确指出,GADTs是可能的未来工作: “最后,本文的技术可能为高级数据类型(如混合方差数据类型、GADTs和依赖类型)的折叠、构建和融合规则的理论提供见解。” - Mathijs Kwik
很酷,有很多东西可以阅读 :) 可能需要一些时间才能找到完美的解决方案,但我相信它会给我很多正确方向的步骤,所以我会接受你的答案。谢谢! - Mathijs Kwik
我已经将它移植到Kmett风格的递归方案了。 https://gist.github.com/xgrommx/4c72ae9d407214deab55fe9aebc08b45 - xgrommx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接