何时在Haskell中使用存在类型与依赖对?

时间:2018-04-21 15:51:59

标签: haskell singleton dependent-type existential-type type-level-computation

何时想要使用专门的存在类型与依赖对(也称为依赖和或sigma类型)?

这是一个例子。

以下是长度索引列表和依赖类型的复制函数。有关如何实施replicateVect的信息,请参见this other question。以下是使用singletons库:

data Vect :: Type -> Nat -> Type where
  VNil :: Vect a 0
  VCons :: a -> Vect a n -> Vect a (n + 1)

replicateVect :: forall n a. SNat n -> a -> Vect a n

有(至少)两种可能的方法来创建一个复制函数,该函数采用普通Natural而非单SNat

一种方法是为Vect创建专门的存在类型。我按照SomeVect

的惯例称呼singletons
data SomeVect :: Type -> Type where
  SomeVect :: forall a n. Vect a n -> SomeVect a

replicateExistentialVect :: forall a. Natural -> a -> SomeVect a
replicateExistentialVect nat a =
  case toSing nat of
    SomeSing sNat -> SomeVect $ replicateVect sNat a

另一种方法是使用依赖对。这使用singletons中的Sigma类型:

replicateSigmaVect :: forall n a. Natural -> a -> Sigma Nat (TyCon (Vect a))
replicateSigmaVect nat a =
  case toSing nat of
    SomeSing sNat -> sNat :&: replicateVect sNat a

这些功能看起来非常相似。使用replicateExistentialVectreplicteSigmaVect也非常相似:

testReplicateExistentialVect :: IO ()
testReplicateExistentialVect =
  case replicateExistentialVect 3 "hello" of
    SomeVect vect -> print vect

testReplicateSigmaVect :: IO ()
testReplicateSigmaVect =
  case replicateSigmaVect 3 "hello" of
    _ :&: vect -> print vect

可以找到完整的代码here

这让我想到了我的问题。

  1. 我应该何时使用专门的存在类型(如SomeVect)与从属对(如Sigma)?
  2. 是否有任何功能可以用其中一种编写?
  3. 是否有任何功能使用其中一个更容易编写?

1 个答案:

答案 0 :(得分:5)

  
      
  1. 我应该何时使用专门的存在类型(如SomeVect)与从属对(如Sigma)?
  2.   

回答这个问题有点棘手,因为:

  1. Sigma本身就是一种专门的存在主义类型。
  2. 创建专门的存在类型的方法有很多种 - SomeVectSigma只是这种现象的两个例子。
  3. 尽管如此,它确实感觉Sigma与GHC中存在类型的其他编码方式略有不同。让我们试着找出究竟是什么让它与众不同。

    首先,让我们全面地写出Sigma的定义:

        data Sigma (s :: Type) :: (s ~> Type) -> Type where
          (:&:) :: forall s (t :: s ~> Type) (x :: s).
                   Sing x -> Apply t x -> Sigma s t
    

    为了进行比较,我还定义了一个典型的"存在类型:

        data Ex :: (s -> Type) -> Type where
          MkEx :: forall s (t :: s -> Type) (x :: s).
                  t x -> Ex t
    

    让我们回顾两者之间的差异:

    1. Sigma s t有两个类型参数,而Ex t只有一个。这不是一个非常重要的区别,实际上,您只能使用一个参数来编写Sigma

      data Sigma :: (s ~> Type) -> Type where
        (:&:) :: forall s (t :: s ~> Type) (x :: s).
                 Sing x -> Apply t x -> Sigma t
      

      Ex使用两个参数:

      data Ex (s :: Type) :: (s -> Type) -> Type where
        MkEx :: forall s (t :: s -> Type) (x :: s).
                t x -> Ex s t
      

      我选择在Sigma中使用两个参数的唯一原因是更接近地匹配其他语言中的从属对的表示,例如在Idris's DPair中。它也可能使Sigma s t∃ (x ∈ s). t(x)之间的类比更加清晰。

    2. 更重要的是,Sigma的最后一个参数s ~> Type的种类与Ex的参数类型不同, s -> Type。特别是,差异在(~>)(->)种类之间。后者(->)是熟悉的函数箭头,而前者(~>)singletons中的一种去功能化符号。

      什么是defunctionalization符号,为什么他们需要自己的类型?他们在论文Promoting Functions to Type Families in Haskell的第4.3节中进行了解释,但我会尝试在此处提供精简版本。从本质上讲,我们希望能够编写类型系列,如:

      type family Positive (n :: Nat) :: Type where
        Positive Z     = Void
        Positive (S _) = ()
      

      并且能够使用Sigma Nat Positive类型。但这不会起作用,因为你不能部分应用像Positive这样的类型系列。幸运的是,defunctionalization技巧让我们可以解决这个问题。使用以下定义:

      data TyFun :: Type -> Type -> Type
      
      type a ~> b = TyFun a b -> Type
      infixr 0 ~>
      
      type family Apply (f :: k1 ~> k2) (x :: k1) :: k2
      

      我们可以为Positive定义 defunctionalization symbol ,让我们可以部分应用它:

      data PositiveSym0 :: Nat ~> Type
      type instance Apply PositiveSym0 n = Positive n
      

      现在,在Sigma Nat PositiveSym0类型中,第二个字段的类型为Apply PositiveSym0 x,或仅为Positive x。因此,(~>)在某种意义上比(->)更通用,因为它允许我们使用更多类型而不是(->)

      (如果有帮助,可以将(~>)视为无法匹配的函数的类型,如Richard Eisenberg's thesis的第4.2.4节所述,而(->) matchable 函数的种类。)

    3. 虽然MkEx构造函数只有一个字段,但(:&:)构造函数有一个附加字段(类型为Sing x)。有两个原因。一个是根据定义,存储这个额外字段是使Sigma作为一个从属对的部分原因,这使我们可以通过projSigma1函数来检索它。另一个原因是,如果您取出Sing x字段:

      data Sigma (s :: Type) :: (s ~> Type) -> Type where
        (:&:) :: forall s (t :: s ~> Type) (x :: s).
                 Apply t x -> Sigma s t
      

      然后这个定义需要AllowAmbiguousTypes,因为x类型变量是不明确的。这可能很麻烦,因此有一个明确的Sing x字段可以避免这种情况。

    4. 现在我已经完成了冗长的解释,让我试着回答你的问题:

        
          
      1. 我应该何时使用专门的存在类型(如SomeVect)与从属对(如Sigma)?
      2.   

      我认为这最终是个人品味的问题。 Sigma很好,因为它非常简单,但您可能会发现定义专门的存在类型会使您的代码更容易理解。 (但也请看下面的警告。)

        
          
      1. 是否有任何只能用其中一种写入的功能?
      2.   

      我认为我之前的Sigma Nat PositiveSym0示例会被视为您无法对Ex执行的操作,因为它需要利用(~>)类型。另一方面,您也可以定义:

      data SomePositiveNat :: Type where
        SPN :: Sing (n :: Nat) -> Positive n -> SomePositiveNat
      

      因此,您在技术上并不需要(~>)来执行此操作。

      此外,我不知道为projSigma1编写Ex等效内容的方法,因为它没有存储足够的信息以便能够编写此内容。

      另一方面,Sigma s t要求Sings个实例,如果没有SigmaSigma可能是(~>)不会工作。

        
          
      1. 是否有任何功能使用其中一个更容易编写?
      2.   

      当您迫切需要使用(->)类型的内容时,您可以更轻松地使用Ex,因为这是它闪耀的地方。如果你的类型只能使用s -> Type类型,那么使用"典型的"可能会更方便。像s ~> Type这样的存在类型,因为否则你必须以TyCon的形式引入噪音,以便将某种类型Sigma提升为Sing x

      此外,如果能够方便地检索{{1}}类型的字段,您可能会发现{{1}}更容易使用。