是否有可能通常推导出递归原理?

时间:2016-02-29 21:12:11

标签: haskell generic-programming

在Idris中,有一些神奇的机器可以自动为用户定义的类型创建(依赖)消除器。我想知道是否可以使用Haskell类型做一些事情(可能更少依赖)。例如,给定

data Foo a = No | Yes a | Perhaps (Foo a)

我想生成

foo :: b -> (a -> b) -> (b -> b) -> Foo a -> b
foo b _ _ No = b
foo _ f _ (Yes a) = f a
foo b f g (Perhaps c) = g (foo b f g x)

我对多变量函数和泛型非常弱,所以我可以使用一些帮助开始。

3 个答案:

答案 0 :(得分:7)

以下是使用GHC Generics执行此操作的开始。添加一些代码以重新关联package会使这更好。需要更多实例,这可能存在符合人体工程学的问题。

编辑:呸,我懒得回到数据系列,以获得我的类型平等派遣的注意力。这温和地改变了界面。我怀疑有足够trickery和/或使用内射类型系列,这可以在没有数据系列或重叠实例的情况下完成。

(:+:)

答案 1 :(得分:5)

正如Pigworker在问题评论中所说的那样,使用默认的Generic表示会导致极大的丑陋,因为我们在类型中没有关于递归的先验信息,我们必须通过以下方式挖掘递归事件:手动检查类型是否相等。我想在这里介绍具有显式f-代数式递归的替代解决方案。为此,我们需要一个替代的通用Rep。可悲的是,这意味着我们无法轻易进入GHC.Generics,但我希望这仍然会有所启发。

在我的第一个解决方案中,我的目标是在当前的GHC功能中尽可能简单地进行演示。第二个解决方案是TypeApplication - 基于GHC 8的重型解决方案,具有更复杂的类型。

像往常一样开始:

{-# language
  TypeOperators, DataKinds, PolyKinds,
  RankNTypes, EmptyCase, ScopedTypeVariables,
  DeriveFunctor, StandaloneDeriving, GADTs,
  TypeFamilies, FlexibleContexts, FlexibleInstances #-}

我的通用表示是产品总和的固定点。它略微扩展了generics-sop的基本模型,它也是产品的总和,但不是函数,因此不适合递归算法。我认为SOP总体上比任意嵌套类型更好的实际表示;你可以找到扩展的论据,为什么在paper中就是这种情况。简而言之,SOP删除了不必要的嵌套信息,并允许我们将元数据与基本数据分开。

但在其他任何事情之前,我们应该决定泛型类型的代码。在vanilla GHC.Generics中,没有明确定义的代码类型,因为sums,product等的类型构造函数构成了ad-hoc类型级语法,我们可以使用类型类对它们进行调度。我们更依赖于依赖类型泛型中的常规表示,并使用显式代码,解释和函数。我们的代码应该是实物:

[[Maybe *]]

外部列表对构造函数的总和进行编码,每个内部[Maybe *]编码构造函数。 Just *只是一个构造函数字段,而Nothing表示递归字段。例如,[Int]的代码为['[], [Just Int, Nothing]]

type Rep a = Fix (SOP (Code a)) 

class Generic a where
  type Code a :: [[Maybe *]]
  to   :: a -> Rep a
  from :: Rep a -> a

data NP (ts :: [Maybe *]) (k :: *) where
  Nil  :: NP '[] k
  (:>) :: t -> NP ts k -> NP (Just t ': ts) k
  Rec  :: k -> NP ts k -> NP (Nothing ': ts) k
infixr 5 :>  

data SOP (code :: [[Maybe *]]) (k :: *) where
  Z :: NP ts k -> SOP (ts ': code) k
  S :: SOP code k -> SOP (ts ': code) k

请注意,NP具有不同的递归和非递归字段构造函数。这非常重要,因为我们希望代码明确地反映在类型索引中。换句话说,我们希望NP也充当[Maybe *]的单身人士(虽然我们仍然在*中保持参数化,但有充分理由)。

我们在定义中使用k参数为递归留下了一个漏洞。我们像往常一样设置递归,将Functor个实例留给GHC:

deriving instance Functor (SOP code)
deriving instance Functor (NP code)

newtype Fix f = In {out :: f (Fix f)}

cata :: Functor f => (f a -> a) -> Fix f -> a
cata phi = go where go = phi . fmap go . out

我们有两个类型系列:

type family CurryNP (ts :: [Maybe *]) (r :: *) :: * where
  CurryNP '[]             r = r
  CurryNP (Just t  ': ts) r = t -> CurryNP ts r
  CurryNP (Nothing ': ts) r = r -> CurryNP ts r

type family Alg (code :: [[Maybe *]]) (r :: *) :: * where
  Alg '[]         r = ()
  Alg (ts ': tss) r = (CurryNP ts r, Alg tss r)

CurryNP ts r使结果类型为NP ts的{​​{1}}次,并且还会在递归事件中插入r

r计算Alg code r上的代数类型。它将各个构造函数的消除器组合在一起。这里我们使用普通的嵌套元组,但当然SOP code r - s也足够了。我们也可以在这里重用HList作为NP,但我发现它过于笨拙。

剩下的就是实现这些功能:

HList

这里的关键点是我们必须将uncurryNP :: CurryNP ts a -> NP ts a -> a uncurryNP f Nil = f uncurryNP f (x :> xs) = uncurryNP (f x) xs uncurryNP f (Rec k xs) = uncurryNP (f k) xs algSOP :: Alg code a -> SOP code a -> a algSOP fs (Z np) = uncurryNP (fst fs) np algSOP fs (S sop) = algSOP (snd fs) sop gcata :: Generic a => Alg (Code a) r -> a -> r gcata f = cata (algSOP f) . to 中的咖喱消除器转换为适当的" Alg代数,因为这是可以在SOP code a -> a中直接使用的形式。

让我们定义一些糖和实例:

cata

示例:

(<:) :: a -> b -> (a, b)
(<:) = (,)
infixr 5 <:

instance Generic (Fix (SOP code)) where
  type Code (Fix (SOP code)) = code
  to   = id
  from = id  

instance Generic [a] where
  type Code [a] = ['[], [Just a, Nothing]]  
  to   = foldr (\x xs -> In (S (Z (x :> Rec xs Nil)))) (In (Z Nil))
  from = gcata ([] <: (:) <: ()) -- note the use of "Generic (Rep [a])"

Full code.

然而,如果我们讨好并且不必使用> gcata (0 <: (+) <: ()) [0..10] 55 - s或元组来存储消除器,那就更好了。最方便的方法是使用与标准库折叠相同的参数顺序,例如HListfoldr。在这种情况下,maybe的返回类型由类型系列给出,该类型系列根据类型的通用代码进行计算。

gcata

type family CurryNP (ts :: [Maybe *]) (r :: *) :: * where CurryNP '[] r = r CurryNP (Just t ': ts) r = t -> CurryNP ts r CurryNP (Nothing ': ts) r = r -> CurryNP ts r type family Fold' code a r where Fold' '[] a r = r Fold' (ts ': tss) a r = CurryNP ts a -> Fold' tss a r type Fold a r = Fold' (Code a) r (a -> r) gcata :: forall a r. Generic a => Fold a r 高度(完全)含糊不清。我们需要明确的应用程序或gcata,我选择了前者,导致GHC 8依赖。但是,一旦我们提供Proxy类型,结果类型就会减少,我们可以轻松地进行评估:

a

我在> :t gcata @[_] gcata @[_] :: Generic [t] => r -> (t -> r -> r) -> [t] -> r > :t gcata @[_] 0 gcata @[_] 0 :: Num t1 => (t -> t1 -> t1) -> [t] -> t1 > gcata @[_] 0 (+) [0..10] 55 上面使用了部分类型签名。我们也可以为此创建一个简写:

[_]

可以用作gcata1 :: forall f a r. Generic (f a) => Fold (f a) r gcata1 = gcata @(f a) @r

我不想在这里详细说明implementation of the above gcata。它不比简单版本长,但gcata1 @[]实现非常多毛(令人尴尬的是,它对我的​​延迟答案负责)。现在我无法解释它,因为我用Agda援助写了它,这需要大量的自动搜索和类型俄罗斯方块。

答案 2 :(得分:1)

正如在评论和其他答案中所说的那样,最好从可以访问递归位置的通用表示开始。

使用此类表示的一个库是multirec(另一个是compdata):

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GADTs, TypeFamilies, MultiParamTypeClasses, RankNTypes #-}
module FooFold where

import Generics.MultiRec.FoldAlgK
import Generics.MultiRec.TH

data Foo a = No | Yes a | Perhaps (Foo a)

data FooF :: * -> * -> * where
  Foo :: FooF a (Foo a)

deriveAll ''FooF

foldFoo :: (r, (a -> r, r -> r)) -> Foo a -> r
foldFoo phi = fold (const phi) Foo

FoldAlgK模块提供具有单个结果类型的折叠,并将代数类型计算为嵌套对。另外还要相对容易。该库提供了一些其他变体。