如何制作Applicative的固定长度矢量实例?

时间:2018-03-10 07:46:59

标签: haskell types pattern-matching typeclass gadt

我最近了解了促销活动,并决定尝试写矢量。

{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
module Vector where
  data Nat = Next Nat | Zero
  data Vector :: Nat -> * -> * where
    Construct :: t -> Vector n t -> Vector ('Next n) t
    Empty :: Vector 'Zero t
  instance Functor (Vector n) where
    fmap f a =
      case a of
        Construct x b -> Construct (f x) (fmap f b)
        Empty -> Empty

到目前为止,一切正常。但是在尝试制作Vector的{​​{1}}实例时,我遇到了一个问题。

Applicative

我不知道怎么做instance Applicative (Vector n) where a <*> b = case a of Construct f c -> case b of Construct x d -> Construct (f x) (c <*> d) Empty -> Empty pure x = _ 。我试过这个:

pure

但第一行出现case n of Next _ -> Construct x (pure x) Zero -> Empty 错误,此表达式的第三行出现Variable not in scope: n :: Nat

所以,我使用了下面的黑客。

Couldn't match type n with 'Zero

它完成了工作,但它并不漂亮。它引入了一个无用的类class Applicative' n where ap' :: Vector n (t -> u) -> Vector n t -> Vector n u pure' :: t -> Vector n t instance Applicative' n => Applicative' ('Next n) where ap' (Construct f a) (Construct x b) = Construct (f x) (ap' a b) pure' x = Construct x (pure' x) instance Applicative' 'Zero where ap' Empty Empty = Empty pure' _ = Empty instance Applicative' n => Applicative (Vector n) where (<*>) = ap' pure = pure' 。每当我想在任何函数中使用Applicative' Applicative时,我都必须提供额外的无用约束Vector,它实际上适用于任何Applicative' n

这样做会有更好,更清洁的方法吗?

4 个答案:

答案 0 :(得分:7)

您可以直接制作相同的内容:

instance Applicative (Vector Zero) where
  a <*> b = Empty
  pure x = Empty

instance Applicative (Vector n) => Applicative (Vector (Next n)) where
  a <*> b = 
    case a of
      Construct f c ->
        case b of
          Construct x d -> Construct (f x) (c <*> d)
  pure x = Construct x (pure x)

正如我可以推断的那样:对于不同类型的类,代码应该是类型感知的。如果您有多个实例,不同的类型将获得不同的实现,并且很容易解决。但是,如果您尝试使用单个非递归实例来创建它,则基本上没有关于运行时类型的信息,并且始终相同的代码仍需要确定要处理的类型。当您有输入参数时,您可以利用GADT为您提供类型信息。但对于pure,没有输入参数。因此,您必须为Applicative实例提供一些上下文。

答案 1 :(得分:2)

这是一个利用singletons包的(注释)替代方案。

非常粗略,Haskell不允许我们在上面的代码中对类型级值(如n)进行模式匹配。使用singletons,我们可以在此处和那里要求并提供一些SingI的实例。

{-# LANGUAGE GADTs , KindSignatures, DataKinds, TemplateHaskell, 
             TypeFamilies, ScopedTypeVariables #-}
{-# OPTIONS -Wall #-}

import Data.Singletons.TH

-- Autogenerate singletons for this type
$(singletons [d|
   data Nat = Next Nat | Zero
   |])

-- as before
data Vector :: Nat -> * -> * where
   Construct :: t -> Vector n t -> Vector ('Next n) t
   Empty :: Vector 'Zero t

-- as before
instance Functor (Vector n) where
   fmap _ Empty = Empty
   fmap f (Construct x b) = Construct (f x) (fmap f b)        

-- We now require n to carry its own SingI instance.
-- This allows us to pattern match on n.
instance SingI n => Applicative (Vector n) where
   Empty <*> Empty = Empty
   -- Here, we need to access the singleton on n, so that later on we
   -- can provide the SingI (n-1) instance we need for the recursive call.
   -- The withSingI allows us to use m :: SNat (n-1) to provide the instance.
   (Construct f c) <*> (Construct x d) = case sing :: SNat n of
      SNext m -> withSingI m $ Construct (f x) (c <*> d)

   -- Here, we can finally pattern match on n.
   -- As above, we need to provide the instance with withSingI
   -- to the recursive call.
   pure x = case sing :: SNat n of
      SZero -> Empty
      SNext m -> withSingI m $ Construct x (pure x)

使用它需要在每次使用时提供SingI n实例,这有点不方便,但不是太多(IMO)。可悲的是<*>并不真正需要SingI n,因为原则上它可以从手边的两个向量重新计算出来。但是,pure没有输入向量,因此它只能与提供的单例进行模式匹配。

作为另一种选择,与原始代码类似,可以编写

instance Applicative (Vector Zero) where
   ...
instance Applicative (Vector n) => Applicative (Vector (Next n)) where
   ...

这不是完全等效的,并且需要在Applicative (Vector n) =>未知的所有函数中添加上下文n,但这可能足以用于多种用途。

答案 2 :(得分:2)

考虑这是@ chi答案的附录,以提供对单身方法的额外解释......

如果您还没有这样做,我建议您阅读Hasochism paper。特别是,在该论文的第3.1节中,他们正好处理了这个问题,并将其用作隐式单例参数(@ chi的答案的SingINATTY类型类的激励示例在Hasochism论文中)是必要的,而不仅仅是方便的。

因为它适用于您的代码,主要问题是pure需要运行时表示它应该生成的向量的长度,以及类型级变量n不符合要求。解决方案是引入一个新的GADT,一个“单例”,提供直接对应于提升类型NextZero的运行时值:

data Natty (n :: Nat) where
  ZeroTy :: Natty Zero
  NextTy :: Natty n -> Natty (Next n)

我尝试使用与论文大致相同的命名约定:Natty是相同的,ZeroTyNextTy对应于论文的Zy和{{1 }}

这个显式单例本身很有用。例如,请参阅论文中Sy的定义。此外,我们可以轻松编写vchop的变体,使用显式单例来完成其工作:

pure

我们还不能使用它来定义vcopies :: Natty n -> a -> Vector n a vcopies ZeroTy _ = Empty vcopies (NextTy n) x = Construct x (vcopies n x) ,因为pure的签名是由pure类型类确定的,我们无法将其显式化单身Applicative在那里。

解决方案是引入隐式单例,这允许我们在需要时通过Natty n函数在以下类型类的上下文中检索显式单例:

natty

现在,如果我们处于class NATTY n where natty :: Natty n instance NATTY Zero where natty = ZeroTy instance NATTY n => NATTY (Next n) where natty = NextTy natty 上下文中,我们可以致电NATTY nvcopies natty提供明确的vcopies参数,这样我们就可以写:< / p>

natty

使用上面instance NATTY n => Applicative (Vector n) where (<*>) = vapp pure = vcopies natty vcopies的定义,以及下面natty的定义:

vapp

注意一个奇怪的地方。出于不明原因,我们需要引入这个vapp :: Vector n (a -> b) -> Vector n a -> Vector n b vapp Empty Empty = Empty vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d) 辅助函数。以下没有 vapp的实例与基于NATTY的定义和类型检查相匹配:

case

如果我们添加instance Applicative (Vector n) where Empty <*> Empty = Empty Construct f c <*> Construct x d = Construct (f x) (c <*> d) pure = error "Argh! No NATTY!" 约束来定义NATTY

pure

instance NATTY n => Applicative (Vector n) where Empty <*> Empty = Empty Construct f c <*> Construct x d = Construct (f x) (c <*> d) pure = vcopies natty 的定义不再进行类型检查。问题是第二个(<*>)案例左侧的NATTY n约束不会自动暗示右侧的(<*>)约束(NATTY n1 }),所以GHC不希望我们在右侧调用Next n ~ n1。在这种情况下,因为在第一次使用约束之后实际上不需要约束,所以没有(<*>)约束的辅助函数,即NATTY,可以解决问题。

@chi使用vapp上的大小写匹配和辅助函数natty作为替代解决方法。这里的等效代码将使用一个辅助函数将一个显式单例转换为隐式withSingI上下文:

NATTY

允许我们写:

withNATTY :: Natty n -> (NATTY n => a) -> a
withNATTY ZeroTy a = a
withNATTY (NextTy n) a = withNATTY n a

这需要instance NATTY n => Applicative (Vector n) where Empty <*> Empty = Empty Construct f c <*> Construct x d = case (natty :: Natty n) of NextTy n -> withNATTY n $ Construct (f x) (c <*> d) pure x = case (natty :: Natty n) of ZeroTy -> Empty NextTy n -> Construct x (withNATTY n $ pure x) ScopedTypeVariables

无论如何,坚持使用辅助函数,完整的程序如下所示:

RankNTypes

{-# LANGUAGE DataKinds, GADTs, KindSignatures #-} module Vector where data Nat = Next Nat | Zero data Vector :: Nat -> * -> * where Construct :: t -> Vector n t -> Vector ('Next n) t Empty :: Vector 'Zero t data Natty (n :: Nat) where ZeroTy :: Natty Zero NextTy :: Natty n -> Natty (Next n) class NATTY n where natty :: Natty n instance NATTY Zero where natty = ZeroTy instance NATTY n => NATTY (Next n) where natty = NextTy natty instance Functor (Vector n) where fmap f a = case a of Construct x b -> Construct (f x) (fmap f b) Empty -> Empty instance NATTY n => Applicative (Vector n) where (<*>) = vapp pure = vcopies natty vapp :: Vector n (a -> b) -> Vector n a -> Vector n b vapp Empty Empty = Empty vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d) vcopies :: Natty n -> a -> Vector n a vcopies ZeroTy _ = Empty vcopies (NextTy n) x = Construct x (vcopies n x) 库的对应关系是:

singletons

自动生成单例(包含构造函数$(singletons [d| data Nat = Next Nat | Zero |]) SZero,而不是SNatZeroTy;以及类型NatTy而不是SNat )和隐式单例类(称为Natty而不是SingI并使用函数NATTY而不是sing),给出完整的程序:

natty

有关{-# LANGUAGE DataKinds, GADTs, KindSignatures, TemplateHaskell, TypeFamilies #-} module Vector where import Data.Singletons import Data.Singletons.TH $(singletons [d| data Nat = Next Nat | Zero |]) data Vector :: Nat -> * -> * where Construct :: t -> Vector n t -> Vector ('Next n) t Empty :: Vector 'Zero t instance Functor (Vector n) where fmap f a = case a of Construct x b -> Construct (f x) (fmap f b) Empty -> Empty instance SingI n => Applicative (Vector n) where (<*>) = vapp pure = vcopies sing vapp :: Vector n (a -> b) -> Vector n a -> Vector n b vapp Empty Empty = Empty vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d) vcopies :: SNat n -> a -> Vector n a vcopies SZero _ = Empty vcopies (SNext n) x = Construct x (vcopies n x) 库的功能及其构建方式的更多信息,我建议您阅读Introduction to Singletons

答案 3 :(得分:1)

其他几个答案引入了NattySNat类型来实施pure。实际上,拥有这种类型大大减少了对一次性类型的需求。然而,传统的Natty / SNat GADT的潜在缺点是,即使在编译时已知Nat,您的程序也会实际构建表示然后使用它。使用辅助类方法通常不会发生。你可以通过使用不同的表示来解决这个问题。

我将使用这些名称:

data Nat = Z | S Nat

假设我们定义了通常的

data Natty n where
  Zy :: Natty 'Z
  Sy :: Natty n -> Natty ('S n)

我们可以写出它的消除器(归纳原理):

natty :: p 'Z -> (forall k. p k -> p ('S k)) -> Natty n -> p n
natty z _ Zy = z
natty z s (Sy n) = s (natty z s n)

出于我们的目的,我们并不真正需要Natty;我们只需要它的感应原理!所以让我们定义另一个版本。我想这个编码有一个正确的名称,但我不知道它可能是什么。

newtype NatC n = NatC
  { unNatC :: forall p.
              p 'Z  -- base case
           -> (forall k. p k -> p ('S k))  -- inductive step
           -> p n }

这与Natty

同构
nattyToNatC :: Natty n -> NatC n
nattyToNatC n = NatC (\z s -> natty z s n)

natCToNatty :: NatC n -> Natty n
natCToNatty (NatC f) = f Zy Sy

现在我们可以为Nat编写一个我们知道如何消除的类:

class KnownC n where
  knownC :: NatC n
instance KnownC 'Z where
  knownC = NatC $ \z _ -> z
instance KnownC n => KnownC ('S n) where
  knownC = NatC $ \z s -> s $ unNatC knownC z s

现在这里是一个矢量类型(我已根据自己的喜好重新命名):

infixr 4 :<
data Vec :: Nat -> * -> * where
  (:<) :: t -> Vec n t -> Vec ('S n) t
  Nil :: Vec 'Z t

由于Vec的长度参数不是最后一个,我们必须将其翻转以与NatC一起使用:

newtype Flip f a n = {unFlip :: f n a}

induct2 :: f 'Z a
         -> (forall k. f k a -> f ('S k) a)
         -> NatC n -> f n a
induct2 z s n = unFlip $ unNatC n (Flip z) (\(Flip r) -> Flip (s r))

replC :: NatC n -> a -> Vec n a
replC n a = induct2 Nil (a :<) n

instance KnownC n => Applicative (Vec n) where
   pure = replC knownC
   (<*>) = ...

现在,如果在编译时知道向量长度,则pure向量将直接构建,不需要中间结构。