【问题标题】:How to make fixed-length vectors instance of Applicative?如何制作 Applicative 的固定长度向量实例?
【发布时间】:2018-03-10 07:46:58
【问题描述】:

我最近了解了推广,并决定尝试编写向量。

{-# 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

到目前为止,一切正常。但是我在尝试创建VectorApplicative 实例时遇到了问题。

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,它实际上适用于任何n

有什么更好、更清洁的方法?

【问题讨论】:

    标签: haskell types pattern-matching typeclass gadt


    【解决方案1】:

    你可以直接做同样的:

    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 实例提供一些上下文。

    【讨论】:

      【解决方案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)。可悲的是&lt;*&gt; 并不真正需要SingI n,因为原则上它可以从手头的两个向量重新计算它。但是,pure 没有输入向量,因此只能与提供的单例模式匹配。

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

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

      这并不完全等效,需要在以后的所有函数中添加上下文 Applicative (Vector n) =&gt;,其中 n 未知,但对于许多用途来说已经足够了。

      【讨论】:

      • $(singletons [d| 语法是什么?例如,在这种情况下,它对 Nat 做了什么?我是否也可以手动生成单例,如果可以,那么如何? (只是想了解它是如何工作的。)
      • @Liisi 这里发生了很多事情。 $(singletons ... 是 Template Haskell 语法,在编译期间调用 singletons 函数。该函数返回 Haskell 语法,然后编译。这将产生一个带有构造函数SZero,SNext 的GADT SNat n,其想法是每个n 将只有一个SNat n 类型的值。这样,n 上的编译时间信息在运行时进行。可以手动定义它(加上它的SingI 实例),但它是无聊的重复样板。您可以在 GHCi 中加载罚款并使用 :i SNat 来查看一些结果。
      【解决方案3】:

      将此视为@chi 答案的附录,以提供对单例方法的更多解释...

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

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

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

      我尝试使用与论文大致相同的命名约定:Natty 相同,ZeroTyNextTy 对应论文的 ZySy

      就其本身而言,这个显式单例很有用。例如,见论文中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 n 上下文中,我们可以调用 vcopies natty 以提供 vcopies 及其显式 natty 参数,这允许我们编写:

      instance NATTY n => Applicative (Vector n) where
        (<*>) = vapp
        pure = vcopies natty
      

      使用上面vcopiesnatty的定义,以及下面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
      

      (&lt;*&gt;) 的定义不再进行类型检查。问题是第二个(&lt;*&gt;) 案例左侧的NATTY n 约束不会自动暗示右侧的NATTY n1 约束(其中Next n ~ n1),所以GHC 不会想让我们在右边打电话给(&lt;*&gt;)。在这种情况下,由于第一次使用该约束后实际上并不需要该约束,因此没有NATTY 约束的辅助函数,即vapp,可以解决该问题。

      @chi 在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)
      

      这需要ScopedTypeVariablesRankNTypes

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

      {-# 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
        |])
      

      自动生成单例(使用构造函数SZeroSNat 代替ZeroTyNatTy;使用类型SNat 代替Natty)和隐式单例类(改为SingINATTY 并使用函数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)
      

      有关singletons 库的功能及其构建方式的更多信息,我建议阅读Introduction to Singletons

      【讨论】:

      • 我真的很喜欢你的vapp/vcopies 方法。很干净。
      【解决方案4】:

      其他几个答案引入了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
      

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

      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 向量将直接构建,不需要中间结构。

      【讨论】:

        猜你喜欢
        • 2019-10-04
        • 2023-04-08
        • 2012-08-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-02
        • 1970-01-01
        • 2018-01-14
        相关资源
        最近更新 更多